Why does Option not include null?

One thing that’s been a little annoying is that the option type only checks for undefined in the runtime, and not also null.

Is there a reason rescript doesn’t include null checks when using option?

1 Like

They’re different types. undefined is the absence of a value, null is an explicitly defined object that explodes when used as an object. Even though they‘re both used to represent empty in JS, they’re different types.

If a value is null, it’s because someone, somewhere set it to the value null (same as any string, number, object, Boolean, etc.), whereas an undefined value is missing (as in, optional).

While I agree that they are different types in javascript, it doesn’t have to be in rescript. I can’t find a single case where I’ve had a Js.nullable and haven’t converted it to an option when actually using it.

1 Like

This used to be a topic that the team went over again and again. It’s definitely true that including null would have made parts of the binding experience better.

However, one of the deal breakers of having None comparison accepting both undefined and null (while, naturally, only compile to only one of the two) was that some crucial boolean comparison logic is no longer right. For example, value !== None would no longer mean that value is a Some, since it could have been null !== undefined. This would have introduced bugs if you weren’t careful about rewriting your conditions. In a few of the codebases, the final decision on this ended up preventing several big bugs.

2 Likes

Yeah that makes sense. Why wouldn’t something like value !== undefined && value != null work?

You’re talking about compiling value !== None to what you wrote right? For one, it wouldn’t work well with polymorphism, e.g. if you have a function that’s a generic let f = (a, b) => if (a === b) {...}

How are these cases usually handled with the current implementation of None ? From a very naive perspective I would think that including a null check with every undefined check would be enough, could you elaborate why it isn’t?

Continuing my previous example, you might have f(1, 2) and f(value1, value2). Where value1 and value2 are null and undefined What does the output of f look like?

Hmm, since the constructors are tagged, I imagine that you could narrow down the comparison to be
if ((a == b) || ( a == null && b === undefined) || (a === undefined && b == null))

Would that work?

EDIT: here’s a repl https://repl.it/@cevr/GrimyGummyPolygons#index.js

null and undefined are so semantically loaded in different ways that it really wouldn’t have made sense to unify them in one option type. It’s also hard for the type checker to have one type represent two kinds of values (simply said).

Check out previous discussions on the rescript-compiler issue tracker to get a better understanding of why this is.

E.g this one https://github.com/rescript-lang/rescript-compiler/issues/4016

Also, null and undefined mean different things.

Take GraphQL Relay for example: undefined here means, the value hasn’t been requested, null, that the value has been requested but didn’t return anything.

I imagine, that it would be way harder to bind to these libraries, if option types would encapsulate both types :slightly_smiling_face:

IMO the biggest pain point with Nullable values is that they aren’t pattern-matchable. Would it be feasible to make that type representable as a variant? Even if it was distinct from the option type. (I didn’t follow the topic when this first came up a while back, so apologizes if this has already been discussed to death).

Js.Nullable.toOption is sufficient in simple cases, but it’s tedious if you’re pattern matching a deeply nested JS object (as is common for cases like GraphQL query results).

I can imagine something similar to this:

type nullable<'a> = 
  | NotNull('a) // compiles to an unboxed 'a 
  | Null // compiles to null
  | Undefined // compiles to undefined

let f = x =>
  switch x {
  | {data: NotNull({field: NotNull({nestedField: NotNull(nested)})})} =>
    NotNull(nested)
  | _ => Null
  }

That’s a lot nicer than:

let f = x =>
  switch Js.Nullable.toOption(x.data) {
  | Some({field}) =>
    switch Js.Nullable.toOption(field) {
    | Some({nestedField}) => Js.Nullable.toOption(nestedField)
    | None => None
    }
  | None => None
  }
4 Likes

Is it possible to make nullable matchable to three constructors ? Some | None | Null

Don’t know if that’s feasable

1 Like

You could use flatMap or a let-PPX for that currently.

It would conflict with the built-in option type.

Ideally, no let-ppx please; we’re trying hard to move the community away from it.

flatMap is ok-ish. We’ll provide some better facility, e.g. if let, to deal with these cases.

(sorry for the tangent)

2 Likes