Wow tough crowd. I appreciate the pragmatic approach taken here, and as long as there is type safety round nulls im happy to use the language. Nitpick: As of C# 8 they’ve mostly solved nullable types for classes too with nullable reference types (though retrofitting it made it awkward and imperfect).
Why nullable types?
111–119 of 119 posts
Re: Why nullable types?
#112I have this funny question all the time: why dont we just use a bit boolean for representing option/nullable/unknown instead? (one bit of hasValue/isEmpty like C# HasValue, but like struct uncertain_value { T value; bool dont_care: 1; } and uninitialized value f () => uncertain_value { dont_care = 1 })
Given a 64-bit variable, you can fill it with 64 bits of value, or 64 bits of pointer to some value elsewhere. With your scheme that would be 63 (which I'm not objecting to). Pointers are already special-cased to only contain 0 already. There's no use-case for having 63 bits of pointer and an extra bit saying "ignore the other 63 bits". As for the value case, I think the argument is pretty similar. Why store 63 bits…
My original idea exactly contrary to your thinking: rather than making 0 NULL and give it the sentinel we always had, why don't we just avoid using it like using some data structure magic? You know, the null is infamous for causing the special case in integral constant expression that C++ inherited from C, and is confusing too, because then 0 is can be considered as (void ) and thus can cause an overload fiasco, that consider f(void) and f(int), what do you think f(0) would be? So the gist is I always think using x=0 to be able to represent NULL/false and !(x=0) to represent true is really really confusing (at least in C/C++). This also caused other languages to have some probable cases of negative number being cast to true so like `if (-1) console.log('lol')` it will print, wat?
Also, with my configuration, not only we can always assume the value was intended to have a place to stay without indirection load, but the optional/null/don't-care/noninit will always have a stable state between 0 and 1 -- so we can clamp down without having to deal with extra states like I shown above, which is good from theoretical (because we can express its definite intent rather than guessing the type of the wrapped value) & logical standpoint (that we should not think in 3VL for null, rather, judge it by its concrete properties), not quite so in practice however (again, optimization kicks in nullifying all the good works), this might not entirely escape the pointer-not-null-then-consider-valid problem (what if we loaded a garbage value that might have a random 0/1? nowhere to tell also).
In fact, this kind of configuration is not rare to be found: an open addressing hash table can use this kind of setting to represent "tombstone"/deleted field, but in a more optimized manner that we "bit set" because we are going to allocate a contiguous block of memory anyway so like the current universe can have 32 entries then we use lg(32) bytes bit-set at the beginning/end, if insert or delete value at entry x then toggle bit-tombstone of x, if search value y then find first i, x in entries if compare(x, y) = exact and !bit-tombstone[i] then (yes, i) else no...so on.
But at the end of the day even that I don't like using 3VL to represent NULL, and my configuration is still like mapping 3VL into double binary anyway, iirc its like if value = U then don't care (either true and false; neither true nor false) else value.
An analogy for NULL: if addr = NULL then value is no where to be found/random garbage value else load memory from addr.
Re: Why nullable types?
#113Earlier quoted context omitted.
The reason Option > isn't a problem is because it's a monad, so people will naturally transform it into Option while writing their programs. There is no irony there. If it wasn't a monad, the GGP would be incorrect, and ad-hock solutions would be very valuable.
In a language with Haskell's guarantees, you can mechanically get from the standard "bind" function to prove that "m (m a)" can be converted to "m a" [1]. That function is called "join", and is a lesser-well known way to write monad implementations which is equivalent to the more famous bind function. (IMHO, join is a better way to understand the typeclass intuitively. The standard bind is better to program with in g…
They would be equivalent if there was an isomorphism, but of course there isn't.
Re: Why nullable types?
#114Earlier quoted context omitted.
> You can map, filter, reduce, chain, compose functions that all work with optionals, and lift functions that work on numbers (or any other type) to be functions that work on Optional but none of that is even mentioned in this piece (likely cause then its harder to justify picking nullable types instead) Maybe I'm missing something, but I don't see how that's special to option types. Here's a set of extension methods…
By compose I quite literally mean the compose function. const add3 = val => val + 3; const multiplyBy10 = val => val * 10; const subtract5 = val => val - 5; const doABunchOfMath = compose(add3, subtract5, multiplyBy10); const ninetyEight = doABunchOfMath(10); const optionallyNinetyEight = Some(10).map(doABunchOfMath) That Dart already provides these extensions feels like they are providing an optional type but throug…
Outside Lisp/Scheme has anybody used those for anything other than examples in FP tutorials?
Every case I ever seen is non-production tutorial thing like "add3".
Re: Why nullable types?
#115Re: Why nullable types?
#116If we look at set theory the NULL set is a valid set. The problem with NULL is often it's just treated as zero or if passed to a language that does not have NULL it's pretty often to just treat it as Zero. This bit equivalence between zero and NULL zero is often what you see lead to problems. It's why for instance with GPS coordinates NULL island exists. However, errors aside NULL is a perfectly logical value to enco…
> NULL is a perfectly logical value to encounter or need. Great! Assign to it the type Null. This is legal: Null myNull = null; Make this illegal: Integer i = null;
To represent a type that can be NULL or an integer it should be a different type.
Like I said the problem again is mostly your data is not always contained to your language. If you export GPS coordinates some systems may treat this composite type incorrectly. So even if you have a strong type system people will just be lazy.
Re: Why nullable types?
#117Earlier quoted context omitted.
I agree with all your other points but > > We can’t perform arithmetic on an Option any more than we could on a List . > Why not? One sensible definition is (...) It's not the only sensible definition. It would be equally sensible to implement it as: Some(x) + Some(y) => Some(x+y) Some(x) + None => Some(x) None + Some(y) => Some(y) None + None => None
It really depends on what the meaning of None is. OP's definition makes perfect sense if it means "unknown". Yours make sense if it means "zero", but that's far less useful. If it means "missing" - which is what the article says they wanted - then any operation involving None should be an error (but Some can still be handled automatically). It can be interesting to see what other languages do in this situation. For e…
Exactly. This will generally be application specific so we can't choose The One Right Definition for adding `Option`s.
> Yours make sense if it means "zero", but that's far less useful.
Not in my experience but I agree there are different situations. Personally, I'd default to `Option::None` meaning "no number there, so just skip it"[1] and prefer `Result::Err` to denote "there should be a number here but we don't have one", which would behave like a SQL NULL (propagate in all calculations).
1. What's the length of data in a header-only packet? I wouldn't say it's zero, especially when trying to fit a read() like API on top of a framed protocol (read() returning Ok(0) marks the end of file).
Re: Why nullable types?
#118Earlier quoted context omitted.
By compose I quite literally mean the compose function. const add3 = val => val + 3; const multiplyBy10 = val => val * 10; const subtract5 = val => val - 5; const doABunchOfMath = compose(add3, subtract5, multiplyBy10); const ninetyEight = doABunchOfMath(10); const optionallyNinetyEight = Some(10).map(doABunchOfMath) That Dart already provides these extensions feels like they are providing an optional type but throug…
> By compose I quite literally mean the compose function. Outside Lisp/Scheme has anybody used those for anything other than examples in FP tutorials? Every case I ever seen is non-production tutorial thing like "add3".
In the TXR internals, I have a C function called chain, which composes N functions together (left to right, not right to left like typical compose functions). It is variadic: the end of the arguments is signaled by nao: a not-an-object constant:
git grep '\
The _f variables are pre-computed function objects, stored in globals, to avoid consing them repeatedly. That func_n1(cdr) seen in match.c could be replaced by cdr_f , not to mention by the func_f1(rest); it conses a new function object referencing the C function cdr each time it is called.Re: Why nullable types?
#119Earlier quoted context omitted.
By compose I quite literally mean the compose function. const add3 = val => val + 3; const multiplyBy10 = val => val * 10; const subtract5 = val => val - 5; const doABunchOfMath = compose(add3, subtract5, multiplyBy10); const ninetyEight = doABunchOfMath(10); const optionallyNinetyEight = Some(10).map(doABunchOfMath) That Dart already provides these extensions feels like they are providing an optional type but throug…
> By compose I quite literally mean the compose function. Outside Lisp/Scheme has anybody used those for anything other than examples in FP tutorials? Every case I ever seen is non-production tutorial thing like "add3".
Even if you don't use compose directly, the mindset of working to build systems in a composable manner is invaluable.
Its a mental shift that leads you to thinking how a system can be represented as a series of inputs being piped from one function to the next between boundries of my code (server response data --> client side caller --> transformation functions --> local data store --> UI).
Achieving these pipelines requires expressing intent with functions and with a self imposed constraint of writing pure functions, you begin needing functors and applicatives and monads to help store stateful information.
If at any point you need a new capability or need to add a new code path, you just modify the relevant pipeline(s), write any new (pure) functions along with any adapters you might need to inject it in your pipeline, and you're good to go. If everything is a pure function, testing and debugging instantly become easier. All this from wanting to build composable functions.
Some languages also support piping (runs the functions in the reverse order that compose does) which can help visually since functions are invoked in left to right order which is how we read too.
[1] https://hackernoon.com/forms-of-composition-in-javascript-an...