9 ms·
A rough proposal for sum types in Go (2018)
- xiaodai 5y agoStop trying to make Go complicate. Stop try my to add templates. Just stop. KISS
- kadoban 5y agoYes please! I really dislike every golang function returning "foo*, error" when it really means either foo or an error.
- feffe 5y agoSum types would be nice to describe protocols such a grpc as well (given suitable syntactic sugar to switch on them). I think sum types, possibly in combination with tuples would have been nicer than multiple return values. But I guess everyone has their particular wish list of what a programming language should be :-)
- pkaye 5y agoSo why not use Rust instead since it already implements sum types?
- deleted 5y ago[deleted]
- ziml77 5y agoBecause Rust throws out the GC which means that you have to deal with complexities of lifetime management. (I don't agree that sum types should be added to Go, as much as I dislike the language as it is)
- BenFrantzDale 5y agoAs someone coming from C++ I don’t comprehend this complaint. How do you even do anything in a language without deterministic destructors? All my experiences with garbage collection have been painful. RAII is so much easier to use.
- azth 5y agoFor resource management, Java/C# have try-with-resources/using statements. Python has `with`, golang has "defer". GC addresses other issues like memory sharing, cyclic data structures, etc.
- tialaramex 5y agoThey said they liked sum types, that doesn't automatically mean they want to learn Rust. I liked the STL containers when I read about them in the 1990s, but I don't like C++ and the existence of the containers did not change that.
- kadoban 5y agoBecause I have a job and am not yet a dictator at my company for technical decisions.
- Andys 5y agoWon't the generics feature type sets fulfill this use case?
- xh-dude 5y agoI feel like an object lesson here would be context.Context in Go, which sort of rhymes with Result types in a way … I figure this sort of programming in Go post-generics won’t emerge as uniform baseline but thematic variations will arise in some particular domains. But I really don’t know.
- akira2501 5y agoMany functions return foo AND the error. This is occasionally very useful.
- kadoban 5y agoOcassionally, but pretty rarely in my experience. Both types of functions should exist, it should not be so hard to return an either/or.
- tialaramex 5y agoThat's not an error then, or, that's not what I'd consider a result, or both. Compare Rust's https://doc.rust-lang.org/std/str/fn.from_utf8.html https://doc.rust-lang.org/std/str/fn.from_utf8.html If you've got a buffer full of bytes, which you hope are UTF8 text but maybe aren't, call std::str::from_utf8 and you either get back a string slice with your text in it, or you get a std::str::Utf8Error structure which explains what the problem was in an actionable format. You can write code to process this Utf8Error and your bytes - maybe you're reading blocks of data and it's all valid UTF8 but the last character is continued in the next block for example, you can discern that from Utf8Error and just turn the valid part into UTF8 and keep the rest until the next block is available.
- baby 5y agoNot an error then, it doesn't make semantic sense.
- __s 5y agoWhen it's occasionally useful return (type, err), but generally the two results are distinct so there's no need to return two results in the general case when one suffices Indeed, I recently updated some code to return a relevant value in the error case: https://github.com/wal-g/wal-g/pull/1143/files#diff-d896e5d50cc3693c055fd9a6cfbfaccd1cc82fc3aaea3881c38cf9eb15a63b50R240 https://github.com/wal-g/wal-g/pull/1143/files#diff-d896e5d5... where true means the error is rooted in a corrupted page, & false means that the error isn't due to a corrupted page. It does feel a little dirty & a comment is probably necessary to make this case clear Still, Result can work here. For example, Rust's binary_search https://doc.rust-lang.org/std/vec/struct.Vec.html#method.binary_search https://doc.rust-lang.org/std/vec/struct.Vec.html#method.bin... which returns Ok(idx) when that's the location of the element, or Err(idx) when that's the location the element would be if it were there (this can be a bit annoying when you just want to insert, since you end up writing `let idx = match result { Ok(idx) => idx, Err(idx) => idx }`)
- bborud 5y agoI disliked it initially. After 5-6 years of using Go as my primary language I’ve come to appreciate it. It is clearer than having to figure out what is returned. And if an error can be returned. It’s easier when it is right there in the function signature.
- Zababa 5y agoThe thing sum types would allow is to make a clear distinction between functions that return either a result or an error and functions that can return both a result and an error. Many functions in Go will always return either res, nil or nil, err. Those functions would benefit from a way to show that in the signature, to differentiate them from the functions that can return res, err at the same time. This is not a replacement for multiple return values, as there are many cases where multiple return value are useful.
- monocasa 5y agoBut a Result<foo*, error> is clearer than foo*, error. I think that's the point the parent is making.
- kadoban 5y agoExactly, at least it's clearer in the vast majority of cases where that's what I actually mean (but golang won't let me encode easily). The way golang ~makes me encodes it makes using these functions annoying, because then you ~need to test for the nonsense cases where you have neither an error or a result, and maybe where you have both.
- bborud 5y agoBut how is it clearer? I can see the point of where you get neither error nor result, but you can get that in both scenarios, so I don't see how anything is gained. And you still have the same burden of checking whether an error condition did arise. That is the bit that usually matters when you call a function: knowing if it has error conditions (many functions don't) and checking if/what they are. I don't see any practical advantage here. Explain it to me like I'm five :)
- Ciantic 5y agoThat's right. This is about the language too, if Go doesn't have pattern matching, using sum types (or tagged unions) is a bit gnarly. This is the reason for instance in C# Hot Chocolate GraphQL library the newly proposed return type for fallible mutations is, e.g. for hypothethtical register function: { user: Option<User>, errors: [Errors] } Instead of more type-correct way of: union result = user | Errors Precisely because using unions is gnarly in JS who are the main consumers of the API. Similarly if GO doesn't have a way to pattern match, the actually using sum types is very annoying and this "foo*, error" can be seen as more convinient.
- Zababa 5y agoThe problem here is not the absence of pattern matching, it is the absence of the "tag" part of tagged unions. Union types like the one you presented (union result = user | Errors) are different from sum types, as they don't have a tag. Without the tag you need a way to know the type of the result at runtime. For example in TypeScript, you can use a type assertion and control-flow analysis to emulate sum types and pattern matching: https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#union-types https://www.typescriptlang.org/docs/handbook/2/everyday-type.... While sum types and union types are close, they are not exactly the same. Let's take an example: union bools = bool | bool enum Bools = A(bool) | B(bool) The example is a bit weird but shows an important point: bools has 2 values, true and false. Bools has 4 values, A(true), A(false), B(true), B(false). In bools (the union), true and true is always the same. In Bools (the sum/enum), A(true) is not the same as B(true). I don't know enough about type theory to know if union types have a relationship with sum types. They can be used the same for most cases but they have differences around the edges.
- Ciantic 5y agoYeah I was a bit sloppy when I wrote my answer. With GraphQL you can mimick tagged unions by giving each branch an object type. I actully wrote just few days ago about how cool it would be if TypeScript-like language too had tags: https://github.com/Ciantic/thoughts/blob/master/2021/dynamic-language-with-tagged-unions.md https://github.com/Ciantic/thoughts/blob/master/2021/dynamic...
- ibraheemdev 5y agoThe type sets proposal for Go has already been accepted as a clarification to the generics proposal [0]: type SignedInteger interface { ~int | ~int8 | ~int16 | ~int32 | ~int64 } Interfaces that contain type sets are only allowed to be used in generic constraints. However, a future extension might permit the use of type sets in regular interface types: > We have proposed that constraints can embed some additional elements. With this proposal, any interface type that embeds anything other than an interface type can only be used as a constraint or as an embedded element in another constraint. A natural next step would be to permit using interface types that embed any type, or that embed these new elements, as an ordinary type, not just as a constraint. > We are not proposing that today. But the rules for type sets and methods set above describe how they would behave. Any type that is an element of the type set could be assigned to such an interface type. A value of such an interface type would permit calling any member of the corresponding method set. > This would permit a version of what other languages call sum types or union types. It would be a Go interface type to which only specific types could be assigned. Such an interface type could still take the value nil, of course, so it would not be quite the same as a typical sum type. > In any case, this is something to consider in a future proposal, not this one. This along with exhaustive type switches would bring Go something close to the sum types of Rust and Swift. [0]: https://github.com/golang/go/issues/45346 https://github.com/golang/go/issues/45346
- pcwalton 5y agoThat looks like a great feature to add to Go! It seems to make the original article (which is from 2018) obsolete.
- saghm 5y agoOne difference between this proposal and Rust enums (i.e. tagged unions) is that enums let you use the same type more than once with a different tag. Obviously Go doesn't have generics yet, but something like `Result<String, String>` doesn't seem like it would be straightforward as a non-generic type either with a type set, since you don't have any way of differentiating between which "type" of string you might have. I _think_ this might be possible with a typeset by defining a newtype for one or both of the string types, but I haven't used Go in long enough that I don't remember if newtypes will implicitly convert to the type they wrap or not.
- jerf 5y agoWhat does this get you that you don't get by putting an unexported method in your Go interface today? Honest question, since I'm not 100% sure I'm sharing the terminology of the author, and I don't know if this is a matter of the author being unaware of this possibility, or aware and not satisfied for some reason I don't understand. You don't get a literal enumeration of all implemented types in the interface specification itself, but it's still trivial/mechanical to extract all implementations of such an interface with some code tool, and no external package can add to the list. (Since I've learned from experience this always comes up: If you put a method named 'unexported' on your interface, it doesn't matter if a value in some other package creates a method called 'unexported', the compiler will not consider it a match. An interface in some package with an unexported method can not be implemented by any other package.)
- renlo 5y agoI think a difference is that current behavior is to check the interface at runtime vs static check ensuring that the interface's underlying value is one of those types. Have personally seen a number of bugs (ie panics) from the runtime checks
- ben0x539 5y agoIn this proposal, the compiler will yell at you if you switch over the contained type of a value of the interface type and don't have a case for each possible type.
- kibwen 5y agoNote that this is from three years ago, and should have (2018) appended to the title.
- dang 5y agoAdded. Thanks!
- titzer 5y agoI didn't see a discussion of equality checking here, so I'll mention it. It's really useful if sum types (aka variants or algebraic data types) do not have identity, i.e. that creating two identical values with the same tag (or case) with the same values compare equal. This allows the compiler to represent sums efficiently, e.g. by packing two 32-bit values into a single 64-bit word. With reference identity (two constructed values are only equivalent if they refer to the same on-heap representation), then this optimization becomes harder (basically only works with escape analysis). Virgil has variants and also enums. They are slightly different things, though treated much the same under the hood. Neither have identity. (How they are different is that enums can have arguments in Virgil, but there are still is a fixed list of named values, whereas variants have named cases and are potentially recursive).
- stevefan1999 5y agobut golang already have a sum type: multiple return values
- saghm 5y agoMultiple returns are a product, not a sum; in the case of (*Foo, error), you could return nil for both, or neither, which gives you four possible combinations (nil and nil, nil and an error, Foo and nil, and Foo and an error). A sum type would not let you return both a Foo and an error, just one or the other.
- eksapsy 5y agoThis is a huge misunderstanding of what Sum types are. Someone already answered why multiple return types are completely different from sum types, I will also mention that a type is usually something that you define for variable types like int and string and structs, not only for what a function returns.
- mafalda 5y agoI’m not a daily go programmer. I’ve learned the language two years ago and know some the background behind it. Based on that I don’t see a place for sum types in Go because it does not solve a problem that is hard to solve with modern Go code. But considering the proposal itself. I would approach it much differently. Sum types could be represented as a closed enum. type Result enum int{…} const ( Ok Result = iota{interface{}}, Err Result = iota{string} ) The initialization would be done like: Ok{10} Err{“Something went wrong”} The variable itself would act just like and other enum with unpacking being done somewhat close to this: Ok{variableName} := result Where the variableName would be set to the zero value in case of a bad match. The enum prefix would enforce a single const block and no extension outside the module package.
- valenterry 5y agoYou are seeing it through the lense of an application developer. As a library author the world looks different though - here you cannot know/define the types in advance.
- valenterry 5y agoGo still seems to be a bit behind. Sumtypes should be standard in every new language - and better type-systems also offer union types now.