1. 14
  1.  

  2. 4

    You get some type safety but not as much as when you have an explicit subtyping relationships - if your method calls Artist.draw() and you pass it a Cowboy instead, you might get surprising runtime behaviour. In Scala “structural typing” is available, but it goes almost entirely unusued in real code - the slight overhead of using proper typeclasses is well worth it for the safety advantage.

    1. 6

      The downside of nominal typing is that you can build some brittle hierarchies. It’s taken years to move Applicative in as a superclass of Monad in Haskell due to the massive amount of code that it broke. On the other hand, if things were purely structural it would have taken just a few seconds.

      A structural system with an awareness of laws always seemed like an interesting point in the design space to me.

      1. 3

        Have you ever had this problem in practice? I haven’t. Sounds like a synthetic example. Go’s been typed “enough” to make things easy to write but useful.

        1. 5

          Yes I have - scala-arm includes a structural type to allow you to treat anything with a close() method as a managed resource, which broke when I used it with a third-party library type with a close() method which behaved differently. Declaring that you implement an interface means more than that you have methods with particular names; it’s an assertion that you’re conforming to the laws of that particular interface.

          1. 1

            Declaring that you implement an interface means more than that you have methods with particular names

            In Go implementing an Interface means you conform to the full method signature. Is it not enough to support the exact same signature? If the return type is what is expected, I would think, that would be enough. What are the pitfalls?

            1. 5

              It’s not enough, because the same signature doesn’t necessarily mean the same semantics.

              Say you pass a file handle (or similar) to the method. Is the method expected to close it? If the two sides disagree you’ll get a resource leak or an error from early closing.

              Say you pass a callback to the method. Is this callback going to be called exactly once? One or more times? Possibly zero times?

              Is it safe to cache the result of a call to this method and reuse it, or does the method need to be called every time because it performs some important side effect?

              You can lift a lot of these concerns into the type system (e.g. by treating errors as values, “might this method throw an exception?” is not a concern in go), but since go doesn’t have higher-kinded types (or indeed generic types at all) it would be cumbersome to do this with any “custom” effect (e.g. “does this method log audit events?” is very unlikely to be part of the method’s type signature in go, whereas in e.g. Haskell you probably would define a type that expressed that).

              1. 1

                Say you pass a file handle (or similar) to the method. Is the method expected to close it? If the two sides disagree you’ll get a resource leak or an error from early closing.

                An io.Reader versus an io.ReadCloser. Interfaces in go are very simple and can be composed. The two sides agree because the interfaces should attempt to be clear in naming… naming is hard.

                Say you pass a callback to the method. Is this callback going to be called exactly once? One or more times? Possibly zero times?

                OnceCaller, ManyCaller, MaybeCaller, respectively. Naming is hard, but I bet you could make the intention clear. Also, documentation helps.

                Is it safe to cache the result of a call to this method and reuse it, or does the method need to be called every time because it performs some important side effect?

                That goes into some major questions about how you like to design your systems doesn’t it? Having every possible layer do its own caching can be very memory inefficient (layers and layers of caching and state management). If it is cachable, I would hope the function would do that so I don’t have to think about it. Also, again, documentation.

                “does this method log audit events?”

                That is an interesting question… I feel I would need more information, obviously the name could be FooAuditBar… or it could take a interface that is a ReadAuditCloser… or it could use optional upgrading, take a ReadCloser and check in the function if it is also an Auditor, and if so, Audit, else don’t – which would be optional auditing. Also, at the risk of repeating myself, documentation.

                1. 3

                  OnceCaller, ManyCaller, MaybeCaller, respectively.

                  And the go compiler will do nothing to enforce it - that was the original point. If both interfaces just contain void callback() then any OnceCaller will be treated as a ManyCaller and vice versa.

                  Naming is hard, but I bet you could make the intention clear. Also, documentation helps.

                  If you’re willing to rely on naming and documentation then why bother with a type system at all? If you’re going to pay the overhead of a type system then surely it’s worth going that little bit further to make it one that can express every property you care about.

                  Having every possible layer do its own caching can be very memory inefficient (layers and layers of caching and state management). If it is cachable, I would hope the function would do that so I don’t have to think about it.

                  You want it to happen at exactly one layer, but you don’t always want that to be the lowest possible layer. Maybe you always combine the results of three functions, in which case it would be better to cache the combined result rather than each individually. Maybe you’re writing a library where some applications might want caching and others might not.

                  There’s an elegant solution with a type system: use a generic wrapper type to mark whether a value is something that should probably be cached, or has already been cached - the type system then enforces that any candidate value passes through a caching layer exactly once. In a language with higher-kinded types, you’ve then already got a bunch of library functions for doing anything you might want to do, e.g. compose together a list of cache-candidate values to a single cache-candidate list.

                  obviously the name could be FooAuditBar… or it could take a interface that is a ReadAuditCloser… or it could use optional upgrading, take a ReadCloser and check in the function if it is also an Auditor, and if so, Audit, else don’t – which would be optional auditing. Also, at the risk of repeating myself, documentation.

                  With a cross-cutting concern like audit events, or error handling, or async, you don’t really want to include it in the method name because it’s rarely the main focus of the method; you want to call the method sendMoney or whatever its primary purpose is, not sendMoneySynchronouslyAndLogTransferAndHandleErrors. Documentation is a fallback; types are better because it’s automatically enforced that they’re kept up-to-date. Especially in the optional case, it’s really good to see in the method signature that it behaves differently depending on what type it’s passed, otherwise it can be quite surprising when it happens.

          2. 3

            This is easily the most frustrating part of Typescript’s interfaces which are similarly structural only.

            On the other hand, OCaml’s module signatures are a structural discipline as well and I love using them. I think due to the existence of abstract local types it becomes significantly easier to write interfaces which have enough typing information to, while not actually capture laws, at least be difficult to collide with.

          3. 1

            That’s a beautiful example. :)

          4. 2

            One downside of the implicit approach: it means that you always pay the overhead for dynamic dispatch, since you cannot monomorphize. At least, that’s my current understanding.

            1. 2

              Golang method calls on concrete types (structs and the like) are monomorphic, so you don’t pay the cost of dynamic dispatch. Only calls via interfaces pay the dynamic dispatch overhead. The caller, not the callee, decides whether they need polymorphism. This seems Ian Taylor blogged about the implementation details of this in gccgo in 2009. Unfortunately he seems to have come down with the Google Omerta at some point since then.

              1. 1

                Right, I was speaking about calls via interfaces. With explicit annotations, you can get monomorphized calls, and hence static dispatch. So it’s convenient, but you do pay a cost.

                1. 1

                  I don’t follow. How does declaring that a type implements an interface prevent dynamic dispatch at a polymorphic call site? Presumably you can only make it monomorphic if you can prove statically what concrete type you have, which you can do equally well (or poorly, as the case may be) whether the implementation was declared or inferred. For example, Haskell requires you declare implementations, but still requires dictionary passing at runtime.

                  1. 1

                    Ah! So, I’m slightly conflating two things, come to think of it. Due to Rust.

                    fn foo<T: Bar>(x: T) // <- static dispatched
                    
                    fn foo(x: &Bar) // <- dynamic dispatched
                    

                    Haskell / Go basically always end up with the second case, but it’s not so much exactly due to the annotation, specifically.

                    1. 1

                      Yeah, you’re conflating static overloading and interface dispatch.

                      I don’t really know Rust all that well yet, but is Bar equivalent in both of those expressions? It seems like a Trait in the former and a concrete type in the later case. If my understanding of Rust is correct, then this is exactly the same situation in Go, only there’s just one syntax for it:

                      func (x Bar) foo() { ...
                      

                      That’s dynamic dispatch if Bar is defined as an interface type:

                      type Bar interface { ...
                      

                      Or it’s a static dispatch when Bar is any other kind: primitives, structs, etc. Go overloads the method call syntax, not method resolution. It can get away with this because it only offers single-dispatch. Rust needs the disambiguating syntax to deal with more sophisticated static resolution involving multiple arguments and type bounds.

                      1. 1

                        It’s not equivalent: it’s a value in the first, and behind a pointer in the other. The first one gets monomorphized, the second uses a vtable and dynamic dispatch.

                        Yeah, the second form is identical to the Go. You can get either with any trait, though, not just primitives.

                        1. 1

                          Ah, OK, I see. So the non-pointer case (c'mon now, pointers are values too :-P) the compiler will error if T can’t be resolved to a concrete type.