racket / rhombus

Rhombus programming language
Other
352 stars 62 forks source link

"no value" idiom in Racket #137

Open chansey97 opened 4 years ago

chansey97 commented 4 years ago

Is there any "no-value" idiom in the current Racket?

Generally speaking, there are two kinds of "no value":

  1. Representing partial function

    Some functions may fail. Usually, they will raise an error/exception, but if they don't raise an error/exception, they may return the "no-value". This "no-value" is similar to Haskell's Maybe Nothing.

  2. Representing null-pointer

    In imperative style code, we use mutable data structures frequently and there are some situations where we can not know the values of some variables in advance. (e.g. filling cross-reference, etc). Usually, the initial values of those variables are "no-value". This "no-value" is similar to null in Java.

After some discussion in Slack, it seems that a lot of people regard #f as "no-value" in both these two cases? IMO, it's not a good idea.

I have found some alternatives to #f, rebellion/base/option for 1st case and racket/undefined for 2nd case.

So should we encourage the use of "no-value" instead of #f?

Thanks.

Related issue https://github.com/racket/rhombus-brainstorming/issues/67

tgbugs commented 4 years ago

The problem with using anything other than #f is that it leads to unnecessary complication when testing whether a value is set. The only time you ever run into an issue with current practice is if you are checking whether a bool is unset, but what is the use case for forcing higher order logics into a dynamic language, and in what kind of uncertain universe do you encounter a need to know that a name is bound, that it is currently bound to nothing, and that it will be bound to boolean in the future, and that you care about it not being false by default, but that somehow you can't assign that value before someone else needs to check if the name is bound? This seems like only the kind of situation that could arise due to exposure to a static type system, and there isn't a static type system that is going to scream if a value that is going to become a string starts out as a boolean.

Then again I have been spending a lot of time in other lisps where there is no concept of #f that is distinct from the empty list '(), so Racket already seems vastly more expressiveness when it comes to being able to talk about no-value ((void), (values), null, #f). Keep in mind also that no-value has major overlap in meaning with (values) and it is impossible to assign (values) to anything because the arity is zero. I suspect that this is equivalent to your null-pointer scenario, and it is always an error, otherwise you allow the creeping insanity of C to wind its way into your programs (which you can always still do via the ffi).

Practically speaking, it also sounds like you have a use cases for pre-allocating a bunch of memory that you can later fill with values. In other lisps mutable cons cells can be used to allocate a bunch of memory that can then be mutated to hold the values you need. You can fill these with any value that you want (that will fit in a single car) and can be defined internally within your program without a need for any other external conventions. Racket's cons cells are immutable by default, so I don't know what the current idiomatic way to do this is, though you could always use mcons if you were in a pinch.

Another possibility would be to use a macro to check if the variable is bound, but in Racket that means that you are going to have weird interactions across phases.

chansey97 commented 4 years ago

@tgbugs

I suspect that this is equivalent to your null-pointer scenario, and it is always an error, otherwise you allow the creeping insanity of C to wind its way into your programs (which you can always still do via the ffi).

I don't think (values) is equivalent to null-pointer scenario, because we are talking about mutable variable. (values) seems more like 1st case, but I am not familiar with multi-value (sorry).

Practically speaking, it also sounds like you have a use cases for pre-allocating a bunch of memory that you can later fill with values.

Strictly speaking, there are two sub cases in null-pointer scenario:

  1. Representing a variable, whose binding is a invalid store location That means the location have not been allocated on the store, so give it a invalid store location first.

  2. Representing a variable, whose binding is a valid store location, but I want to set! a "nothing value" at the location on the store.

But in practice programming, it seems that there is no significant difference between them, because Racket uses implicit reference.

An application of undefined usage is racket/class

All fields in the newly created object are initially bound to the special # value (see Void). Initialization variables with default value expressions (and no provided value) are also initialized to #.