The infamous https://github.com/niclas-ahden/roc-maybe is here :tada: However, before using it, please ask yourself if you really want to? Many of us come from languages with less expressive type systems than Roc, so we may miss out on its awesomeness if we default to old habits. I encourage you to look at Roc's tags with payloads first, with which you can often create way nicer APIs than with a Maybe! You can see examples of that in other packages or platforms like basic-cli. If your heart is still set on a Maybe, here it is :love:
This can be a useful thing to link to to help people make their decision: https://roc-lang.org/faq#option-type
For sure! Thanks, I’ll add that to the README!
I was debating whether to release this at all, frankly, but yeah :man_shrugging: Let me know if people prefer that I remove it!
Someone was going to make it eventually :p
It's good that we have the necessary caveats in the README.
Indeed. I think I'd stop using it if I could use common transformations like map, with_default etc. on tag unions in an ergonomic way somehow. I use it mainly for null handling in database requests etc. where there truly is an optional value and there's no more information to convey. I could have a Null type for my db queries to be more domain-specific, but that's just Maybe in different clothing, and I'd have to redefine it for each "domain" :shrug:
Niclas Ahden said:
Indeed. I think I'd stop using it if I could use common transformations like
map,with_defaultetc. on tag unions in an ergonomic way somehow.
I've thought about this! We can infer map for structural tag unions that are shaped like Maybe for sure.
I wonder if there's a more generic name we could use than "with default" since not all of these would have "maybe" semantics necessarily
Would the inferred functions be present only for types that are defined outside a function, or would they be inferred any time some function takes an argument that happens to be like [MyThing String, OtherThing], even if this type is never used anywhere else?
Richard Feldman said:
Niclas Ahden said:
Indeed. I think I'd stop using it if I could use common transformations like
map,with_defaultetc. on tag unions in an ergonomic way somehow.I've thought about this! We can infer
mapfor structural tag unions that are shaped likeMaybefor sure.
ok this PR added it for map - try it out!
we can add with_default later after figuring out what the name should be :smile:
@Kasper Møller Andersen the way it works is:
{} or single-tag unions), then that tag union gets map and map! methods inferred which work in the only way that could make sense for a sum type of that shape :laughing: is_eq works: we look at the type to figure out if it's eligible (e.g. is_eq eligibility depends on whether there's a function type in there anywhere, since function equality is undecidable) and if it's eligible then it has the method, and otherwise if you try to use map on it you just get a METHOD MISSINGIs there any reason to have the derived function just be "map"? I ask because that limits you to only having the shape of a tag union as you describe, and I don't know if it's useful to standardise the name to map, as you can't parameterise over "something that defines map" without higher kinder types, or some associated type derivations. Given that Try has map_ok and map_err, what would be the downside of following suit and auto deriving snake case map_my_tag_name for each single-payload tag instead?
what's the use case?
I suppose I'm looking at it more like, if it's going to be added, limiting auto-derived map to just the case where there's one single payload tag seems like an artificial constraint, due to calling it map, which then needs a resolution for "which payload should be mapped?". I think it's fair to say that, if there are cases where you want to use something different but similar to Maybe whilst retaining the conveniences of map et al., you could also want something similar to Try with similar conveniences. But I will try and think of something more concrete :smile:
In the most minimal case it's the same behaviour, just with a suffix on the map. But say you add a tag later on after you realise there is another state, are you now locked out of using map if it's a structural union? (I'm not sure but) I suppose it also doesn't work with open tag unions currently, because you can't guarantee that .. doesn't contain another tag with a payload?
Something more concrete :sweat_smile: Say you were waiting for data in a RemoteData style. NotAsked, Loading, Success(Data), Failure(Error)]. You might want to map_success or map_failure.
I'd rather wait until we have an actual use case in practice, like Maybe :smile:
Richard Feldman sagde:
Kasper Møller Andersen the way it works is:
- if you have a tag union with exactly 1 tag that has exactly 1 payload, and all other tags in the union have no payloads (or have payloads that can never contain any data, e.g.
{}or single-tag unions), then that tag union getsmapandmap!methods inferred which work in the only way that could make sense for a sum type of that shape :laughing:- in general it works the same way our auto-derived
is_eqworks: we look at the type to figure out if it's eligible (e.g.is_eqeligibility depends on whether there's a function type in there anywhere, since function equality is undecidable) and if it's eligible then it has the method, and otherwise if you try to usemapon it you just get aMETHOD MISSING
I understood as much at least :smile: my question was really whether this works such that only types that are defined in a module get map functions, or if these are created for any random assortment of tags that fit the bill in a given function?
these are created for any random assortment of tags that fit the bill
:point_up: this
I appreciate making it easy to map as needed, but I don’t really like the implications of this. For example, if I publish a package with some Maybe-shaped type, isn’t map now part of my API, maybe even if my type is opaque? And even if it’s not opaque, if I expect to add more tags later, I may only want map_{tagName} in my API, because those are forward compatible.
no, this is only for structural tag unions
it's just like is_eq - you have to opt into it for nominal types if you want it
but now you can say map : _ (or write out the type) to get the automatic implementation if you don't want to implement it yourself
Oh okay, then I’m not worried :smile:
I’m not sure I would do anything for with_default though. From a performance perspective, with_default tends to be a negative influence, because it requires you to fully construct your default value, even if you end up not using it. Using raw pattern matching does not have that concern, because you only construct the default value in case you take the given branch. Of course, if your default value is just an integer, then there is no performance concern, but a default value can be anything.
In Roc, you could do a database lookup to fetch your default value, and without lazy evaluation on that, with_default becomes much more expensive than pattern matching. You can then introduce a with_lazy_default that takes a function to run instead. Or maybe use that as the only option?
Anyway, my point is that I’d rather not make something like with_default too easy, so we can avoid the footgun :grinning_face_with_smiling_eyes:
Last updated: Jul 23 2026 at 13:15 UTC