1. 21
  1.  

  2. 19

    Alternative 1a is to use Alternative 1 with https://github.com/BurntSushi/go-sumtype /plug

    go-sumtype requires the interface to be sealed (which you’re already doing) and one small annotation:

    //go-sumtype:decl TheInterfaceName
    

    Then you just run go-sumtype

    $ go-sumtype $(go list ./... | grep -v vendor)
    

    and it will do exhaustiveness checks in any type switch in which TheInterfaceName participates. This will prevent the “For example, during a refactor a handler might be removed but a type that implements the interface is not.” failure mode mentioned in the article.

    1. 2

      This is really cool, thanks for working on this!

    2. 6

      I really need to get around to writing my sum type proposal for Go.

      Instead of introducing an entirely new feature, the idea is to tweak the existing features to support it.

      The bare idea is simple: “closed” interfaces. If you declare an interface as closed, you pre-declare all the types that belong to it, and that’s it. The syntax could be something like

      type Variant1 struct {..}
      type Variant2 struct {..}
      type Foo interface {
          // methods
          for Variant1, Variant2
      }
      

      You continue to use type switches (i love type switches) with these interfaces, except that the default case can’t be used for exhaustive switches (you can also enforce that in non-exhaustive switches).

      It would also be nice to lift the restriction for implementing methods on interfaces; and make it be possible to run interface methods on explicitly-interface (not downcasted) types. There was a proposal for that too..

      Under the hood, these could possibly be implemented as stack-based discriminated unions instead of vtable’d pointers, though there might be tricky GC interaction there.

      I haven’t really written this up properly, but I suspect it might “fit well” in Go and be nicer than directly adding sum types as a new thing.

      1. 4

        I encourage you to make this suggestion on https://github.com/golang/go/issues/19412 which was recently marked as “For Investigation” which suggests someone is collating ideas.

        1. 2

          +1, that’s an excellent thread with a lot of interesting insights about the constraints that the core language devs are battling with when introducing a new feature like this. It’s super long but I’ve found the discussion to be really informative.

          1. 1

            I actually just wrote https://manishearth.github.io/blog/2018/02/01/a-rough-proposal-for-sum-types-in-go/ yesterday

            But I don’t have the time/desire to really push for this. Feel free to use this proposal if you would like to push for it!

        2. 7

          This only covers a small fraction of the use case of sum types; namely, when there is a small set of standardized tasks that is shared across multiple types.

          You probably wouldn’t even use a sum type for this in Haskell or Rust; you would use a typeclass or a trait, which is basically what the author ended up doing in Go.

          By far the most useful feature of sum types (and further generalizations on multi-constructor types, like GADTs) is the exact representation of types with non-power-of-2 cardinalities. It’s hard to appreciate this if you’re used to working without it, but this single feature probably eliminates (conservatively) 60-70% of logic bugs I would make in languages like C or Java. I am not aware of any pattern or technique that satisfyingly reproduces this power in languages without native sum types.

          1. 3

            Could you give a simple example of that which a Go programmer might run into?

            1. 3

              The classic example is the null pointer. You want to represent either your data structure D or some special case representing absence or whatever. This has cardinality |D| + 1. The null pointer is the traditional way to express this, and it’s bad for obvious reasons.

              Second most straightforward example is you have two different data structures depending on the situation. Let’s say an error description or a success result. This has size |D| + |E|.

              Parsers are one of the most recognizable scenarios where you have types with weird sizes, corresponding to the various clauses of the grammar. This is, I believe, one of the primary things ADTs were invented for.

              One I ran into recently was representing a bunch of instructions in an ISA and their respective arguments.

            2. 2

              when there is a small set of standardized tasks that is shared across multiple types

              Isn’t this what interfaces are for?

              By far the most useful feature of sum types […] is the exact representation of types with non-power-of-2 cardinalities

              It would be great if you could provide an example of how this is useful.

            3. 4

              I use this approach:

              type EventSum struct { 
                  EventPublish *PublishData
                  EventSubscribe *SubscribeData
              }
              

              Only one field is ever non-nil. You can then let the “sum type” implement the Event interface by delegating all calls to the first non-nil field.

              Of course the type system does not check and guarantee the “only one non nil entry” property, but this is easy to ensure “manually”.

              1. 2

                I originally saw this approach in the Kubernetes source. Here’s an example.

                The VolumeSource type is in effect a sum type

                VolumeSource = HostPath | EmptyDir | GCEPersistentDisk | ...
                
              2. 4

                I dislike these kinds of posts because instead of discussing effective uses of Go they discuss how to imitate language X in Go. That’s just not an appealing way to use a programming language.

                1. 6

                  Many developers learning LISP and functional languages have said it changed how they think about some problems with their coding style picking up on that. Some people also imitate useful idioms to get their benefits. So, with no claim about this one, I think it’s always worth considering in general how one might expand a language’s capabilities.

                  Double true if it has clean metaprogramming. :)

                  1. 2

                    I don’t entirely disagree. Maybe it’s just the quality of most of these posts that leave something to be desired.

                  2. 6

                    The goal of the post was to show how you would solve problems in Go that you would commonly use sum types for in other languages; not how to “get” sum types in Go.

                    I agree that the first two approaches are trying to do imitate sum types, and there are disadvantages to that. But I would argue that using a vistor pattern is quite different, and is the “Go way” (as in it’s the only way that works harmoniously with the type system).