I had a couple ideas for a couple of things that might be interesting in Roc, and I thought maybe theyre something of value so here they are:
1. Parametric Types as Simple Transformations
While Roc's type system is currently quite simple, such a thing has it's disadvantages. Some of these, like classes, are neccasarry losses, however, there is one thing that I belive has a simple implementation, makes the type system if anything simpler, and on the compiler level would be pretty easy.
The idea is that rather than viewing type aliases as their own special type of value, we view them as, well, tradionatal aliases. That is instead of Pair : (_, _), we write Pair = (_, _). Then, instead of using Pair as a value when type-checking, instead, process any instance of it first, so that the type-checker checks Pair -> Pair as (_, _) -> (_, _), which reduces the complexity of type-checking
In addition, it allows for a form of restricted polymorphism. It does this, as, if, for instancePair = (_, _), we could also write Pair a = (a, a). This is just the same as the above version, except now, we also use a abstraction around a type. However, fundementally, it's still the same thing, the type-checkers would read Pair a -> Pair b as (a, a) -> (b, b)
2. Struct as intersection types, tags as union types
A structure type specifically in Roc bears much resemblence to intersection types. This is true in Roc specifically as, for instance, we can write {name : Str, id : Int}a -> {name : Str, id : Int}a, where we abstract over a possible extension to the struct. However, one could reason that it might be sensible to write {name : Str, id : Int} as {name : Str}{id : Int} using the extension syntax, except this time on a concrete type. That is what the idea would be, instead of modeling structs as a special concept, treat them as things that have collection of properties they must have (there fields having certain types). This means we can model the above {name : Str, id : Int}a -> {name : Str, id : Int}a as {name : Str} & {id : Int} & a -> {name : Str} & {id : Int} & a, where now the extension is just an arbitrary type
Tag unions are, well, generic union types.
3. A couple notes
These aren't proposing a complex dependent type system on Roc. Indeed, the first only requires that types can
In addition, performance wise, given this limited system, it is known that if two types are applications of a type with equal parameters, then they must be equal.
In other words, we could have a system where whenever we "apply" a "type function" we would add a note of the value used to produce, so for instance, Pair a -> #(Pair a) (a, a) This would allow us, given the above fact, to note that if we have two values of, say Person a = ..., so long as we something annotated with #(Person x) ..., we know without inspecting the inner term that if x ~ y then #(Person y) ... must type-check. This is nothing more then an optimization point, having, or not having an annotation dosen't change the "theoretical" value of a type (ie, #(Pair a) (a, a) still checks (a, a))
I'm not sure how you're using * here - can you use named type variables instead? :sweat_smile:
Pair : (*, *) isn't valid today, and it also wouldn't be valid if those were named type variables
wait it isn't?
that makes this a lot more simple then, no need to introduce lexical scope of type vars
and it is already the case that the type-checker "desugars" aliases (e.g. if you make the alias Foo : Str, the only difference between Foo and Str is that we might reference one or the other in error messages)
The main point is the parametric part
yeah you also can't use underscores in type aliases, at all :sweat_smile:
Wait can you use them in types
yeah but not aliases
Perfect example of the utility of this
only named type variables are allowed (no * and no _ at all) in aliases, and all named variables have to appear before the : too
I think this would result in a ton of hidden type variables being instantiated behind the scenes, no? to the point of essentially being subtying
but this is a perfect example, if you name Pair you lose abilities
you could also not implement the third point, and again, i was mainly not thinking about introduced type vars, although it is a reasonable extension
Basically the point of the was that you won't ever lose utility by naming a type
sure, but the costs are not the same
in terms of the performance?
in a function's type annotation, using _ just means "the type of this function is unchanged but I'm choosing not to annotate it"
wait so they should be * then
as in any type
today, _ literally just means "I am choosing not to annotate this part"
it doesn't affect anything in any way
if you do foo : _ that is exactly the same as not annotating foo at all
yeah it should be *, then.
wait isn't that just the same as *
yeah, so the problem is that allowing * in type aliases has massive downsides
it's the same as saying "you can use type variables in type aliases that you don't have to mention in the name of the alias
"
even with this, where it's essientally just as if the user had copy-pasted it
yes, the tradeoffs are super different with aliases
but still, that means that there are certain things that can't be aliased
yes, that's correct
because the downsides of allowing that are very serious :big_smile:
this plan essientally just makes aliases equivalent to copy-pasting
ie, parsing -> type transformation -> type checking -> ... would be the system, where the transform doesn't interact with the checker
I haven't though about how to explain this, and unfortunately I gotta go do kid stuff, but please believe me that this is not a simple idea, it's not a simplification to the type system (it would be a major new source of complexity to the implementation), and the reason that ML family languages going back to the 1970s don't offer this isn't that they didn't think of it, it's that it has major downsides that are being overlooked right now :sweat_smile:
@Ayaz Hafiz might have a good explanation though?
really sorry, gotta run! I'll be back on later though
see ya
although i would like to point out again how these "type transforms" aren't actually a part of the type checker
I probably am missing something, but 1 feels like how type aliases work today. They are just structural fundamentally and the names don't matter.
For 2, I agree that we should have more power and flexibility with records, it was discussed significantly with the proposals around .. syntax, but that probably could use a concrete design doc at this point cause I'm not sure it is clear what exactly is planned.
when would you want to use an alias with an unbound type variable?
So yes, this is how type aliases work, however, iirc, type aliases as of current do not allow arbitrary types, that is, given something of the form
and
,
the validty of a does not imply the validity of b, even though in almost all type systems, both formal and informal, these do indeed hold (delta-reduction fails)
Not one hundred percent sure why that isnt working
Ayaz Hafiz said:
when would you want to use an alias with an unbound type variable?
In terms of the very simple type system of Roc, you wouldn't: This primarly deals with parametric, not ad-hoc, polymorphism, although if type aliases are always reducible then if a type can contain type vars then a type alias should be able to contain type vars
Brendan Hansknecht said:
For 2, I agree that we should have more power and flexibility with records, it was discussed significantly with the proposals around
..syntax, but that probably could use a concrete design doc at this point cause I'm not sure it is clear what exactly is planned.
Also I might point out that struct types can be paraterized in terms of in terms of their extensions, where that is just a intersection type
im struggling to understand the essence of your proposal, what does it enable that is harder/impossible to express in the current language? the current mechanism of aliases is such that aliases are equivalent to underlying type (equal up to unification), but there are well-formedness rules which mean that you cannot say Pair : c for example.
Aliases can’t take parameters
Or can they :skull:
Wait seriously they can’t currently right
I’m not just like losing it… right???
Al c : c is valid
if thats what you mean by parameter
:skull::skull::skull: ok im just gonna reconsider my life choices for a moment
Ok back from slamming my head against a wall
Uhm I do think that maybe the syntax should be changed
From ‘:’ to ‘=‘ (like Haskell type aliases) as in highlights the type/term distinction
Also had an extension to this idea that I thought was pretty dumb but that’s kinda out the window now so, types that take type functions as args as a form of staging, basically, if we have a type of the form (* -> *), * -> *, say, a transformer, we first hide the details of all type functions, and therefore make it ?, * -> *, we apply with the given args, and then we use the other function to transform
Ok because I don’t think anything could have been worse than that suggestion time to suggest something crazy
Record types as abilities/typeclasses, except they’re values don’t need to be records
Basically the idea is that R0 : {field : t0 } can be thought of as a projection get-field : R0 -> t0 (essientally as a Haskell lenses), however the idea would be to extend these lenses to non-records types, ie, abilities, so, we might model a Show ability as Show a = { show : Str }a, and if we need some type that implements Show, we can do this at either the declaration site, ie GameResult = (Win | Draw | Lose) & { show = … }, or at usage by providing an instance at that time (instances probably wouldn’t be allowed outside due to instance resolution getting very complex). Just a random thought I had, also sorry that the formatting is really poor, I just had this idea and I don’t have access to a computer rn
Just to make sure you are up to date, abilities are going away and being replaced by static dispatch. It actually should allow something similar, but still restricted behind custom types.
Yeah I’m really not :skull: on your comment on their not being a formal design think this might be a interesting way to do it
Basically the idea would be that types can either be something that describes a single representation (i32), a union or intersection of other types (Num, Records, Enums), or something that describes a runtime erased property of the type (a ability / class)
Don’t know how useful that is
the problem is you cannot really do this and also get guaranteed type specialization (monomorphization) without a bunch of machinery and I'm not sure that the machinery can be guaranteed either
"undecidability of semi-unification" is the relevant term
(deleted)
Not that familiar with it, but why would this need pattern matching?
(The records)
not sure i follow the comment re pattern matching
Just looking up semi unification it seems to be about applying typing to pattern matching
though im not familiar with it
am i missing something here?
I think the part (if I’m correct) that is being missed is that types aliasing/replacement comes strictly before type inference and checking and that they are not codependent
The only thing that would need to be added would be a simple occurs check to make sure types are (co-)recursive
im pretty sure what you're proposing requires arbitrary subtyping comparisons
unless i'm misunderstanding
semiunifcation is about the idea that you cannot decidably answer arbitrary subtyping (and equality) constraints with full inference
Ah ok, the thing is that this system of aliasing is separate from the substitution, that is, we apply aliases first and only then type check, so, almost like Zig generics, that it where the generics don’t add much complexity, and this system would add even less due to not requiring a full type runtime. Basically this system would probably end up actually being similar, due to the fact that the type checker dosent need to worry about type functions, as it gets the fully expanded types (unless I’m misunderstanding the problem)
Apologies, I'm having a hard time understanding what the concrete proposal is. If you are suggesting that F a : a (or F a = a, let's ignore syntax for now) gets expanded to just a, this is already done.
The proposal is that we go from F a b = (a,b) ; F Int c |> (Int, c) BEFORE it goes to the type checker
I think I’m probably missing something
Oh sorry I misread your message
But it’s not done completly due to the fact free vars can’t be included in aliases
why would you want free variables in aliases?
Sudo-Ad hoc polymorphism
Which you can have in type sigs
can you give some examples
Sure, a Frac is two numbers essientally of the form (a,b), where a and b are arbitrary integral types
Or a generic operation on a list-like structure
sorry I'm confused again, can you explain how this would be used specifically? like a function implementation
Yes, basically the alias acts almost like a macro
So we transform the type using the alias, lexically scoping as necessary, and then check
right but what's an usage of that look like in code
The same as type aliases do now
Just plug it in
So f : Pair Int -> Pair Str |> f : (Int,Int) -> (Str, Str)
Oh also another cool potential use case for this (even though I know the faq says no) is HKTs as a hypothetical
Because they would just be placing in values in templates, it would be valid to have them, but that’s way too large a scope
I think basically this is what if you made the Zig type system functional
f : Pair Int -> Pair Str already works and is equivalent to f : (Int, Int) -> (Str, Str)
i think i'm still missing what the new addition is
Well that was in response to what it looks like
Mostly just the limited ad hoc polymorphism
Which dosent have many use cases
that's what i don't understand. what's an example of code what would take advantage of that/
Honestly maybe I’m just tired now (or I was just loopy when I was writing this proposal) but I actually can’t think of one rn :sweat_smile:
Oh wait I got one… sort of…
Basically it’s super trivial to implement without aliases
But
List length
Oh wait
Now I remember
Ok so intersection types, we have some type alias Trait a = … something that a must have, then a type GTrait = b & Trait b (a trait object)
what is an example program that uses that though
like in code
Rust impl and dyn
but like in Roc how do I use that? Like what is an example function we could write that would take advantage of it
Honestly I can’t think of one right now and i don’t want to take any more of your time saying all the things at the edge of my mind, I’ll think about it and see if I can think of any more concrete examples for later
my feedback would be to push more on this. i think without concrete examples or usage patterns it's hard to understand the value.
Yeah, but right now I think I might need a little sleep, thanks for the discussion :)
gn
So the main thing I’ve been able to think of are objects + a method in those objects, I’ll provide a concrete example of this later, and also, I would like to note GHC has something very similar https://downloads.haskell.org/ghc/latest/docs/users_guide/exts/liberal_type_synonyms.html#extension-LiberalTypeSynonyms
Last updated: Jun 16 2026 at 16:19 UTC