Proposal: Rename `Optional[T]` to `?[T]`

motivation

?[T] is 7 characters shorter than Optional[T].
This proposal is nothing special, it just allows ? as an identifier, thereby greatly simplifying the language by avoiding the special ?T syntax.
Even though 7 characters may not seem like much, they can add up to a large number of lines given the formatting limit of 80 characters.

1 Like

The only downside to this is we would need compiler help to make ? a special case for types - (which isn’t a bad thing at all, just pointing it out).

Funnily enough in the mean time you can actually use backticks to make any identifier you want (including a type name)

struct `?`[T: AnyType]:
    pass
2 Likes

Counter-proposal: remove Optional. Alternatives here: FAQ | Roc

2 Likes

“Default Zero Values” cause a lot of headaches. I’ve seen a lot of correctness bugs in Golang result from them, and I would prefer to avoid them.

No idea what you’re responding to, sorry.

“Default Value Record Field” as an alternative to Optional. It turns into something like Golang’s zero values.

It looks like you’ve just assumed what the feature is based on its name. If you look it up you will find it’s not what you expect.

Regardless, it’s only one of several alternatives listed in the FAQ. The interesting feature is actually Roc’s anonymous tagged unions.

The roc docs seem to be describing the return version of that as something like Go’s zero types, but I may be misreading them.

I’ll look more at the anonymous tagged unions.

If my safe union proposal were implemented, it would not be necessary to remove Optional, since builtin enums (Optional and Result) would be equal to all enums.
If you think about it, if Option and Result have builtin methods, this means every user defined enums needs to define them to have a similar API.
Here is a quick sneek peak of my proposal:

This works of course through tag checking(union access checking) and the properties CheckedUnionPropertyDescriptor and UnsafeUnionPropertyDescriptor

optional.checked_union.some # trap if not .some
optional.unsafe_union.some # UB if not .some
result.checked_union.success
result.checked_union.failure
optional.some # checked by the compiler (union_access_is_safe)
result.success
result.failure

The API of optional excluding a few dunders would look exactly like this. All other methods would be removed.

@unsafe_union_property_descriptor
@checked_union_property_descriptor
enum Optional[T: AnyType]
    var none
    var some: T
    @always_inline("union_check")
    fn __is__(self, _none: NoneType) -> Bool:
        return tag(self) == 0
    

I updated my proposal to optimize the graph traversal. I have finished collecting ideas and taking notes. I will update my safe union proposal in a few days, once I have the time.

if you are curious what tag does.

@always_inline("nodebug")
fn tag(enum: some Taggable) -> Int:
    """
    Returns the logical tag of an enum.
    "Logical" means that the compiler is allowed to optimize the tag away or reorder fields.
    However, this function returns the tag as defined by source order, which may involve computing or looking up the tag.
    """
    return enum.__tag__()

If we want niche optimisations, having an open encoding (“a value of a sum is exactly a tag and a payload”) is a disadvantage. In order to support pattern matching, we need language-level support for some kind of sum anyway; I’d still vote to have ML-style sums.

2 Likes

I agree with sora, defining sum types as having a tag is not a good idea. I’d like Optional[NonNullPointer[c_char, read_origin]] to have the same ABI as const char*. match is also too useful of a construct to not include and I don’t think it’s reasonable to implement without compiler support.

Taking ideas from a functional language is very dangerous since you may end up with an imperfect design. Especially if no project in it exceeds 1000 lines. For example rust and swift copied the enum pattern matching approach while not considering the superior field approach.
Consider the data you are representing and the tradeoffs that come with it.

Is there really a reason to define new types with same memory layout but different identifiers.
Just document your code.

I don’t see a problem with tag and niche optimizations .
For Optional[Pointer] tag just returns Int(self._union) != 0.

For example rust and swift copied the enum pattern matching approach while not considering the superior field approach.

I think they’re mostly equivalent in terms of performance, so the main difference is whether it’s reasonable to support the field-level approach and whether you can do things like niche optimizations with the field approach. I don’t think you easily can do safe niche optimizations with the field approach, which makes it a perf hit for pointers stuff like “negative on error int” values.

Consider the data you are representing and the tradeoffs that come with it.

Niche optimizations are the thing that directly targets this. They are very hard to do with simple “int + data” tagged unions, especially if you expose fields directly, and let you handle things like null pointers gracefully. Sum types also enable some other tricks like moving the tag to the last N bytes of the type to make sure the data at the front is aligned, something which most tagged unions can’t do easily.

Is there really a reason to define new types with same memory layout but different identifiers.

Absolutely. You would do that whenever you want to have different behavior for similar types. For instance, integers that do wrapping vs saturating vs checked vs unchecked math. Another example would be a message passing protocol, which might have absolute addressing for pointers if you’re passing messages in the same process, but also have a relative addressing mode which tells you how many bytes ahead in the buffer to look if the message was serialized and sent over the network.

For Optional[Pointer] tag just returns Int(self._union) != 0.

But is size_of[Optional[Pointer[...]]]() the same size as size_of[Pointer[...]]? With niche optimizations, we can say that Mojo’s Pointer type is always a non-null pointer, so we can use the null pointer to represent Optional[Pointer].None. This also lets you do fun things at FFI boundaries where you can force people to properly handle null pointers that might be handed back by C code, since Mojo’s Optional[NonNullUnsafePointer[T]] and C’s T* can have the same ABI.

1 Like

Don’t you see that sum types are control flow sensitive, while safe unions are just tag sensitive. The resulting assembly will always check the tag, so why not model it that way?

Safe unions are just structs with getters and setters, which means their layout and tag are field-independent.

Enums shouldn’t have methods in the first place. If you are just interested in the type’s ID, you can define a generic zero-sized struct.

Could you please elaborate on this a bit more formally?

1 Like

Enums shouldn’t have methods in the first place. If you are just interested in the type’s ID, you can define a generic zero-sized struct.

Nowhere in my explanation did I mention methods or a type id, and you can’t send the value of a ZST over the internet.

Nowhere in my explanation did I mention methods or a type id, and you can’t send the value of a ZST over the internet.
[/quote]

But can’t you serialize it?