How do you model a "zero or one" relationship without null?
Maybe your answer is "with Optional" (or Option, or Maybe). We just choose to use union types and have "Nil | T" (Nil or T) be the same as "Option(T)" in other languages.
Well, in Crystal Nil is a separate type that can be combined with others. But, say, a String is always a String, it doesn't implicitly have the Nil type. Same goes with every other type.
Maybe you are thinking of Java/C#, where reference types can also be null, but this is not true in Crystal. It's also in a way similar (but not quite) to Swift, where optional types are different than types that can't be null.
Do you have a technical write-up of how Crystal does that? Or otherwise some links/papers that explain the principle in a language accessible for someone who is basically self-taught and lacks a formal CompSci education?
I find the distinction important as it allows to establish a contract without falling into defensive programming. Null pointer analysis is great, I admit, but how it would help to write a library function without checking explicitly if its parameter is nil?
They both accomplish the same thing: no unexpected null. The exact mechanism was never the interesting part. Maybe being monadic does offer some other benefits but the killer advantage is the responsible handling of nil. Crystal doesn't seem to have a foundation in monadic programming so the type unions seem like a reasonable approach there.
The advantage of the monadic approach is that it's easy to abstract over, because it's just an ordinary type in the language. So e.g. in scala I can call the same "sequence" method on a List[Option[Int]] as I do on a List[Future[Int]] or a List[ErrorMessage \/ Int]].
Unions seem like more of a language-level feature, so I'm not sure you could abstract over them in the same way.
I totally agree. Ridding yourself of nils via the Maybe monad offers incredible abstraction potential, but would feel out of place in Crystal's Rubyishness without deeper thought into bringing other monads into play too.
Nonetheless, I am thrilled that we are seeing more and more languages that don't have implicit nullability on any type.
I re-read my initial comment and can see some ambiguity in my phrasing "Maybe being monadic ...". That wasn't me saying "maybe it is true that being monadic..." but rather "the fact that Maybe is monadic..."
That just depends upon whatever type abstraction capabilities Crystal grows. It probably won't be getting higher-kinded nominal types so generalizing over Monad is sort of dead in the water anyway.
A tiny observation here: we don't do "null pointer analysis". We do this kind of analysis for every type: if you invoke a method on a type and that method doesn't exist, you get a compile error. In the case of a type union, all types must respond to that method. "Nil | T" is just one particular type union, but you can also have "Int32 | String", etc.
Why would a constructor be confused with a type constructor? I guess I just don't have any experience with this particular approach to "sums". Untagged variants?