Hacker News new | ask | show | jobs
by malcolmgreaves 2188 days ago
Since this post is illustrating the use of generics, why not go all the way with the get implementation to return an Option[V] type? It's a natural thing to do here. The return type is already a kind of sum type: it's either the value you want (non-zero value, true) or it's not (zero value, false). If the implementation uses the optional type, it'll become impossible to write code that uses the zeroed-out returned value incorrectly. Calling code must always explicitly check the returned Optional[V] value to access the value of V and continue or to perform some code to handle the not present case. As it stands, it's very possible to ignore the 2nd returned boolean value and write code that'll easily break.

Now, I can see why the author would _not_ want to do this, since this "explosion" of sum-typed things is present in all go code (e.g. the err := ...; if err == nil { ... pattern). So, it might be easier for Go programmers to see how they could use generics in their own code by re-using this pattern. However, I think this is a disservice to why generics are an incredibly useful construct in programming languages. They can be used to align code more closely with the semantics that the programmer wants to convey.

6 comments

> If the implementation uses the optional type, it'll become impossible to write code that uses the zeroed-out returned value incorrectly.

Since Go doesn't have sum types, it would most likely be possible: the option type would just be a reification of the MRV. At best it could panic if you try to get the value of an "empty" optional but now you've got a panic.

It’s always possible, even in a Rust Option you’ll panic when you extract the value with unwrap() without knowing that it’s valid.

What it does is preventing the accidental use of a missing value. You can’t pass the Option<T> on to a function taking a T without explicitly doing it.

This doesn't add anything. You get the same protection in Go with pointers. For example, a `*T` can't be passed to a function taking a `T` without explicitly dereferencing it. The problem of course is that the type system doesn't guarantee that the pointer isn't nil when you go to dereference it, similarly your `Option<T>` doesn't guarantee that the option.Value is set correctly. You need sum types to provide this substantial guarantee.
Pointers in go aren't a signal for optionality, they're a signal for stack or heap or optionality. They also don't play nice with interfaces.

Let's say I have a hashmap that returns a `&T` and I have a `func foo(s Stringer)`. Because 'Stringer' is an interface, it's possible for it to take both `T` and `&T` without a compile-time complaint.

In addition, I may wish to have my function `bar(t &T)` always take a pointer because I want the object to exist on the heap, or to be able to mutate its value for the caller.

Since pointers mean so many other things, they're not a good way to have a compile-error indicate optionality.

On the other hand, if my caller does `hm.Get` and gets an `Option<s: Stringer>`, it's clear what to do to pass it to foo, and if it's an `Option<&T>`, it's clear both that I want a pointer, and that it should be checked / unwrapped.

I agree that `T` / `&T` would be just as powerful as option types in go (without sum types) if pointers didn't already have other substantial meaning, and if they could sensibly interact with interfaces.

As it stands, I think you're off the mark though.

(note: All the asterisks are & because I dunno how to escape stuff on hn)

> Pointers in go aren't a signal for optionality, they're a signal for stack or heap

No, a pointer in Go doesn't mean that it's on the heap. The compiler keeps it on the stack if it's safe to do so, regardless of whether you're using pointers.

You can even write code like `t := new(T); t.Foo()` that very much looks like you're allocating on the heap, but it can stay on the stack, yet t is then a pointer to the stack.

Unlike C, you don't need to worry about the heap-vs-stack in Go. It's never even mentioned in the language spec as a concept people need to be concerned with. It's an implementation detail.

> Unlike C, you don't need to worry about the heap-vs-stack in Go. […] It's an implementation detail.

I'm really surprised to read that: yes a beginner can get his code working without wondering about stack vs heap (and that's one of the big reason why Go is easier to learn than Rust for people coming from non-system language), but as soon as you care about performance (which many Go users do!), you need to write code that reduces allocations to the minimum, because Go's allocator is really slow (compared to Java for instance). Interestingly enough, doing so forces you to think about the ownership and lifetime of your objects, like you'd do in C (or Rust).

This is not entirely true. You can exhaust stack headroom by calling functions with very large pass-by-value structs. In these situations, you are then forced to use a pointer, which results in heap allocation.
It guarantees that the value is set if the flag is saying it is set (that's the invariant of the type). It screams "check flag before accessing the value".

To compare with references which are also 0-or-1-thing effectively: A reference where you as a developer know it's never null but always a ref to exactly one thing is denoted "* T" and a reference to "one thing or null" is denoted "* T"! There is no difference in the types! so you can accidentally send one that is "0-or-1-things" to a method accepting a * T that MUST be a thing. Type system didn't help you document which case it was.

Options, apart from the annotation benefit it also helps making the syntax nicer in many cases, with e.g. "or()" fallbacks etc.

    let data = get_cached().or(load_from_disk()).or_panic();
I agree that there are semantic issues with pointers, but my point was to illustrate that you need sum types in Go to get any real benefit out of an Option type. If you need an option type and you aren't content with a pointer, you can use `(T, bool)`, but this is still a far cry from a real Option type.
I use Option<T> types very happily in C# without sum types. Most of what would be pattern matching can be done with just methods.

    Car c = maybeCar.GetValueOr(CreateCar()) // inline fallback 

    maybeDog.Do(d => b.Bark()) // only performs call if present

    Sailboat s = mybeBoat.As<SailBoat>() // none unless of correct subtype
And so on. With nullable reference types C# now has a builtin alternative to this, but it has worked we’ll for many years.
I like the idea, I just haven't given it a try with the new draft yet. Sounds like it's worth exploring at the very least.
Cool! I'm glad you read my comment. I appreciate that you went through the effort to make a blog post (my comment is very low effort in comparison). I hope it came off as constructive.
I really wish Go would pursue sum types instead of generics
Sum types and Generics solve different but related problems. So they are not really comparable. I would like to see both get implemented.
I'd say it's really hard to abuse sum types, I wouldn't say the same of generics.
That is true. However sum types don't allow us to abstract over common code. For example a sum type wouldn't allow us to create a type safe channel "fan in" function that works for all channel types.
Sum types without generics will still be good enough? I think for things like Option/Result we need both sum types and generics.
I don't think you would need user generics for this to work.
Without generics, sum types would have to be a built-in like maps and arrays.

With generics, discriminated unions can take on all sorts of user-defined shapes.

Without generics, structs can take on all sorts of user-defined shapes. It's just that the types of its fields are fixed at declaration.

Without generics but with sum types, the sum types can still take on all sorts of user-defined shapes. It's just that the types of its various alternatives are fixed at declaration.

Non-generic sum types are useful but you can’t build an option or result type out of them, so the usefulness is somewhat hampered.
But you don't need to, generic Option and Result types could be built as part of the standard library.
Yes but then you need neither sum types nor user-defined generics, you can just build-in whatever sugar is useful or necessary (possibly repurposing existing sugar e.g. the `select` statement).
Aren't the new interfaces from the draft, where you can list bunch of possible types, sum types?

edit: oh. no.

https://go.googlesource.com/proposal/+/refs/heads/master/des...

I don't use Go myself but knowing its philosophy I wonder if they'll end up replacing the idiomatic "if err == nil" with an generic optional type even if they end up implementing generics in Go.

For one thing it would generate a massive amount of churn to upgrade existing code, and if you don't update you'll quickly end up with very ugly mixed error handling patterns. On top of that Go seems to really value compilation speed, so I suspect that they won't want generics "contaminating" interfaces all over the place only to do error handling.

I'm really curious to see (from the outside) how all of this is going to coalesce in the end.

Generics aren't the gap (multiple return values are already generic), but rather sum types. Sum types are what allow you to express that this is either None/Sum(T) (Option) or Ok(T)/Err(E) (Result) or Nil/Cons (List) or etc.
Sum types are what give you compile-time safety over those options. Run-time safety and developer-intent-signaling is entirely feasible with just generics.
Right, but as previously mentioned, Go's multiple return values are already "generic" and already signal developer intent. If you have a library method that returns (int, err) every single Go developer will check the err first before using the int. User-defined generics don't improve this use case.
Result types can, at runtime, by making the err case panic if the value is accessed, instead of just returning a zero value. They let you move past intent and into enforcement. Multiple returns are nothing but intent, and cannot be made stronger.

You can do that without generics, of course. But the developer overhead is large enough that it effectively does not happen, as you have to redo that by hand for every type. That's what generics bring - ergonomics good enough to stop using less safe workarounds (e.g. `interface{}`, multiple returns).

Take a look at the comment tree starting here in the Generics announcement thread from the other day for some discussion of Option[V] under the current proposal:

https://news.ycombinator.com/item?id=23545361

For what it worth, returning `(value, err)` is conceptually the same thing as returning a Result[V]. You can ignore the error case on a result / the None case on an option as easily as you ignore the `err` today.

> You can ignore the error case on a result / the None case on an option as easily as you ignore the `err` today.

The point of a result type is that you _cannot_ ignore the None case. Any method that would provide you with the value will also check that the error is not present.

In comparison, you can happily ignore `err` in Golang and continue with an invalid `value`.

I see what you're saying, and while I think you're right, I think a lot more of the value is in being able to use `Map()` /`FlatMap()` to be able to avoid thinking about error checking at all, as I laid out in the second part of this comment https://news.ycombinator.com/item?id=23549396 . The convention of returning `(value, err)` goes a long way towards enforcing the checking of errors; I have golangci-lint's `errcheck` linter enabled and basically cannot accidentally ignore the `err` values. In any case, Option/Result types are extremely useful and I'm happy that with generics the language will be able to include them!
For an ergonomic implementation of Result and Option you need to be able to represent tagged unions/enums/sum types, and having pattern matching makes dealing with them much more natural. These features are not planned for Go (yet?), right?
> In comparison, you can happily ignore `err` in Golang and continue with an invalid `value`.

I'm struggling to envision a Result type that requires you to be more explicit than `foo, _ := fallible()`. Seems like `fallible().Ok()` and similar are strictly less explicit.

To do anything with a Result/Optional, you have to explicitly get the value inside the box at some point. But the container abstraction also gives you a nice composition abstractions instead of the uncomposable top-level `val, err := do()` destructure.

Though perhaps not quite as compelling without real sum types. I'd have to play with Go's generic-typing sandbox more to form a stronger opinion.

> returning `(value, err)` is conceptually the same thing as returning a Result[V]. You can ignore the error case on a result / the None case on an option as easily as you ignore the `err` today.

No, you can't. If you want to pull the value out of a result and ignore errors, you have to explicitly do so. With `(value, err)` style error handling, you can write bugs by simply forgetting to check `err`.

I'm not understanding this. Are you saying an Option[V] would reduce the number of lines of code (the explosion) that Go code uses for "err := ...; if err == nil"?
I don't think it would in Go. You'd still end up with

    result := ThingThatReturnsOption(...)
    if result.Error() != nil {
        // ...
    }
happening in general, or some other equivalent construct.

The main point of having Option in this case would be to make it so that where Go programmers normally write

    result, err := ThingThatMayError()
you can get precisely one of a result or an error. At the moment with the current calling convention it is possible to both return a result and an error.

However, I will say that while in theory this is advantageous (and I mean that seriously), in practice this is nearly a non-issue. I don't think I've ever had a bug because I had both things and misused them. I expect a dozen or more "Option" implementations to pop up nearly overnight once this is released, and for Go programmers to settle pretty quickly on not using it.

In non-Go languages, Options can have additional features that make them yet more powerful, such as chaining together optional computations in a way that makes it easy to shortcircuit whole computations, e.g., in Haskell:

     do
         x <- optionalFail
         y <- somethingElseFail x
         z <- moreMightFail x y
         return (extractFromZ z)
In Haskell, assuming the right definitions of the various functions, while that may look like it's not handling errors, it actually is, because the machinery behind their Option type (called Either in Haskell) is handling all the short circuiting. Go comprehensively lacks the features necessary to make that sufficiently pleasant to use that anyone will, though.

It is not unique in lacking those features, most languages are missing at least one thing to make it easy enough to use people will, but it does quite comprehensively lack them. It's not just a matter of adding this one little thing or that other thing, it'd be a whole suite of necessary changes, e.g., you might be able to write an .AndThen(...) function to operate on a Option type, but it's going to be too inconvenient to use, even post-generics, and even if you force it because it's the Right Thing to Do in languages that aren't the one you are currently programming in, it's still going to be a lot of disadvantages for not much advantage. Personally I don't value "Doing the Right Thing in language X while working in language Y" very highly, but some people seem to.

When you chain multiple things that can go wrong with Result[T,E] or Option[V] it should be possible assuming there are methods for chaining/fallback. E.g. opening a file, reading the contents, parsing the contents etc. If all those 3 return Result[T,E] and you want the overall result (parsed) T or the first error to occur, then you should be able to chain that.
I think they are saying using sum types will remove a source of errors (forgetting to check for error conditions).

I also think part of their remark is on ‘either’, not ‘option’, but that’s not important for the point being made.