The fact is when you're working with any data coming from any other system, the data is or will become null, somehow, some way, and your program code which treats this as impossible is just literally wrong in a way that is complete jibberish. Additionally, programmers don't want to pass huge lists of parameters to every function, but instead bundle things into structs to be easily passed around, however this model makes it impossible to treat a value as Optional at an early part of the callstack and Non-optional later in the callstack after it's been checked and verified. So you either pass everything as a separate parameter, copy things into different structs all over the place, or just make the value Optional everywhere, deleting the usefulness of making Optionals.... Optional. Just let it be null everywhere, and if it's null somewhere it shouldn't be, the program throws an error--like it should, because there's an error.
Actually, javascript is the only language that has it right. Not only can anything be null, anything can be undefined, (which isn't even remotely similar and anyone who doesn't understand why doesn't belong in the conversation,) AND values you don't know about can exist.
That's fine though. The program that treats it as possible but fails to handle it in every possible spot is wrong too, but wrong in a completely unpredictable place, way and even number of times.
It's freeing to start out assuming "title" can never be null. If it is, you get a quick failure at the very edge of your application where the data comes in. You make it nullable, and the compiler tells you all the places where you made the wrong assumption and now need a fix. Or even better yet, you give it a default value, even an empty string, and nothing more is required.
Sanitizing input is a very common pattern and I don't see why this option is such a deal breaker. And it would normally just be needed at the system boundary, rather than "all over the place".
Or, you share types between different services using something like grpc or thrift, and then you really can trust that the values coming in are not going to be null.
I definitely don't agree that settling for implicit null is the best option.
i don't really understand this part. you can't treat it as impossible, because `Optional[T]` is a different type than `T`. anywhere you try to use an `Optional[T]` as a `T` is a type error.
i think you're saying "there are some cases where `T` can also be null that are now unaccounted for," which i don't think is really relevant. that's more a matter of API design; a hypothetical function that receives data from a server should return an `Optional[T]`. one that treats the operation as infallible would throw an exception just like in an implicitly null language, right?
> The fact is when you're working with any data coming from any other system, the data is or will become null, somehow, some way, and your program code which treats this as impossible is just literally wrong in a way that is complete jibberish.
Your program and the other program has to agree on some protocol. That can have optionality built into it if you expect them to not send certain fields. If that protocol is violated then either you made a mistake (protocol was not accurate) or they did (sent poorly formatted data); either way, you can handle the error without nulls.
> however this model makes it impossible to treat a value as Optional at an early part of the callstack and Non-optional later in the callstack after it's been checked and verified.
I think what’s being described here could be easily encoded as variants/sum types, or you could have functions that give optional fields default values. Without a specific example it’s hard to discuss what you’re really trying to say.
Optionals everywhere makes that explicit, vs. something implicit that you can forget.
If you mean `unknown`, that's a TypeScript thing.[0]
And personally I love working on a code-base and with coders that differentiate between null and unknown. But there's no denying it's a footgun and many get no value besides bugs from these two. It's kind of similar to how JS gets boolean comparisons weird, which is worked-around by convention
[0] https://www.typescriptlang.org/docs/handbook/release-notes/t...
I think they are very similar in that there are now 2 nully things to guard for.
Option types suck in Java as they were tagged on later. Maybe in Haskell, or even nicer, Maybe in Elm is were the party's at. It is build in at the center of the language. And getting a value (v) from a list by an index should give you a "Maybe v", as you could be out of bounds. That's the strong typing that basically keeps me safe when viciously attacking a code base I have not touched for some time (or is not written by me to begin with).
Adding optional arguments everywhere is a bit like declaring checked exceptions everywhere. It's true that optionals make the code more explicit, and they force developers to handle every situation, but in the end you still need developers who actually care about writing readable and maintainable code for it to be a net gain.
Something I have come across a fair bit is code where too many values are declared as optional, so the developers try to build an elegant solution by mapping functions over these optional values, returning a default at the end. If there is a value, then the expected code path gets called, else it just silently skips the rest of the code and returns back up to the top layer. This causes a similar issue to what can happen with promises - you lose the context of where the first "error" happened. This could be avoided by eagerly throwing an exception in the first absent case, but that takes us back to where we started with null.
I think the "best practice" way of using optional values is to get buy-in from all the developers on the project that basically nothing should be optional, ever. The goal, then, is to explicitly throw an exception (or return an error) at the very top level and ensure that only actual values enter into the main part of the project's code. But enforcing that is more of a management problem than a technical problem.
No, it doesn't; there's a couple easy, obvious ways to do that depending on how you get the data components and how far it is useful to pass them as a group without ensuring none are null. It's not even mildly challenging, much less impossible.
Yes. They are as important as records/structs a.k.a. product types.
> F#, OCaml, Reason, and Elixir
Haskell, Elm, PureScript, Kotlin, Idris, ...
> I know they add a significant amount of complexity to the language
Really? I dont think it can be so much more than generics :)
Elm has 'm and the whole language is 5k lines of code.
Generics... Well, I just come from a Elm gig (knowing Haskell/C++/Java/Kotlin/Ruby) and I must say I was not too bothered with the lack of generics there.
Elm has generics.
No. That would be fixed by having something akin to typeclasses, traits or interfaces.
Elm's List, Maybe and Array are all defined using a generic type parameter.
BTW, one doesn't have to chose between them in Go either, but only because there are under-the-hood generics for the native types, that are only accessible to the language designers.
https://en.wikipedia.org/wiki/Parametric_polymorphism
Go doesn't have implicit nullability. You have to declare that something is a pointer for it to be nullable.
There are a few kinds of pointers: "normal" pointers, slices, maps, function pointers, and interfaces. Any of those pointer types are nilable. Regular structs and primitives are not nilable.
var x someStruct = nil //this will not compile
var x int = nil //this also won't compileSorry, but to me that's a clear symptom of implicit nullability.
Edit: clarification, to me it does not show in the type signature that something may return a null, also I do not have to unpack a possible null value (like with Maybe or Option).
Your definition of "implicit nullability" is suspect here.
Go doesn't even implicitly de-reference things, which is what I think you're intending to say that it does. (EDIT: field access does cause a dereference, as was pointed out in a response to this comment. I still think of this as an explicit action when you're specifically operating on a pointer variable, but I'll concede this point here.)
Your position on this is really confusing at the moment. Go handles nullability in an extremely different way from Java.
Edit for your edit:
> clarification, to me it does not show in the type signature that something may return a null
The best way to know something won't be null is to use value types where possible. Don't return nilable values unless you have a good reason. When you return nilable values, only return nil when the error value isn't nil. This is a common pattern in Go code. Why would you return nil if there wasn't an error? If there was an error, why would you care what else was returned, outside of some very exceptional circumstances?
> also I do not have to unpack a possible null value (like with Maybe or Option).
I write Rust and Go professionally... I'm familiar with the pros and cons here, and I wish that Go would adopt Sum Types, but this has nothing to do with "implicit nullability", and in practice... it's really not a big deal. I've written huge amounts of Go code, and I just don't remember encountering nil exceptions in production except once in a blue moon -- and it is similarly common in Rust to hit an "expect" or "unwrap" that you thought was statically impossible to reach. Go has good editors (like GoLand) and linters (like golangci-lint) that make it easy to avoid most of the practical pitfalls.
And then make a Maybe sum type? But then it is too late as the std lib communicates with nils instead of Maybes
> The best way to know something won't be null is to use value types where possible. Don't return nilable values unless you have a good reason.
I'm not doing this, I'm using an API that does! I cannot choose what some code returns, all I can do is --sigh-- add another null guard.
If it is my discipline to add null guards or have runtime explosions I consider the nulls to be implicit.
That you find as much runtime problems with unwrap/expect in Rust (which have a lot of red tape on them in API docs) as with "missing null guards" in Go, is great for you, but to me that does not make it a good design choice in Go.
But they're still explicit nulls.
You seem to be saying that only implicit nulls require null guards, but that's just not what that means (as far as I have learned).
You seem to hate nullability as a concept, which is fine, but that's different from hating implicit nullability.
> That you find as much runtime problems with unwrap/expect in Rust (which have a lot of red tape on them in API docs) as with "missing null guards" in Go, is great for you, but to me that does not make it a good design choice in Go.
It's also a comment on how NPEs are just not a common hazard in Go compared to my past experiences with Java, especially when combined with good editors and linters.
Except that there may be other reasons to use pointers than just nullability. And in that case, there's no mechanism to specify whether or not the thing being pointed to is nullable.
I think that's where the implicitness in question comes in.
But I might well be mistaken.
Except when accessing a field via a pointer to struct.
It is just a bad contract, as it is often misunderstood, and it allow for runtime errors that could easily have been caught at compile time.
I agree that non-nullable references are wonderful, and Go will probably have to grapple with that eventually. I just don't want people to get the wrong idea. Go has value types -- not everything is a reference that is implicitly nullable.
C isn't implicitly nullable, you are correct.
I agree that Go should consider adopting non-nullable references.
That being said, implicit nullability leads to mostly human type of errors and comes from our limitation to fit all the parts of a complex system in our brains. I'm curious if there are example where machines write code and use plenty of implicit nullability without that causing any NPE issues ever.
Just a thought.
Well Haskell compiles to binary, or Elm compiles to JS. The resulting binary/JS is basically machine written and does not throw any NPE's.
It's certainly common, but of the things which would be useful to be able to constrain only to allow explicit and non-default nillability, pointers are pretty high on the list.
The same thing as is done for any other non-nullable value in static type systems.
But this is not easily reversed. Not as easy as tagging generics onto the language. And the parallels with Java's maturing are just lovely. Also, we have seen what Kotlin is now doing for Java: a new language was needed to truly fix that one mistake (implicit nullability), introduced in the grandparent of Java (namely C), and the great old grand uncle of Go.