When nil is not nil
bitmaybewise.substack.com
bitmaybewise.substack.com
"It is by far the most problematic part of language design. And it's a single value that -- ha ha ha ha -- that if only that wasn't there, imagine all the problems we wouldn't have, right? If type systems were designed that way. And some type systems are, and some type systems are getting there, but boy, trying to retrofit that on top of a type system that has null in the first place is quite an undertaking." -Anders Hejlsberg
https://news.ycombinator.com/item?id=19568681
That's one thing I will heartily credit C# for, despite not being a great fan of the language. Actually retrofitting null-safety in a language never designed for that was an impressive undertaking.
I love programming in C# myself, and think it's quite a nice language, carefully designed to say "Fuck You!" to Java, the same way Java was carefully designed to say "Fuck You!" to C++, which itself was insanely designed to say "Fuck You!" to C.
Whereas TypeScript, which I also enjoy and appreciate, was carefully designed to say "Fuck You!" to JavaScript, but JavaScript was less designed than just haphazardly thrown together as LiveScript, and then deceptively renamed and marketed to say "Me Too!" to Java.
Now to be safe I'm always explicit when checking for undefined and null values, and also try to limit how I use them.
The empty string ("") is also falsy. So is `false`, obviously. And the bigint 0. And NaN. Here is the complete truth table: https://262.ecma-international.org/#sec-toboolean
FWIW undefined and null are loosely equal to one another, it's one of the few cases where loose equality is legitimate. As in
if (blah != null)
will exclude null and undefined, and only those two[0]. That's step 2 and 3 of IsLooselyEqual: https://262.ecma-international.org/#sec-islooselyequal[0] and Document.all but if you're dealing with that you're beyond the reach of god
"Proof that Brendan Eich never really cared about equality."
The really short answer is that the problem stems from a incorrectly imported concept from C, where void pointers are all 0. If you treat Go as Go, and don't try to understand it as another language, you would understand there's no particular reason to treat nil the way people are treating it. It isn't a void pointer. There's also an issue where people incorrectly attribute the problem; it isn't where the nil (of one type) fails to compare equal to nil (of another type), it's where the value was created in the first place that didn't mean what a C-based understanding thought it meant. Go isn't C, just like any language X isn't language Y.
In reality, once you understand this issue correctly, it essentially dissolves simply because you have the correct understanding. I've been programming in Go now for years and I simply don't hit this problem. You do have to be willing to let go of your wrong ideas, but, it's not a matter of opinion about design or something; they are objectively wrong ideas about how the values in Go work. Once you move the problem from trying to "detect" the problem to not creating the problem in the first place, it's not hard.
Which is then annoying because if you want an optional integer you now need a pointer, and a heap allocation.
Pointers, referencing, dereferencing; that can get confusing even for skilled engineers.
So, guess if it's a pointer or an Option, and hope that your peers guess the same.
While I agree that's a problem, it's mostly a separate problem to the nil interface vs. interface containing a nil. The "nilness" provides a cognitively-convenient hook, especially with the attractive nuisance of the whole issue of nil/null in general, but the root cause of this particular issue is really "putting things into interfaces that don't fulfill the contract of the interface", which as I show in my post, doesn't require a nil anywhere in sight to do it.
When you stop doing that, this whole interface nil vs. containing a nil problem goes away. Basically, in Go code, you should never be trying to penetrate an interface value to see if it contains a nil. If you're doing that, the error occurred when the value you are examining was created, not at the point of use.
In fact, stop putting values that can't implement interface contracts into interface values and other code issues just go away, too. Not just in Go, either. Most large dynamically-typed language programs I've ever worked in are also shot through with this exact issue, it's just less visible because the compiler doesn't help you with the interfaces. They're still there, though. Just in the dark.
So I don’t think the authors of the language see it the same way you do.
(FWIW I was also a Go style reviewer working with the Go team for a few years, and I’ve never heard this idea from them in that time.)
Because effectively, basic pointer behavior (nil check) is dangerously (panics) different depending on returning a nil interface or nil *struct (both are interchangeable).
Granted, I don't run into this problem every day, but it's not due to "understanding the issue correctly".
I think you mean "null pointers are all 0". void pointers are something entirely different, and certainly not always 0.
When you understand something then you don’t have a problem understanding it any more.
But making the runtime go out of its way to store the exact type of an object you don’t have is damn weird. In theory a method could accept a nil receiver, but in practice they never seem to.
This means that
if x and x == nil
evaluates to true - a highly unusual condition in Lua. It makes sense if you think about it for a bit, but it really makes you scratch your head when you encounter this situation for the first time.We found that this had the added benefit of semantically checking for something that did not exist, versus some contextual "false" value.
I suspect this is a good practice in other languages as well.
if x and x == nil
ever evaluate to true in LuaJIT? What does the FFI have to do with it? Please explain.Edit:
I think I see what you mean here. The x variable is actually a CDATA object returned by the LuaJIT FFI. Upon comparison, the NULL pointer value inside would be converted to nil. I don't see this as being a 'footgun' unless you forget that you're dealing with a CDATA object.
local x = require('ffi').cast('void*', nil)
if x and x == nil then
print("x is truthy and nil at the same time")
endSo of course standard C# can report a NullReferenceException if you try to do something with an actual "null" value, but in Unity3D you can also have object references to C++ object wrappers (like GameObject) that override == and != to act like they're null in some cases, but report a different MissingReferenceException if you try to do other things with them. And that makes comparing Unity C++ objects to null much slower, since it has to thunk into native code to perform the comparison, not just use one machine instruction.
This causes a some very subtle bugs (and performance problems) because many people don't actually understand what's really going on behind the scenes. They don't realize there's another level of indirection and overriding going on, and just brute-force a solution by checking for null more often, without realizing the people getting confused really do have a valid reason for their confusion and aren't just sloppy programmers.
Notice how some of the forum replies in the links below just mansplain to the people reporting problems that they "simply" need to check for null harder as if they were dummies, without mentioning Unity's obscure == and != C++ object wrapper overriding hack that's the root of the problem, even though they did check for null (i.e. before calling a coroutine, which was too early), but it unexpectedly changed out from under them. This is a classic leaky abstraction and foot gun.
https://blog.theknightsofunity.com/story-nullreferenceexcept...
>But Unity objects are a bit different. Unity objects can report themselves as null even if they are still referencing an object!
>No, that’s not a bug! Some referenced objects are scene objects. Imagine what happens if a scene is being replaced by another scene – all regular scene objects have to be destroyed. Since Unity cannot update your valid references inside your code (they still will be valid after destroying the scene), it is faking a null check for those that you are still referring to, so it will (should) stop you from use these any longer. In case you do try to use any of these objects, you may receive a message like this one:
>MissingReferenceException: The object of type 'Texture2D' has been destroyed but you are still trying to access it. Your script should either check if it is null or you should not destroy the object.
So you can have a non-null reference in a variable, that apparently changes to a (virtual) null behind your back, without you ever changing the value of the variable! Here's an example of somebody who was confused by that, because they checked for null BEFORE entering a co-routine, and then even though they never reassigned the value of the variable in the co-routine, the GameObject that it referred to got destroyed, morphing the C++ GameObject wrapper into a virtual null reference behind their back. (i.e. an actual C# wrapper object that lies that it's equal to null, because the underlying C++ object it points to has been destroyed.)
It gets even more confusing when you mix it with coroutines and C# native implicit null checking operators ?. and ??.
https://forum.unity.com/threads/missingreferenceexception.17...
>Fake-null objects are an important requirement due to the fact that Unity is a C++ engine with explicit memory management and C# / .NET is a managed environment. Objects derived from UnityEngine.Object have an actual partner on the native code side. You can not destroy objects explicitly in the managed world. However we have the Destroy method to destroy any objects derived from UnityEngine.Object. What the method does is actually destroying the object on the native side and mark the managed wrapper object as "being destroyed". Such an object will essentially pretend that it is null since it's no longer useable since the native counterpart is missing. That fake null object will eventually be garbage collected once all references to it are gone.
Here's a good explanation from Lucas Meijer at Unity:
https://blog.unity.com/technology/custom-operator-should-we-...
>Custom == operator, should we keep it, by Lucas Meijer.
>[...] Going over all these upsides and downsides, if we were building our API from scratch, we would have chosen not to do a custom null check, but instead have a myObject.destroyed property you can use to check if the object is dead or not, and just live with the fact that we can no longer give better error messages in case you do invoke a function on a field that is null.
>[...] We're a bit nervous about that, as if you haven't read this blogpost, and most likely if you have, it's very easy to not realise this changed behaviour, especially since most people do not realise that this custom null check exists at all.[3] [...] This led to the "well if even we missed it, how many of our users will miss it?", which results in this blogpost :)
It's a common problem raised on the forums:
https://answers.unity.com/topics/missingreferenceexception.h...
https://answers.unity.com/questions/1705335/how-can-throw-mi...
https://stackoverflow.com/questions/58607172/missingreferenc...
https://forum.unity.com/threads/missingreferenceexception-ar...
https://forum.unity.com/threads/missingreferenceexception-i-...
Also note that null comparisons against Unity objects are considered "expensive" because of this hidden overriding of the equality operator.
https://blog.jetbrains.com/dotnet/2019/02/21/performance-ind...
https://github.com/JetBrains/resharper-unity/wiki/Avoid-null...
>Avoid null comparisons against UnityEngine.Object subclasses
>Classes deriving from Unity.Object inherit equality operators that change the behaviour of the == and != operators. While these operators will perform standard .NET reference equality, if comparing one side to null, these operators will call native code to check if the underlying native engine object is still alive. For more details on the background of this, please see the explanation of the "Possible unintended bypass of lifetime check of underlying Unity engine object" inspection.
>This transition to native code can be expensive, as Unity will perform lookups and validation to convert the script reference to the native reference. While small, the cost of comparing a Unity object to null is much more expensive than a comparison with a plain C# class, and should be avoided inside a performance critical context, and inside loops.
>This inspection will add a performance indicator highlight to null comparisons against UnityEngine.Object subclasses inside a performance critical context. It will also provide the following Alt+Enter context actions:
>Move outside loop. This will introduce a variable to hold the result and move the comparison outside the scope of a loop. Move to Start or Awake. This will introduce a field to hold the result and move the comparison to Start or Awake. This inspection was first added in Rider/ReSharper 2018.3
https://github.com/JetBrains/resharper-unity/wiki/Possible-u...
>Possible unintended bypass of lifetime check of underlying Unity engine object
>This is a Unity specific inspection. It only runs in a Unity project.
>This warning is shown if a type deriving from UnityEngine.Object uses either the null coalescing (??) or null propagation or conditional (?.) operators. These operators do not use the custom equality operators declared on UnityEngine.Object, and so bypass a check to see if the underlying native Unity engine object has been destroyed. An explicit null or boolean comparison, or a call to System.Object.ReferenceEquals() is preferred in order to clarify intent.
It pops up everywhere in python libraries. Core libs, like inspect.py.
I’ve never seen a good treatment of unset. There are a bunch of interesting corner cases, like how to store an unset value, or whether setting a hash map entry to unset should delete the entry, or whether it’s falsey. (Falsy?)
You want it so that you can tell whether an argument to a function wasn’t passed at all, vs whether someone explicitly set it to nil.
It pops up everywhere because Python has default values and sentinel objects.
In a different language with a different logic, it wouldn't pop up anywhere as much. And usually (though not always) it doesn't overlap with the need for a null (and / or the distinction doesn't matter).
When it does, you can just add a layer of indirection e.g. `Maybe (Maybe a)`. That's what you get if you store an option in a map, in a language with option types:
lookup :: Ord k => k -> Map k a -> Maybe a
so if you have a Map k (Maybe a)
(a map from any key to an optional value, aka your standard Java or JS or Python map) lookup will return a `Maybe (Maybe a)`, the outer Maybe is the result of the lookup, and the inner Maybe is the stored value. There is no information loss, and if you don't care about the distinction you can just `join` the result in order to collapse it.Being able to check if some identifier isn’t bound is useful, but as soon aa you do so by means of an “unset value”, you need abother way to detect if it was really unset or just set to the unset value.
> It pops up everywhere in python libraries
It pops up everywhere in Python libraries as a means of hacking around the fact that function defaults are evaluated at compile time, so, particularly with mutable defaults, you end up with a shared object rather than a fresh one with each default invocation, so a sentinel value is used to know you need to create a fresh value in the function body rather than using an default thar gets improperly shared.
Arguably, what Python mostly needs to streamline this isn’t a systematic handling of sentinel values but syntax for specifying that the default value is the return value of a specified callable rather than a specific function-definition-time value.
"Unset" has that problem. But a sentinel value for "unbound" is very doable under certain limitations. A language can have "being unbound" be exclusive to data structures, and "containing the sentinel value for being unbound" be exclusive to local values. Then it's never ambiguous. This also implies you won't have a "locals()" equivalent.
But nil/null/undefined doesn't seem like the best way to go about this when designing a language. Not every value is optional, but if anything can be nil then you will spend a lot of unit tests and mental overhead on managing that disconnect. Systems that allow you to explicitly specify what can have no value (like SQLs NotNull, or Rust's or c++'s Optional) seem to do much better in terms of preventing bugs.
_DEFAULT = object() # bonus points: make this reload safe
def frobnicate(bar=_DEFAULT):
if bar is _DEFAULT:
...
But really the main reason this comes up is because python went with the dumb decision to evaluate default args at defintion and not call time (unlike, say, common lisp etc.). def frobnicate(bar = object()): # object() evaluated once, at definition time
Python behaves as if by a transformation to: __hidden_global_42 = object(); # evaluation at definition time
def frobnicate(bar = __hidden_global_42):
which is exactly what you have done manually.Having spread that, I have to go wash my hands now.
In Go, every value is stored as a tuple: (concrete type, value). So (int32, 5) is not the same as (int64, 5), even though the numeric value is the same.
In general, the compiler will prevent comparisons that are always going to be false, such as comparing an int32 to an int64: https://go.dev/play/p/q0skdfUnWsD
The only time when you'll be able to compare two entities with identical values but different concrete types (and have your code compile) is when they're being compared through variables stored in an interface: https://go.dev/play/p/t0QDFh70Ut4.
This can be surprising when you first encounter it, but once you understand how it works, it's easier to remember.
With floats being the exception
And complex as they're basically a pair of floats.
And transitively any array or struct containing one of the exceptions.
So really, == is mostly not a bitwise comparison, when you think about it.
If == on string was a bitwise comparison, it would be an identity check. But two different strings with the same content compare equal, meaning go dereferences the buffer pointer.
The bits of a structure containing a string don't also include the contents of the string itself.
Floats are still an exception, but not the rest of that list.
A nil pointer is a valid value and can implement methods and satisfy an interface. An uninitialized interface cannot.
type foo struct {}
func (*foo) String() string { return "foo" }
type Stringer interface{ String() string }
func main() {
var x Stringer
x.String() // panics
x == nil // This is a nil interface value
x = (*foo)(nil)
x.String() // works
x != nil // This is not a nil interface. It is set to a valid implementation, even if it's implemented by a nil pointer.
}https://bitmaybewise.substack.com/p/when-nil-is-not-nil/comm...
I'm not fond of the proposal. Or rather, I'd prefer priority for non-nullable types. Skirts around the problem all together.
How do I know my youth is all spent?
My get up and go has got up and went
But in spite of it all, I'm able to grin
When I think of the places my get up has been
-Get Up and Go, Pete SeegerSubstack authors can have paid content, this is categorically different.
Anything an author marks as free, anyone can read. Substack doesn't have separate subscriptions for Substack, there's no "you've read your Nth free article!" etc.
Conversely, if something is paid, you have to pay for it.
Consequentially, if you see a Substack link here, you will be able to read it.
I see that more like big popup "PLEASE SIGN UP FOR EMAIL, THEY HAVE MY KIDS" overlays with the tiny X in one corner. It's annoying, the designer should probably be shoved in a locker, but it's not a paywall.