Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Two thoughts:

1) The failure of

    func (t *fishTank) fishCount() string {
      return fmt.Sprintf("How many fishies? %d!", t.size())
    }
to work has nothing to do with generics; methods are always "removed" from types defined this way. If you want you can cast `t` to a `*container[fish]` and call it, but this is also where I would ask why "extend" rather than compose - are you gaining anything by ensuring identical layouts?

2) The bigger issue, that interfaces cannot require their implementations to be comparable, is https://github.com/golang/go/issues/52614 and linked tickets.



The answer is complete. I just make some additional explanations here.

1) is totally generics unrelated.

    package main
    
    type T1 int

    func (T1) M() {}
    
    type T2 T1
    
    func main() {
        var t2 T2
        t2.M() // t2.M undefined
    }

The fishTank should be defined as

    type fishTank struct {
        container[fish]
    }
instead.

2) is a temporary restriction, among many in the Go 1.18. Some of the restrictions will be removed from future Go versions.

ref:

* https://go101.org/generics/101.html

* https://github.com/golang/go/issues/50646#issuecomment-10237...

* https://github.com/golang/go/issues/51257

* https://github.com/golang/go/issues/51338

* https://github.com/golang/go/issues/52474

* https://github.com/golang/go/issues/52509

* https://github.com/golang/go/issues/52531

* https://github.com/golang/go/issues/52614


Re. "temporary restriction": Although there's agreement something should improve around `comparable`, there's no concrete design approved to solve it. It may persist for many versions, or indefinitely.

(Vs. e.g re-enabling better type inference where the goal is known and it's "just" implementation work.)


There will be some permanent restrictions in Go custom design, but I think the `comparalbe` one is not one of them. My personal prediction is it will be solved before Go 1.21.


Thanks. But why are methods removed from types defined this way? Is there a good discussion about this somewhere?


Go doesn’t have inheritance. Defining one type in terms of another is not a subclass. It has embedding, which is a close substitute based on composition, but it isn’t the same.

See: https://go.dev/doc/faq#inheritance and https://go.dev/doc/effective_go#embedding

It’s a little confusing because for primitive operations, it kind of looks like Go has inheritance, but this isn’t true for user-defined methods. (And automatic casting might in some cases compound the confusion.)


> automatic casting might in some cases compound the confusion.

This is why you should always be careful to distinguish conversions (which aren't automatic) from assignment of untyped constants/literals (which does automatically attach the type, but isn't casting).


FWIW, it's the same in Haskell:

  data T1 = T1 Int
  
  m :: T1 -> ()
  m t = ()
  
  newtype T2 = T2 T1
  
  main :: IO ()
  main =
    let t2 = T2 (T1 23)
        v = m t2
    in return ()
Gets:

  main.hs:11:15: error:
       \* Couldn't match expected type `T1' with actual type `T2'
       \* In the first argument of `m', namely `t2'
         In the expression: m t2
         In an equation for `v': v = m t2
      |
   11 |         v = m t2
      |               ^^


Go tries to make each design element orthogonal. Type definition is not type embedding.

There may be some discussions about this, hidden in the go-nuts forum. But I don't know how to filter them out.


I'd say prior to generics, removing methods but keeping the same layout was the main reason to use this kind of declaration for non-primitive types.


I think Tim got confused between typedef (`type T Thing`, aka newtyping) and aliasing (`type T = Thing`).


But why (1)? If a `fishTank` is a `container[fish]` what is the benefit of requiring it to be cast again?


> But why (1)? If a `fishTank` is a `container[fish]`

But a `fishTank` is not a `container[fish]`.

In common parlance

    type T Thing
is newtyping. T has the same implementation as the underlying type, and Go allows conversions back and forth, but they are otherwise unrelated, and the new type does not share the interface (method set) of the original.

It is, in a way, a shorthand for

    type T struct { _0 Thing }


Oh I misunderstood `type T Thing` to be an alias and not a haskell-style newtype.

  $ ghci
  GHCi, version 8.8.4: https://www.haskell.org/ghc/  :? for help
  Prelude> class HasField a where { field1 :: a -> Int }
  Prelude> data Foo = Foo
  Prelude> instance HasField Foo where { field1 f = 2 }
  Prelude> field1 Foo
  2
  Prelude> newtype Bar = Bar Foo
  Prelude> :t Bar
  Bar :: Foo -> Bar
  Prelude> field1 (Bar Foo)
  <interactive>:7:1: error:
      • No instance for (HasField Bar) arising from a use of 
  ‘field1’
      • In the expression: field1 (Bar Foo)
        In an equation for ‘it’: it = field1 (Bar Foo)
  Prelude> type Baz = Foo
  Prelude> field1 (Foo :: Baz)
  2


There's an unfortunately-close syntax that does define type aliases in Go,

    type T = Thing


When people say Go is not an OO language, this is what they mean. Concrete type declarations define storage, not is-a hierarchies. A fishTank is not a container[fish], any more or less than it is any other `{ string, []fish }`.


And yet OO-isms are replete throughout implementations in go. `interface{}` all the things is another of the worst things about go. It practically begs for engineers to engage in all the worst practices of OO-ism and unit test zealotry.


interface{} isn’t pervasive in Go, it doesn’t tend to get abused often in practice, it has nothing to do with OO at all, and it doesn’t lead to any particular ideology about unit testing. It seems like you are mistaken on all counts.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: