And I very much don't envision that the Go compiler would be modified to make sure that `x.value` is never referenced unless the presence of that value has been confirmed by an earlier statement.
The compiler enforces it by the fact that you can't pass in the return value from one thing (that might not be a valid value) into the next function that takes an argument that must be a valid value. Your objection that "but I can unwrap/call value without checking so it's useless" is a commonly raised objection - but it's a false one.
With nulls
var x = get_something
do_something(x) // ok even if x is null
With option/maybe x = get_something
do_something(x) // compiler err
x = get_something
do_something(x.value) // ok, *concious action by developer*
x = get_something
if x.hasvalue
do_something(x) // ok
The difference in safety between having to make a concius unwrap IS the whole point of the feature. This works exactly the same everywhere. You can always ".unwrap()" in Rust for example. Having to unwrap, or unwrapping without checking first isn't wrong, in fact it's often right. Unwrapping the value first means "extract the value if it exists, or otherwise panic right here". This decision to "assume success or otherwise panic" is a clear intent that the next developer can read. x = fetch_an_int // x is definitely an int
y = fetch_an_obj // y is *maybe* a T. case wrapped_foo of
NONE => ...
| SOME NONE => ...
| SOME (SOME NONE) => ...
| SOME (SOME (SOME foo)) => ...
On the other hand, with your proposed alternative, you have to create a wrapper struct for every nullable kind of thingy: type MaybeFoo struct { foo *Foo }
type MaybeMaybeFoo struct { foo *MaybeFoo }
type MaybeMaybeMaybeFoo struct { foo *MaybeMaybeFoo }
And, as if that weren't offensive enough, you have to manually pack those pointers into structs and then unpack them back.2) I would consider explicitly nested option types to be a code smell. If `foo option option` happens incidentally via a module functor or something, fine, but if you ever write that by hand, you probably want to explicitly flatten that to a 3 or 4 case sum type with explicit constructor names.
3) Nested structures like this are very unlikely to have synthetic, abstract names like MaybeMaybeFoo. Instead they are more likely to have operational, domain-specific, concrete names names. These structs are likely to _already exist_, since Go lacks type inference for function parameters: You're already going to have to create types in order to talk about these things.
4) You're "manually" packing/unpacking by writing (SOME (SOME ... constructor calls and in your pattern match. The difference between &Foo{&Bar{...}} and (Foo (Bar ...)) is irrelevant. Meanwhile, pattern matching has a syntactic advantage over a `== nil` check, but again, I'd argue that intentional nesting of numerous expected absent values is a code smell. If you really need that, you'd probably employ the null object pattern, which is easy to support in Go because methods can be called on nil instances. I'll say: I've never had to do that in tens of thousands of lines of production Go.
2) What if, in client code, I want to make a value of type “foo option”, completely unaware that foo's underlying implementation is “bar option”? Is it still a code smell? Do I have to know what abstract types defined by other people are implemented as options, so that I don't wrap them in options of my own?
3) Meh.
4) My pattern matching is completely safe. OTOH, with your proposal, you can't safely unwrap those nested structs of nullable pointers in a single step, because you need to unwrap the first layer just to check whether the second layer is safe to unwrap.
---
2) The problem exists whether there exist non-disjoint types or not. Oftentimes I do want to use nested types, because that's the way the data is naturally structured, i.e., that's what shortens the description of the operations that act on the data. Flattening the nested type into a single sum with 4 constructors would merely impose on myself of undoing the flattening every time I want to operate on this sum.
3) It's abstraction for the sake of making code easier to reuse and verify.
a nil *Foo with a nil *Bar
3) You say "meh", but abstraction for the sake of abstraction is exactly what drives working programmers away from so called "better" languages.4) Meh.
Anyway, I'm done with this thread.
If I have an instance of Option<Option<T>> then I'd have to unwrap it more than once. However, if the language has no concept of such automatic unwrapping, the chance that you would ever end up with a nested option is pretty slim. Basically, the monadic type use of these is an effect of the language doing that, not the other way. In e.g. C# where I use option types extensively I'm yet to see a wrapped one. In F# it's different.
In C# there is a feature in which nullables (e.g. int? and int?? or int???) actually work similarly because there are "lifted operators" which sort of half-achieve this, e.g.
int? x = 10;
int? y = 20;
int? z = x + y;
but to be honest this to me feels mostly quirky because it doesn't extend to much else in the language.And C# pays dearly for it. Why doesn't C# have anything like C++'s <algorithm>, or Rust's composable iterators, or Haskell's plethora of composable abstractions (foldables, traversables, lenses, etc.)?
> In C# there is a feature in which (...) but to be honest this to me feels mostly quirky because it doesn't extend to much else in the language.
Agreed. Hardcoded special cases are always a bad idea.
---
Sorry, HN won't allow me to make a new post because “I'm submitting too fast”, so here goes my reply:
C++ has a finer-grained hierarchy of iterator concepts:
(0) InputIterators correspond to C# IEnumerators.
(1) OutputIterators allow you to write to the sequence's current element.
(2) ForwardIterators allow you to dereference the current element arbitrarily many times before moving on to the next.
(3) BidirectionalIterators allow you to move both forward and backwards in the sequence of elements.
(4) RandomAccessIterators allow you to skip arbitrarily many positions in the sequence of elements in constant time.
And there are algorithms defined in terms of these refined iterator concepts.
I'm not very familiar with the details of Rust iterators yet, but there isn't much I'm missing from Linq at least yet (apart from the cost control available in Rust and C++ that it's pretty natural is omitted from C# where it's natural that operations such as iteration can heap allocate).
What is the difference between <algorithms> and the similar Linq (or rather the enumerable extensions enabling linq)?
I don't necessarily think that's a bad thing. Something that may be null is already an option type. It's "some T or null". Making the compiler force you into checking which it is, is no difference from checking for the presence of null. There is literally no difference in complexity - it's actually simpler because you don't have the cognitive overhead of remembering where a certain variable has and hasn't been null checked (After a null check, your variable has magically changed type to "some T, definitely not null".)
and The programmer alone has to remember this - remember to check, and the status of each variable that can be null. Was x checked before? scroll up to see... Add 10 variables and some nested ifs and ask the programmer which variables can and can't be null at any given place. This is the job of a compiler, not a programmer. And if my experience is any guide, the less experienced programmers, which are the ones we are discussing now, would be happy to have 100 locals or 12 levels of nested ifs in a method. They need this added help more than the people who ask for it.