Description
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
ornil
and all that.nil
is just the zero for a bunch of types - Zig:
?T
(withnull
, 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 Option
s 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?? }