-
Notifications
You must be signed in to change notification settings - Fork 15
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
NULL/nil/none/void/... value considered harmful (a "billion dollar mistake" of Tony Hoare) #46
Comments
@dumblob, thanks for the insightful remarks. First, please do not reach any conclusion on the design of the language based on any particular commit, in particular right now as I am refactoring tons of things 😊😊 Second, the general topic of error handling is described in the language design document here: http://c3d.github.io/xl/#error-handling. It is quite insufficient, but the general idea is I believe very similar to what you suggested, including the notation for what I called fallible types. The example given in the documentation for square roots is:
Where Ultimately, the intended return value for most I/O functions will be So let me focus on the points where I see a divergence compared to what you wrote.
So in short, I believe that the XL I hope that I addressed the essence of your remarks. If I missed some subtle aspect of it, let me know. |
Thank you for the thorough answer. XL is so cool and unique that I'll need to think it through. I'm though really excited so far! I'll start my thinking process with implications of "but you can also easily store it if you want" in the context of XL (in other languages this is what causes all the problems - hidden nil, if-err-then-else boilerplate, not seeing in the code that something might be actually an error and thus unwillingly not handling it, the extent to which the compiler enforces handling of errors, etc.). Second thing I'll think about is the "in XL |
First question - is the |
Second question is how do I ensure the function I'm currently implementing is a pure one if |
Third question is whether a variable holding an error value is being immediately automatically propagated or first at the place where it's being used (either being written to or read from)? If the latter, then what's the behavior if I'll pass a I understand it's the first case according to your description in point (3) in your comment #46 (comment) above. But bare with me - I'll be loud with my thoughts 😉. |
Fourth question: could you (dis)approve my understanding that In other words XL treats This assumes And because it doesn't provide any useful information, I'd rather disallow it for safety reasons (one might get easily fooled e.g. by blindly saving the result of the call and passing it further along thinking it's a useful information leading just to confusion). From my understanding Are these observations correct? Could you correct them if not? Thanks! |
Frankly, I'm still thinking about the semantics there. The part that is pretty much settled is that By contrast to For problem a), the current thinking is to use the type annotation notation to declare variables, i.e. For problem b), there are at least two different semantics, copy for something like Bottom line, what distinguishes That was your first question. On to the next ones ;-) |
A function is pure if it has no side effect. This is what you are declaring when you mark a form with the
The way to detect this is to check if the things you call within a |
It's really a matter of how you consume it. If you have
will print the error, because On the other hand,
Same if you write
If you do that, the type for
It should not compile. You need to handle the error. You can brute-force it with something like
At the moment, none of this is implemented, and implementing it often reveals design errors. |
Actually,
That observation is true for procedure evaluation. But consider the expression
Nothing is coercible in XL without you giving an implicit conversion operator (something like
This can be useful in the case of generic code. Consider the following:
I think they apply to the non-generic procedure case. That being said, |
This post will be written with XL being conceptual language in mind. At some places it might sound differently, but I've chosen to use that terminology to be clear about the intent. The concepts I'm presenting are though generic and apply as well to XL as a conceptual language.
I believe everybody is familiar with the "billion dollar mistake" (as Tony Hoare calls it). I saw there is a bunch of recent commits (c3dde14 , 68b258d , 3da52b4 , 11ed0bd ) leveraging
nil
functionality.I'd like to oppose using
nil
value. The question is though "how to deal with such interfaces"?My answer is "use ephemeral optionals". First "optionals" is a well known concept (Haskell's
Maybe
, Rust'sOption
, V's?
, Zig's ...). Despite all the pros (being dead-simple to reason about even in highly parallel systems, no stack unwinding like with exceptions, universal - can be used for errors or alternative values of any kind etc.), it brings some serious usability issues.Namely it "reifies" the fact whether it's a value or error and thus feels like a "primed bomb" you're tossing around. So what does the
ephemeral
mean? That means just one thing - the primed bomb can't be assigned to anything. In other words it can be only returned and directly at the caller site has to be dealt with.Sounds still too inflexible? Well, not anymore if you provide some sugar covering the most common use case - namely propagate (return) the optional as I don't want to handle it here yet. V lang does this perfectly - you just need to write
?
after a function call and it'll propagate it. And to handle the ephemeral optional you just appendor { print( err ) }
to the function call to "catch" the optional and work with the data the callee has put into the optional (which is under the hoods a struct holding arbitrary data).All in all I'd like XL to get rid of a "lonely"
nil
value completely and instead use this ephemeral optionals trick together with e.g. sum types (aka tagged unions aka variants). Note there should definitely exist anil
type (a type can't be assigned to anything in run time because it's a type 😉 and thus doesn't appear in run time) - e.g. for tree structures - but no lonelynil
value. So a function returning sum typeT | nil
(the value is either of typeT
or of typenil
- note herenil
is not lonely but "bound" toT
) will be something totally different than?T
(aka "optional T" - the value is either of typeT
or an ephemeral error).Note one can easily have
?(T | nil)
. E.g. a function which shall return the leftmost child of the leftmost tree node at level 5 - if the tree depth is 4 or less, it'll return an error otherwise it'll return the value of type "sum type of T and nil" which can be assigned to a variable unlike a lonelynil
(this assumes it's impossible to define a sum type with less than two types - otherwise one could again create the genericnil
bomb by defining a sumtype consisting of onlynil
type). In other wordsnil
as value actually exists under the hood, but always as a "special" case of some more important value thus providing a compile time explicit guarantee that all suchnil
values under the hood will be handled and will not leak anywhere.A bit related is the fact, that
nil
value should never be used to designate an all-encompassing "alternative value". Not havingnil
value at all (but only anil
type) is a simple measure to ensure this. Thus e.g. all the I/O functions in one of the commits I refer to above should return some optional instead ofnil
.Of course this ephemeral optionals mechanism is totally agnostic from any exception-like or effect-like or Dylan-like or any other mechanism for dealing with alternative computational branches. So no need to do anything on that side.
Any thoughts on this?
The text was updated successfully, but these errors were encountered: