I am not sure what to call this, other than type wrapping. I am new to Roc and no more than intermediate level experience with strong (functional) types. My goal, which is possibly misguided, is to take a List(U8) and specialize it with a known length and intended usage. e.g. 32-byte sk, 32-byte pk, 64-byte sig
Under roc-alpha-4, I used opaque types with some success for this. But my focus is now roc-nightly. With the resources I've gathered for how roc-nightly works, I did a lot of exploratory testing with LLMs to determine how to solve this problem. Here are my findings:
https://gist.github.com/rickhull/733e6020e1995c0997016559e291b0cc
Single-variant tag unions - open type
PublicKey := [PublicKeyBytes(List(U8)), ..]
SecretKey := [SecretKeyBytes(List(U8)), ..]Simple constructors - just wrap the bytes, no validation yet
make_public_key = |bytes| PublicKeyBytes(bytes)
make_secret_key = |bytes| SecretKeyBytes(bytes)
Single-variant tag unions are the idiomatic and working solution for type-safe wrappers in Roc. The pattern:
- Use open tag unions with
..for compatibility with Try.Ok- Use pattern matching for extraction (verbose but type-safe)
- Get compile-time type safety - PublicKey ≠ SecretKey
This is how core types like
BoolandTrywork in Builtin.roc.
As to whether these findings are accurate, this is an open question for me. I am not here to burden the channel with fixing my LLM dalliances, but I would like some validation, positive or negative, if I am barking up the right tree. Thanks!
Do you want to be able to hide the internal bytes so that e.g. only some functions of a specific module can access the raw bytes?
it's a chunk of data, but I want to associate a type with this chunk of data. the type says: it is Red, not Blue. it has this size. So now I have Red32, Red64, Blue32, Blue64. and the compiler can enforce that make_orange only takes Red32 and not any chunk of data.
e.g. At runtime, the user provides some user input. This could be the chunk of data. The roc program takes the user input and constructs a Red32 according to some validation rules. This can fail, and the user might be reprompted.
Is there something flawed or wrongheaded about this idea?
from what I understand, a nominal tag union with a method bytes that returns List(U8) would be nice. I struggled to make this work.
it's actually working great now. Currently I have a Signature, with methods create and bytes. there is only a single internal representation, List(U8), so I don't want to use from/to in the method names. I was thinking of create / resolve as a more generic interface. thoughts on method names?
Is there something flawed or wrongheaded about this idea?
Nope, you're good, it's a relatively common pattern. See also https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/
thoughts on method names?
I do not think that we have really built conventions around this
i don't want to use unwrap as that feels overloaded. Signature "resolves" to a List(U8)[64] (my notation). but i think create and bytes reads naturally and intuitively
I completely agree with "shotgun parsing" as an antipattern. have a defined "filter layer" at the front of the data pipeline that only lets "well formed objects" through.
I've seen people use 'pack' and 'unpack' as the pair of names for this kind of thing
ooh, that's a good one, kind of like wrap/unwrap but without overloading those terms. in ruby and perl (C?), pack and unpack are quite sophisticated in terms of constructing binary data, whereas I am just doing simple validation. interesting
not sure if this is the right place... I'm working through Exercism and working on the Collatz one, and suddenly realized... I'm not sure how to fmap easily in Roc?
I'm doing this recursively, so in a when...is... block, i'm pattern matching on whether it's odd/even, and adding 1 to the recursive steps function call. In Haskell I might `even n = succ <$> steps (3*n+1)
But how can I add 1 to Ok(1) easily without changing the signature of steps to be Result U64 -> Result U64 to enable pattern matching, when I don't have fmap?
(This is wrong, I know, but it should get the idea of what I'm trying to do)
steps : U64 -> Result U64 _
steps = |x|
when x is
0 -> Err
1 -> Ok(0)
_ if Num.is_even(x) -> steps(x/2) + 1
_ -> steps(3*x + 1) + 1
Hi Rachael, I wish I could help more. I can't tell at a glance, but are you on the new compiler or old compiler?
The Exercism track is still using the old compiler, isn’t it?
Not sure, this is the roc-cli version it's unit tests are depending on
pf: platform "https://github.com/roc-lang/basic-cli/releases/download/0.20.0/X73hGh05nNTkDHU06FHC0YfFaQB1pimX7gncRcao5mU.tar.br",
Yes, almost certainly the stable Roc version documented on the website. Outside of my very limited comfort zone.
Hi @Rachael Sexton,
You can use the ? operator or Result.map_ok
Last updated: Feb 20 2026 at 12:27 UTC