Re:Is it time to stop using sentinel values for null / “NA” ...
by derbOac on Monday June 05, 2023 @10:51AM
(#36196968)
I think I'm following this but on the other end, I've often thought that there is a need for a NA class/type hierarchy, so the Arrow approach discussed in this piece might be too simple at some level?
I'm being really loose and am unfamiliar with Arrow but it seems as if these kinds of discussions are useful but also maybe heavily language-dependent. E.g., how Julia, Python, R, Rust, or whatever interfaces with Arrow is also relevant.
Treatment of null and missing data in the context of data science has rarely gotten the full consideration it probably warrants in design.
Re:Is it time to stop using sentinel values for null / “NA” ...
by thriftwy on Monday June 05, 2023 @10:59AM
(#36197089)
Arrow-based systems are also notorious for completely broken UIs/displaying of blank pages without missing a beat. You definitely do want some error reporting, however not in the form of SIGSEGV.
Re:Is it time to stop using sentinel values for null / “NA” ...
by ChrisRackauckas on Monday June 05, 2023 @12:05PM
(#36198363)
Julia has language-level support for missing values (https://docs.julialang.org/en/v1/manual/missing/) which acts like the solution here, but generally for any value that can be put in an array by doing `Union{Missing,T}`. The array type will automatically optimize by having a bitmap of represented values (for any singleton type this works, not just Missing).
There were lots of pre-v1.0 design discussions on this vs sentinels, such as https://discourse.julialang.org/t/representing-nullable-valu... which can be dug up. The conclusion was more about the safety than performance, since sentinel values are fundamentally unsafe in generally since they can take a valid value and make it "special" (like in the blog post, minimum integer). Thus Julia has the following 3 choices:
* Use `nothing`, which throws an error at each operation like `nothing + 1`. This requires `if x === nothing` handling everywhere, is the safest option, but is generally considered clunky for statistics use. As `nothing` is a singleton type, it has the same optimizations as what's mentioned in the blog post.
* Use `missing`, which propagates, i.e. `missing + 1 === missing`. This allows for any `Union{Missing,T}` to use a scheme similar to the blog post: as `nothing` is a singleton type, it has the same optimizations as what's mentioned in the blog post.
* Use a sentinel of your choice, risks known and dependent on the type.
And you can build your stats libraries however you so choose, though JuliaStats has strongly gravitated towards the middle option as something close in ergonomics to R's NA but while having a bit more safety guarantees and generality.
Re:Is it time to stop using sentinel values for null / “NA” ...
by derbOac on Monday June 05, 2023 @04:42PM
(#36203306)
Interesting. I was familiar with Missing and nothing in Julia but had never really thought about other options from a safety angle. It makes sense though.
I have run into issues where it would be nice to have some sort of missing type/class hierarchy though, in that the reason for missingness is coded somewhere and then how to represent that in those three categories gets complicated. At some level the different missing types get treated the same for many functions (such as your missing + 1 propagation example) but for other things not. Then you end up being tempted to use the third option which has its own issues. I suppose you could create another variable with type of missing information but then that also becomes complex quickly.
Re:Is it time to stop using sentinel values for null / “NA” ...
by LorenPechtel on Monday June 05, 2023 @05:14PM
(#36203697)
Yup, over the years I've had some pretty impressive gains from precalculating maps of various data items that are fundamentally bits and tend to be blocky in nature.
Re:Is it time to stop using sentinel values for null / “NA” ...
by ryandrake on Monday June 05, 2023 @11:00AM
(#36197130)
I used to work with GPS mapping software, and the number of engineers who would lazily use "lat:0.0, lon:0.0" as a default or "invalid" location on Earth was... a little scary.
When nuclear war breaks out, I am -not- going to want to be floating on a raft in the ocean off the coast of Africa at coordinates 0.0, 0.0 when the missiles fail and revert to their "default" target.
Re:Is it time to stop using sentinel values for null / “NA” ...
by NoboruWataya on Monday June 05, 2023 @12:56PM
(#36199234)
When you think about it, we're somewhat fortunate that (0, 0) is in the literal middle of nowhere. You can pretty much always safely say that a reading of (0, 0) is a error (unless you're working with the weather buoy that's located there, I guess). If there was a major metropolis there things could get a lot more confusing.
Re:Is it time to stop using sentinel values for null / “NA” ...
by jimktrains2 on Monday June 05, 2023 @01:03PM
(#36199370)
You mean like the house thats near the geographic center of the us and keep getting visits from the police because maxmind default to those coordinates for ip addresses.
Re:Is it time to stop using sentinel values for null / “NA” ...
by LorenPechtel on Monday June 05, 2023 @05:10PM
(#36203619)
This isn't a null problem, but a precision problem. (And it's a lot more than just that one place, the same problem exists at many levels.)
All location fixes inherently have a precision component and that routinely gets ignored. There's one tower that can reach the phone, the phone must be somewhere within the footprint of the tower. If the phone didn't supply more detailed info that's the only thing you can determine. The true area is the footprint but it gets reduced to a location and a precision--which maps it to a dot and a circle. A one-tower location fix will give a circle of uncertainty that extends off the visible map in most cases and humans ignore it. (The center of the US problem is a bit different but it's the same basic concept.)
What I think they need to do is prohibit any system from showing the central dot (or any equivalent to that) when the precision is too low. Only render the zone of uncertainty and only at a map scale where you see the whole zone, attempting to zoom beyond that gets you a message that the location can't be determined more accurately, do not try to find the center of the zone.
Re:Is it time to stop using sentinel values for null / “NA” ...
by jimktrains2 on Monday June 05, 2023 @07:22PM
(#36205733)
I was responding to the idea that it's good nothing is at null island because it's a default. I was showing that there are aome default locations that have people and there are consequences to that.
Re:Is it time to stop using sentinel values for null / “NA” ...
by HPsquared on Monday June 05, 2023 @02:27PM
(#36200783)
In the vein of Douglas Adams you could open "the restaurant in the centre of the world". Probably won't get many customers, but you might end up with an enormous number of misdirected parcels.
It's just another variant of that: having INT32_MIN is just as bad: these things inevitably end up unrepresented in the typesystem, someone writes a naïve function over that, and suddenly your "nulls" are being summed or computed on, particularly when they're rare in the dataset, or at least in the one the coder tested with.
(Even in SQL, where it's a separate value — NULL — it's still an awfully weak type system, so sanity is by no means assured.)
Give me must-be-explicitly-handled Option<T>.
(So, I'd argue that even if there were a performance loss, in most cases, the gain in correctness is the right tradeoff.)
Re:Is it time to stop using sentinel values for null / “NA” ...
by morelisp on Monday June 05, 2023 @11:45AM
(#36197989)
Most mainstream languages conflate memory layout and type definitions so your Option<T> is going to be implemented internally with worse-than-sentinel-values and take an even larger performance hit.
Re:Is it time to stop using sentinel values for null / “NA” ...
by morelisp on Monday June 05, 2023 @01:03PM
(#36199373)
Not sure how you come to this conclusion - the sentinel is a performance hit. If you're lucky enough to be working in a language and with the blessed types where it figures out a good sentinel for you, you're still paying the sentinel cost, just not any more than that.
Re:Is it time to stop using sentinel values for null / “NA” ...
by weinzierl on Tuesday June 06, 2023 @05:19AM
(#36210202)
The discriminant is certainly the bigger performance hit, converting to a sentinel when possible is the best we can do. So I don't se how it would be classified as performance hit at all when essentially this is what we couldn't hand code any better.
Re:Is it time to stop using sentinel values for null / “NA” ...
by morelisp on Tuesday June 06, 2023 @07:05AM
(#36210889)
> converting to a sentinel when possible is the best we can do.
Did you, uh, do the thing I can't ask if you did on HN? Because it's entirely about how to get better performance than sentinel values by specially structuring your discriminant.
Re:Is it time to stop using sentinel values for null / “NA” ...
by weinzierl on Tuesday June 06, 2023 @11:56AM
(#36214770)
EDIT: Ah, I think I understand where this went wrong. I was answering from my comment thread and did not see your other comments, especially the one where you explained how Julia handles this case. I did not know about that. I'm sorry, I didn't mean to berate you I just was under the wrong impression you said what you said because you were not aware of Rusts discriminant elision. My apologies.
ORIGINAL:
Not sure what you mean, so I'll try again.
An Option<T> in Rust should occupy the size of T plus the size of a discriminant. On a 64-bit machine for example an Option<i32> will take up 8 bytes. Four bytes for the i32 and four bytes (including alignment) for the discriminant. A 100% increase in space is clearly a performance hit.
If, however, T is a non-nullable type, the Rust compiler is smart enough to use this null value as a sentinel and you end up with an Option<T> that only takes up the size of T.
So, for example, both a &i32 and an Option<i32> will take 8 bytes, no difference, zero waste.
Re:Is it time to stop using sentinel values for null / “NA” ...
by morelisp on Monday June 05, 2023 @12:55PM
(#36199230)
C++ would be the obvious poster child.
It might be more instructive to list some exceptions:
- As described elsewhere in this thread, Julia is able to optimize Array<Option<X>> internally into a bitfield and a tightly-packed array of X
- Rust can use knowledge of a type's valid values to optimize Option<T> into something the same size of T for common T (pointers, arrays, etc, also notably I think user-defined enums which aren't exhaustive of their value space) - but as far as I know it cannot do what Julia does, you need to reach for e.g. https://docs.rs/vec-option/latest/vec_option/
- This is somewhat the raison d'etre for Jai, if it ever gets released.
Re:Is it time to stop using sentinel values for null / “NA” ...
by morelisp on Tuesday June 06, 2023 @07:01AM
(#36210871)
The compiler is only vaguely allowed to do this by general "as if" rules; in practice there are so many formal constraints on field layout I don't think it could do better even with the world's most brilliant optimizer. It would need to behave "as if" if the two fields are laid out next to each other, in address-ascending order, with nothing but padding in between.
Re:Is it time to stop using sentinel values for null / “NA” ...
by marcosdumay on Monday June 05, 2023 @02:08PM
(#36200448)
C and C++ do really not do that, and programing as if the memory structure is equivalent to the type structure is an undefined behavior factory that will turn your program into a mine field.
But those languages are very good at pretending they mix the two concepts.
Re:Is it time to stop using sentinel values for null / “NA” ...
by jmull on Monday June 05, 2023 @11:59AM
(#36198264)
I think you're conflating different things.
This is about how to represent a null values in memory (for a columnar data structure), not how null values are expressed and accessed in a particular language's type system. However null values are represented in memory, you'd want an appropriate language binding/reppresentation.
Re:Is it time to stop using sentinel values for null / “NA” ...
by deathanatos on Monday June 05, 2023 @02:29PM
(#36200808)
Yes, but only from a strictly theoretical view. One influences the other, and in particular, one makes the other much more difficult. Representing "INT32_MIN is None" in, e.g., Rust, is possible, but it's much more troublesome. I'd need something like Option<ASpecialI32>, where that inner type excludes the sentinel. Cf. representing "all i32s are valid, plus there's a None value", that's just Option<i32>.
And in practice, if someone has an odd sentinel value like that in the data format, library authors tend to be lazy and not take the time to do that wrapping; instead, they just expose the i32 directly, maybe Option<i32>, and sharp edges abound around the sentinel.
(That is, the data format — a sentient value — encourages buggy code. And that is the billion dollar mistake: that a nullptr pretends to be a pointer, while it is not, and gets swept up in the same operations.)
Re:Is it time to stop using sentinel values for null / “NA” ...
by Dylan16807 on Monday June 05, 2023 @04:06PM
(#36202784)
What's the memory representation of an Option<> then? Do you want to add 4 bytes to each value to hold the tag? Even if you do that, a lazy author might still provide raw access to the ints.
Re:Is it time to stop using sentinel values for null / “NA” ...
by cratermoon on Monday June 05, 2023 @12:02PM
(#36198305)
However, if you define a "null" number as 0, the summation works fine. Division by 0 can still fail, and that's good because the naïve coder will get a result like NaN or, in some languages, undefined behavior, alerting them to the problem.
Re:Is it time to stop using sentinel values for null / “NA” ...
by arthurjj on Monday June 05, 2023 @12:55PM
(#36199228)
>the summation works fine
This is only true if that summation is not used or compared anywhere else which isn't usually the case. For example if in sql you are reporting the SUM(column_A)/ SUM(column_B) and one column can be null without the other one being null you're likely reporting the wrong number
Re:Is it time to stop using sentinel values for null / “NA” ...
by richardjam73 on Monday June 05, 2023 @12:11PM
(#36198438)
I was under the impression that this article was talking about how to store nulls in memory not their usage. An Option type still needs to store either the value or the None value, which takes up the same space as having a Null value.
Re:Is it time to stop using sentinel values for null / “NA” ...
by deathanatos on Monday June 05, 2023 @05:48PM
(#36204351)
I mean … CSV hasn't a notion of Option, or Int, so you'd have to define your own.
But you don't have to use what the article calls a "sentinel value" (i.e., you don't have to dedicate -2489391827 as "null") to do it: simply "null" for None/Null/Nothing, and the integer for Some(<int>) / Just int would work.
Re:Is it time to stop using sentinel values for null / “NA” ...
by cratermoon on Monday June 05, 2023 @11:56AM
(#36198206)
There's a design pattern, known alternately as Null Object or Special Case, related to this. The general goal is to eliminate the need to add checks for null/NA/sentinel and simply define valid behavior for these cases. Eliminating boundary checks both simplifies the code and improves performance.
Re:Is it time to stop using sentinel values for null / “NA” ...
by jfengel on Monday June 05, 2023 @04:22PM
(#36203008)
There is no one solution to null. That's the point: "null" is the billion-dollar mistake because it solves so many different problems in exactly the same way.
To fix it, you have to go back and reconsider what it means for your data to lack that value. Is it a different type of thing? Does it mean that the value must exist but isn't known? Does it mean that it's really some default value? Does it mean that there's an error but you're just trying to muddle along? Does it mean you could actually have zero or more of something? Does it mean it's uninitialized?
You might try to solve any of these with a null. That's simple, but it does bad things to your type system. At least using sentinels makes it easier to reason about your type system, but if you're just using it as a "typed null" then you've just hidden the problem further.
I find that the better you design your types to model the real problem domain, the less you need nulls or sentinels or other hacks. It all comes together naturally. Not always, but any time it doesn't, it's a smell.
Re:Is it time to stop using sentinel values for null / “NA” ...
by synthetigram on Monday June 05, 2023 @05:47PM
(#36204335)
This is impractical a lot of the time. There are a class of problems where you want to put 2^N + 1 possible values in an N bit container, and it won't ever fit cleanly. Null is that 1 extra value that won't fit cleanly.
Another case is an array based queue. It can be implemented with head+tail pointers, or size+offset. However, there will always be an ambiguity with either, because two words of memory aren't enough to represent all possible states of the queue.
Re:Is it time to stop using sentinel values for null / “NA” ...
by taeric on Monday June 05, 2023 @05:38PM
(#36204168)
I'm a little curious if doing an equality comparison with self is the best way to determine something is NaN? I'm assuming that is as fast as any specific isNaN function available?
I'm also assuming a nice thing about the bitmap approach is that it works for non float values? I recall hearing that Pascal had some good primitives for managing parallel arrays that were used like this. I can't remember the details, that well. Definitely feels related to the array of structures versus structure of arrays idea.
I'm being really loose and am unfamiliar with Arrow but it seems as if these kinds of discussions are useful but also maybe heavily language-dependent. E.g., how Julia, Python, R, Rust, or whatever interfaces with Arrow is also relevant.
Treatment of null and missing data in the context of data science has rarely gotten the full consideration it probably warrants in design.
Interesting read though.
There were lots of pre-v1.0 design discussions on this vs sentinels, such as https://discourse.julialang.org/t/representing-nullable-valu... which can be dug up. The conclusion was more about the safety than performance, since sentinel values are fundamentally unsafe in generally since they can take a valid value and make it "special" (like in the blog post, minimum integer). Thus Julia has the following 3 choices:
And you can build your stats libraries however you so choose, though JuliaStats has strongly gravitated towards the middle option as something close in ergonomics to R's NA but while having a bit more safety guarantees and generality.I have run into issues where it would be nice to have some sort of missing type/class hierarchy though, in that the reason for missingness is coded somewhere and then how to represent that in those three categories gets complicated. At some level the different missing types get treated the same for many functions (such as your missing + 1 propagation example) but for other things not. Then you end up being tempted to use the third option which has its own issues. I suppose you could create another variable with type of missing information but then that also becomes complex quickly.
When nuclear war breaks out, I am -not- going to want to be floating on a raft in the ocean off the coast of Africa at coordinates 0.0, 0.0 when the missiles fail and revert to their "default" target.
[1] https://en.wikipedia.org/wiki/Null_Island
https://youtu.be/vh6zanS_epw
https://www.washingtonpost.com/news/morning-mix/wp/2016/08/1...
All location fixes inherently have a precision component and that routinely gets ignored. There's one tower that can reach the phone, the phone must be somewhere within the footprint of the tower. If the phone didn't supply more detailed info that's the only thing you can determine. The true area is the footprint but it gets reduced to a location and a precision--which maps it to a dot and a circle. A one-tower location fix will give a circle of uncertainty that extends off the visible map in most cases and humans ignore it. (The center of the US problem is a bit different but it's the same basic concept.)
What I think they need to do is prohibit any system from showing the central dot (or any equivalent to that) when the precision is too low. Only render the zone of uncertainty and only at a map scale where you see the whole zone, attempting to zoom beyond that gets you a message that the location can't be determined more accurately, do not try to find the center of the zone.
It's just another variant of that: having INT32_MIN is just as bad: these things inevitably end up unrepresented in the typesystem, someone writes a naïve function over that, and suddenly your "nulls" are being summed or computed on, particularly when they're rare in the dataset, or at least in the one the coder tested with.
(Even in SQL, where it's a separate value — NULL — it's still an awfully weak type system, so sanity is by no means assured.)
Give me must-be-explicitly-handled Option<T>.
(So, I'd argue that even if there were a performance loss, in most cases, the gain in correctness is the right tradeoff.)
Did you, uh, do the thing I can't ask if you did on HN? Because it's entirely about how to get better performance than sentinel values by specially structuring your discriminant.
ORIGINAL:
Not sure what you mean, so I'll try again.
An Option<T> in Rust should occupy the size of T plus the size of a discriminant. On a 64-bit machine for example an Option<i32> will take up 8 bytes. Four bytes for the i32 and four bytes (including alignment) for the discriminant. A 100% increase in space is clearly a performance hit.
If, however, T is a non-nullable type, the Rust compiler is smart enough to use this null value as a sentinel and you end up with an Option<T> that only takes up the size of T.
So, for example, both a &i32 and an Option<i32> will take 8 bytes, no difference, zero waste.
for example?
It might be more instructive to list some exceptions:
- As described elsewhere in this thread, Julia is able to optimize Array<Option<X>> internally into a bitfield and a tightly-packed array of X
- Rust can use knowledge of a type's valid values to optimize Option<T> into something the same size of T for common T (pointers, arrays, etc, also notably I think user-defined enums which aren't exhaustive of their value space) - but as far as I know it cannot do what Julia does, you need to reach for e.g. https://docs.rs/vec-option/latest/vec_option/
- This is somewhat the raison d'etre for Jai, if it ever gets released.
to make it obvious, post some code.
FWIW, we have had this discussion a few months ago on HN.
(The compiler is allowed to improve this, but it won't.)
But those languages are very good at pretending they mix the two concepts.
This is about how to represent a null values in memory (for a columnar data structure), not how null values are expressed and accessed in a particular language's type system. However null values are represented in memory, you'd want an appropriate language binding/reppresentation.
And in practice, if someone has an odd sentinel value like that in the data format, library authors tend to be lazy and not take the time to do that wrapping; instead, they just expose the i32 directly, maybe Option<i32>, and sharp edges abound around the sentinel.
(That is, the data format — a sentient value — encourages buggy code. And that is the billion dollar mistake: that a nullptr pretends to be a pointer, while it is not, and gets swept up in the same operations.)
This is only true if that summation is not used or compared anywhere else which isn't usually the case. For example if in sql you are reporting the SUM(column_A)/ SUM(column_B) and one column can be null without the other one being null you're likely reporting the wrong number
But you don't have to use what the article calls a "sentinel value" (i.e., you don't have to dedicate -2489391827 as "null") to do it: simply "null" for None/Null/Nothing, and the integer for Some(<int>) / Just int would work.
Now how do you do that with a string? If you have to ask these questions, CSV is probably not the right format.
To fix it, you have to go back and reconsider what it means for your data to lack that value. Is it a different type of thing? Does it mean that the value must exist but isn't known? Does it mean that it's really some default value? Does it mean that there's an error but you're just trying to muddle along? Does it mean you could actually have zero or more of something? Does it mean it's uninitialized?
You might try to solve any of these with a null. That's simple, but it does bad things to your type system. At least using sentinels makes it easier to reason about your type system, but if you're just using it as a "typed null" then you've just hidden the problem further.
I find that the better you design your types to model the real problem domain, the less you need nulls or sentinels or other hacks. It all comes together naturally. Not always, but any time it doesn't, it's a smell.
Another case is an array based queue. It can be implemented with head+tail pointers, or size+offset. However, there will always be an ambiguity with either, because two words of memory aren't enough to represent all possible states of the queue.
I'm also assuming a nice thing about the bitmap approach is that it works for non float values? I recall hearing that Pascal had some good primitives for managing parallel arrays that were used like this. I can't remember the details, that well. Definitely feels related to the array of structures versus structure of arrays idea.