I'm not sure if there's some syntax I don't yet know, but I'm getting a type error only when I don't annotate push or pop from the following:
Stack(v) :: {s: List(v)}.{
push : Stack(v), v -> Stack(v)
push = |{s}, v| {
{s: s.append(v)}
}
pop : Stack(v) -> (Stack(v), Try(v, [ListWasEmpty]))
pop = |s| {
last = s.last()
dropped = s.drop_last(1)
(Stack.{s: dropped}, last)
}
new = || {Stack.{s: []}}
}
In both push and pop, the opaque backing type is destructured to get the list. If I use without annotations, I get the following error:
┌───────────────┐
│ TYPE MISMATCH ├─ The push method on Stack has an incompatible type. ────────────────────────────┐
└┬──────────────┘ │
│ │
│ $s = $s.push(2) │
│ ‾‾ │
└───────────────────────────────────────────── main.roc:54:10 ┘
The method push has the type:
{ s: a }, b -> { s: c } where [a.append : a, b -> c]
But I need it to have the type:
Stack(v), a -> _ret
where [a.from_numeral : Numeral -> Try(a, [InvalidNumeral(Str)])]
Adding in annotations the error goes away. Here is an example use for reproduction
main! = |_| {
var $s = Stack.new()
$s = $s.push(2)
Ok({})
}
Stack member functions can access opaque internal state, as well as any other record of shape {s} where s.append ...|Stack.{s}|, so I'm not sure how else to signal without annotations that I am expecting Stack not a generic record.Instead of Stack.{s}, you can use Stack.({x})! But I think that pop would still not work without the annotation (the compiler needs some way to know that s is Stack)
Jonathan said:
- I would have thought that without annotations would be ok, as the
Stackmember functions can access opaque internal state, as well as any other record of shape{s} where s.append ...
the `Stack` member functions can access opaque internal state: this is not true! the scope inside the Stack.{ ... } has no special access to Stack internals!as well as any other record of shape `{s}` : this is correct! in many cases the compiler can lift structural types like a record into the nominal type implicitlyMore broadly, this is a known (and not really solvable) constraint. In many cases, the compiler can figure out how to lift structural values (eg { ... } or a tag) into a nominal (eg Stack), but not always. In these cases, either annotation or explicitly using the nominal constructor is necessary (eg Stack.({ ... }))
All that said, Stack.{s} should work, so that is definitely a bug. I will make an issue & fix
Stack.({s})
Ahh :face_palm: Thank you, that's bitten me a few times now.
Stack.{s} should work
Is it conceptually different from Stack(.. or just equivalent sugar?
has no special access to Stack internals
I don't follow here, isn't this the point of opaque? From the langref:
outside its defining module the backing representation is hidden, so the type can only be created and inspected through the methods that module exposes.
equivalent sugar!
then sorry, my wording was confusing there:
so more on that second point: for all accessible nominal types (eg an opaque nominal in the defining module OR a transparent Nominal anywhere), the compiler can coerce that nominal's backing type into the nominal itself, is it's clear from surrounding context. eg:
xxx : Stack
xxx = { s: [ ... ] }
works, because we know that xxx should be Stack, so we can coerce the plain record
what I meant was that inside a nominal types method block (Stack := { s: List }.{ ... }), there no extra coercion logic, just the regular coercion I just described
I have a PR up for |Stack.{s}|! https://github.com/roc-lang/roc/pull/10190
Jared Ramirez said:
what I meant was that inside a nominal types method block (
Stack := { s: List }.{ ... }), there no extra coercion logic, just the regular coercion I just described
ahh, gotcha. Thanks!
Thank you, that's bitten me a few times now.
And also, I'm not sure what to suggest, but I have found it a bit easy to mix up the similar syntaxes |S.({x})|/|S.{x}| and |S({x})| for two distinct types. I don't imagine I'll be the last, perhaps an error hint for this kind of thing?
Last updated: Jul 23 2026 at 13:15 UTC