First-class opaque types (replacing private tags)
Background
Opaque types are a valuable programming language feature.
Currently, Roc implements them using private tags, e.g. @Foo instead of Foo means "a Foo tag, except scoped to the current module." Private tags were added to the language because back in 2018 I was trying to think of a lightweight design for how to support opaque types in Roc, and this was what I came up with at the time.
I think the following is a better design.
Design
Instead of private tags, the idea is to make opaque types a first-class concept in the language.
For example, let's say I want to make an opaque type called UserId, which is an U64 under the hood, but I don't want other modules to be able to access its behind-the-scenes representation. Here's how I might do that with this proposed design:
UserId := U64
You can read this as "UserId is an opaque wrapper around U64."
Within the scope where I wrote the := (it would work in expressions as well as at the top level, just like type aliases do), I can wrap and unwrap the opaque UserId type by writing @UserId. (Note that this is the same as the current private tag syntax, except you're putting the @ in front of the name of the type itself. In this world, private tags would no longer exist.) So for example:
fromU64 : U64 -> UserId
fromU64 = \number -> @UserId number
toStr : UserId -> Str
toStr = \@UserId number -> Num.toStr number
If I tried to write @UserId outside the scope where the UserId := was defined, I'd get an error. That type is opaque outside the scope where it was defined!
That's basically the whole design. Of note, type inference should work with these, so I could do UserId := _ and the compiler should be able to figure out the wrapped type from context.
Benefits
This design has a few benefits.
- It makes Abilities possible. (The Abilities design couldn't be implemented on top of private tags.)
- It should make the language easier to learn completely, because opaque types are a concept that should be taught regardless, and now they can be taught in the context of a feature that directly implements their purpose.
- It will speed up type-checking, because opaque types can have a more efficient representation than tag unions do.
Implementation
- [x] Modify the parser such that
UserId := U64parses to a newDefAST node calledOpaqueType. (Like type aliases, this should be supported as a top-levelDeclarationwithin a module as well as a def inside an individual expression.) - [x] To avoid having
trunkbe in a broken state, make the syntax be$UserIdfor now instead of@UserId, so we can make progress without breaking private tags. (Then at the end we can remove private tags and swap it from$to@.) Parsing$UserIdin both expressions and patterns (to beExpr::OpaqueTypeandPattern::OapqueType) would be a prerequisite for this. - [x] Update canonicalization to use
Symbols for theOpaqueTypeDef as well asExpr::OpaqueTypeandPattern::OpaqueType, and give naming errors if they're not in scope. (We probably want this to work similarly to how we resolve naming errors for - [x] Add a check to canonicalization for if you're using
@FoowithoutFoo :=having been defined in the current scope. (Note that this is not the same as theFootype being in scope! If it were the same, then these types wouldn't actually be opaque; I would be able to use@Fooeven if I'd just imported theFootype from another module.) - [x] Incorporate opaque types into type constraint generation. (We shouldn't need to change solving for this;
Applyalready does what we want.) - [x] Add a way to expose top-level opaque types (just the type) to other modules, and for other modules to import them.
- [x] Convert all tests that currently use private tags over to use opaque types.
- [x] Remove private tags from the language.
- [x] Change the
$in the parser (and tests) to@ - [ ] #2220
- [ ] Add support for instantiating opaque types to the editor.
- [ ] Add support for pattern matching on opaque types to the editor.
Is this all done now except for the editor parts?
yep!