Skip to content

Optional values #117

Closed
Closed
@tertsdiepraam

Description

@tertsdiepraam

Roto should have some support for optional/nullable types.

Research

Here is what a bunch of languages do:

  • Rust: Option<T>
  • Swift: T? == Optional<T>
  • Kotlin: T?
  • Haskell: Maybe T
  • Go: Uses -1 or nil and all that. nil is just the zero for a bunch of types
  • Zig: ?T (with null, only 1 layer allowed)
  • Hare: (T | void)
  • TypeScript: T? == T | null
  • Python: Optional[X] == X | None

There's many possible syntaxes, but semantically, it's about how many "layers" of Option are allowed and where optional types are allowed:

  • It could be an enum: many layers allowed
  • It could be a type union: 1 layer allowed, so Option<Option<T>> does not exist.

Then there are questions such as:

  • Do we expose null as a separate type? Or is it just ()?
  • Do we have type unions in the language?
  • Can we match on it or should we always use special syntax?

For Roto, the situation is interesting, because most scripting languages opt for a type union.

Proposal

We should probably have T? at least. And then also allow T?? as meaning Option<Option<T>>, because Rust allows that too. It will have to be a separate type from Option though because we need repr(C).

This would mean that you can match on these values like so:

match x {
    Some(x) => { ... }
    None => { ... }
}

Additionally we could provide the following syntax sugars:

if x { ... } else { ... }
# equivalent to
match x {
    Some(x) => { ... }
    None => { ... }
}

x or expr
# equivalent to
match x {
    Some(x) => x,
    None => expr,
}

x?
# equivalent to
match x {
    Some(x) => x,
    None => return None,
}

try { x?.foo.bar() && baz }
# equivalent to
match x {
    Some(x) => x.foo.bar() && baz,
    None => None
}

The nice thing about the try block is that multiple ? can be used (just like in functions returning Option in Rust and the block can contain multiple statements.

The main problem lies in expressing Option::map, which is expressible as try { f(x?) } above.

try can also get an else:

try { x } else { 0 }

or that should just be a separate operator or

x or 0
try { x?.foo } or 0

Like with Swift's ??, this operator could take other optional values as well so they can be chained.

It might be possible to omit the {} from the try, but the precedence might get a bit tricky if we do that.

Alternatives

We could go with a map operator instead of the try block.

# similar to `if let Some(x) { x }` in Rust
x map { x.foo }

# or more like Kotlin:
x.try { it.foo }

The downside of try blocks as described above is that nested Options are kind of verbose to deal with. For example, the here is some code in Rust and the equivalent with try blocks:

// This is Rust
x.map(|y| y.map(|y| y.foo));
try {
    let y = x?;
    try { y?.foo }
}

However, note that matching is still possible:

match x {
    Some(Some(x)) => Some(Some(x.foo)),
    x => x,
}

We can remedy this a bit with this syntax sugar:

try x { try x { x.foo } }
# desugars to
try { let x = x?; try { let x = x?; x.foo } }

or mixed:

try x { try { x?.foo } }

A more syntax sugary map might look like this:

x map (x map x.foo)

# or with .try
x.try { it.try { it.foo } }

However, flattening Option<Option<T>> to Option<T> is actually very simple with a try block:

try { x?? }

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions