In our CIR we currently have this... which I think isn't quite right.
/// A declaration of a new type - whether an alias or a new nominal nominal type
///
/// Only valid at the top level of a module
s_type_decl: struct {
header: CIR.TypeHeader.Idx,
anno: CIR.TypeAnno.Idx,
where: ?CIR.WhereClause.Span,
region: Region,
},
In my understanding both Foo : ...
and Foo := ...
are statements. They are both type declarations. One is an Alias Type delcaration, and the other is a Nominal Type declaration.
I could imagine a future where people default to refer to these as;
I'm wondering, how should we represent the nominal types in our CIR? Currently I think the s_type_decl
is being used for Aliases because that is all we have implemented so far. Should we make another separate variant to distinguish between these two, or maybe a flag is_alias
or similar in this variant?
that's how we do it in the Rust version of the compiler but I'm not thrilled with how it has worked out
I'd rather we just had .s_alias_decl
and .s_nominal_decl
or something like that
This is somewhat related, but can nominal tag union be open/extensible? In particular, are these allowed?
Color := [Red, Green, Blue]*
Color other := [Red, Green, Blue]other
OtherColor : [Purple]
Color := [Red, Green, Blue]OtherColor
I didn't think that was permitted
Maybe it would be useful though. I really dont know
I think this would be allowed:
Color other := [Red, Green, Blue]other
OtherColor : [Purple]
MixedColor : Color(OtherColor)
At least I see no reason it wouldn't be allowed off the top of my head....but I may be missing something
I'm not arguing for or against!
When checking types of nominal tag unions, if the above examples are not allowed, then we can really easily check if a tag (eg Blue
or Yellow
) is a member of the closed tag set and raise a diagnostic in Can if not. But if the above is allowed, then we have to check the tag membership in type-checking (so we can fully expand any extensible types)
This on the other hand:
Color := [Red, Green, Blue]*
Would just be equivalent to:
Color := [Red, Green, Blue]
no, nominal tag unions can't be extended
this is actually very important!
if we allow them to be extensible, then they don't solve https://github.com/roc-lang/rfcs/pull/1 and we have that problem again
Ooo okay, good to know.
we could allow some special syntax sugar for them, that's just sort of like "let me copy and paste that for you" e.g.
Color := [Red, Green, Blue, ..OtherColor]
OtherColor : [Purple]
That will make validating nominal tags easier then!
but ..a
would need to be disallowed
extension variables in nominal tag unions must be compile errors
Cool. Can nominal records be extensible?
also no
How is
OtherColor : [Purple]
Color := [Red, Green, Blue, ..OtherColor]
Different from:
OtherColor : [Purple]
AbstractColor(a) := [Red, Green, Blue, ..a]
Color : AbstractColor(OtherColor)
the problem is allowing the type variable:
AbstractColor(a) := [Red, Green, Blue, ..a]
now you can write functions where the type's layout at runtime varies based on the arguments you pass to that function
Are we permitting nominal records? I thought the intent was just Tags for now
Or maybe that was only specifically recursive types must be tags.
Richard Feldman said:
the problem is allowing the type variable:
AbstractColor(a) := [Red, Green, Blue, ..a]
now you can write functions where the type's layout at runtime varies based on the arguments you pass to that function
Ah yeah, I guess it essential makes the extension types not nominal which kinda defeats the whole point of making the type nominal in the first place.
Luke Boswell said:
Are we permitting nominal records? I thought the intent was just Tags for now
I'd love to know the answer to this... is this valid 0.1 syntax?
ImaginaryNumber := { real: F64, imag: F64 }
Last this was discussed, I think the decision was to start simple and require a wrapper tag:
ImaginaryNumber := [Wrapper { real: F64, imag: F64 }]
Not sure if any opinions have changed though.
Definitely an @Richard Feldman question
Would be bice to learn why nominal types feature cant be just a "uniqueness" wrapper around any type. Like if there was a special tag Unique
so Id : Unique(U64)
@Kiryl Dziamura It was originally the Custom Types proposal... and we subsequently renamed them to "nominal types" because that was what everyone called them anyway
So I think the only reason is just we are starting simple and only implementing Tag unions to start with...
There's a huge amount of discussion to recall though, and as I learn more and implement things I keep revisiting these discussions and learning more about them each time.
Luke Boswell said:
Luke Boswell said:
Are we permitting nominal records? I thought the intent was just Tags for now
I'd love to know the answer to this... is this valid 0.1 syntax?
ImaginaryNumber := { real: F64, imag: F64 }
we had definitely planned it at some point. I was talking with @Jared Ramirez about implementation challenges but I don't remember where we ended up. :sweat_smile:
Yeah, since I wrote that comment, I (re)discovered the Custom-Types proposal and linked that just above.
Richard and I discussed how nominal wrappers for all types (beyond just tag unions) and that convo ended with something similar to how opaque types currently work:
module X exposes
[ Color(..) // transparent
, ImaginaryNumber // opaque
]
Color := [Red, Green, Blue]
green = Color.Green
ImaginaryNumber := { real: F64, imag: F64 }
number = ImaginaryNumber.{ real = 1, imag = 2 }
Point := ( F64, F64 )
point = Point.(1.0, 2.0)
UserId := Dec
userId = UserId.(384618)
But then, to pull a value out of the nominal type, you would do:
getColor : Color -> [Red, Green, Blue]
getColor = |Color(inner)| inner
Related to nominal records though, what we can't do is access record fields without unwrapping the type:
ImaginaryNumber := { real: F64, imag: F64 }
number = ImaginaryNumber.{ real = 1, imag = 2 }
x = number.real // Can't do
x = match number is ImaginaryNumber(r) => r.real // Can do
Without introducing something akin to type classes
ahh right, that was it
accessing fields was the problem
Can we just use nominal type static dispatch to make it work....or at least make it mostly work?
number.real()
calls static dispatch to unwrap the type and get the real
field.
Clearly not quite as nice
Yeah, you could do
real = |ImaginaryNumber(inner)| inner.real
// then later
num.real()
or even:
inner = |ImaginaryNumber(inner)| inner
// then later
num.inner().real
Depends on how much you want to expose of the type's internals i guess
I guess we just need an @property
decorator... :laughing:
We need a pitchfork emoji
How should this work with imported transparent nominal types?
// styles/Color.roc
module [
Color(RGB, RGBA, ...)
]
Color := [
RGB(U8, U8, U8),
RGBA(U8, U8, U8, Dec),
Named(Str),
Hex(Str),
]
// MyModule.roc
import styles.Color as CC
blue1 : CC.RGB
blue1 = CC.RGB(0,0,255)
// or
blue2 : CC.RGB
blue2 = CC.Color.RGB(0,0,255)
Here, can you access the RGB
constructor directly with CC.RGB
? Or do you have to do CC.Color.RGB
?
CC.Color.RGB
but if you did import styles.Color as CC exposing [Color]
then Color.RGB
should work
so it's about the type being in scope vs not
Anton said:
We need a pitchfork emoji
I got you a few options:
And we still have:
Last updated: Jul 26 2025 at 12:14 UTC