Skip to content

Optional values #117

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

Open
tertsdiepraam opened this issue Mar 4, 2025 · 0 comments
Open

Optional values #117

tertsdiepraam opened this issue Mar 4, 2025 · 0 comments

Comments

@tertsdiepraam
Copy link
Contributor

tertsdiepraam commented Mar 4, 2025

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?? }
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant