What is the scope of union tags?
If I create a tag union with a tag Gear in one module (function?), and another with Gear in another module, are they the same tag? Do they have to take the same payload? If not, how do I specify whether it's the Gear from Mechanical (with an iteger number of teeth payload)or the Gear from Camping (with a string description payload)? It doesn't seem like roc accepts Mechanical.Gear as a valid way to specify a tag.
I imagine they would be different unions and not unify even if they have the same name, because they have different payloads.
Should be easy enough to test. But I guess your looking for a more formal answer, which I don't really know much about.
tag unions work the same way as records: it doesn't matter where they're defined, what matters are the names of the tags and that the payloads are type-compatible
Richard Feldman said:
tag unions work the same way as records: it doesn't matter where they're defined, what matters are the names of the tags and that the payloads are type-compatible
Does this mean that roc can't infer types from the arguments to tags?
Specifically, if I use (Gear 7) somewhere, can roc object to me using (Gear "seven") elsewhere? (Or for a more likely error, Gear 7 "seven" and Gear "seven" 7?)
I mean, on the one hand when I use pattern matching on Gear I'd like roc to be able to infer the types of the arguments. On the other hand I would rather my program not suddenly explode because I imported a module that uses Gear with different types internally. How is this resolved?
Anne Archibald said:
if I use (Gear 7) somewhere, can roc object to me using (Gear "seven") elsewhere?
nope, that's fine
the type inference is based on the local to the usage site, also like records
just like how record field names are arbitrary strings that are in a shared global namespace, so are tag names
and just like how you can have a record with { email : Str }
in one place and { email : Email }
in another place, no problem, you can also have a tag union with [Email Str]
and [Email Email]
somewhere else, no problem
I guess what I'm asking is if I write
when thing is
Gear a b -> ...
what, if any, constraints does that place on the types of a and b?
just based on that code alone? none whatsoever
like you can write:
\thing ->
when thing is
Gear a b -> ...
and it will infer the type of a
and b
based on how they get used in the ...
and if they just get returned without anything else happening to them that would constrain their types, then their inferred types will both be type variables (probably also named a
and b
, since those are the first two auto-generated type variable names the compiler will choose), and if they don't even get returned, then their inferred types will be *
I guess I was thinking of Gear
in this context as being like a constructor, which would constrain the types of its input. I have been making stream objects with Stream nextFunction initialState
and assuming this would constrain the types of its arguments. Should I create a function makeStream = \nextFunction, initialState -> Stream nextFunction initialState
so that I can provide this restriction?
Anne Archibald said:
I guess I was thinking of
Gear
in this context as being like a constructor, which would constrain the types of its input.
ah, yeah so tags don't do that, they just tag data with a name :big_smile:
like records, they are intentionally just about labeling and that's it
if you want to constrain the types, then yeah making a function like that is a good start, but the best way to enforce that is with an opaque wrapper
https://www.roc-lang.org/tutorial#opaque-types
so something like:
\nextFunction, initialState -> @MyOpaqueTypeName (Stream nextFunction initialState)
I experimented with opaque types (of necessity) when I thought an Ability would solve my polymorphism problem. In this case, though, I don't want to restrict creation and inspection of these objects to code in the Stream module - people elsewhere should also be able to write efficient Stream operations. I guess I can make an opaque type and then provide accessors that let people get at all its gubbins?
you can, although if you give access to every internal detail then every change to internal structure becomes a breaking change, so I generally default to exposing as little as seems necessary :big_smile:
Richard Feldman said:
you can, although if you give access to every internal detail then every change to internal structure becomes a breaking change, so I generally default to exposing as little as seems necessary :big_smile:
This brings me to my polymorphism problem :-p even using Ability streams with different internal state are not compatible unless I wrap them in a callable. And I'm not at all clear whether the optimisations my stream code is meant to allow can work with a callable wrapper in the way. (A chain of stream operations should be inline-able down to a single function that steps through the stream elements, with no recursion/looping anywhere inside.) Anyway, that's another thread. Thanks for clarifying how tags work!
sure, happy to clarify! :smiley:
I just checked, and you probably said so but:
While you can have all the tags "Gear", "Gear 7", and "Gear 7 8", they cannot exist in the same union type.
If I understand correctly, normally, if you have two union types, their, uh union is also a valid type. That is not true if any of their tags have the same spelling but different "signatures".
I guess, though, that one doesn't normally take the union of union types defined in different places. The exception would seem to be error return values - if one package defines HttpFailure to take an error code argument and another defines it without, that might give confusing problems using Result?
Yeah, you would have to add a mapping or wrap tags or something to deal with the discrepancy if using two different libraries with the same error tag name but different contained data.
It's really trivial to just wrap by library if you need to. someTask |> Task.mapErr LibraryWrapperTag
I think long term it should be possible to make really clear errors for this case. Print the source code of both definitions, note they are incompatible, and link to a page that shows common solutions like wrapping and mapping.
Last updated: Jul 06 2025 at 12:14 UTC