For my upcoming talk about parser combinators, I've defined the following types:
Parser a := Str -> ParseResult a
ParseResult a : Result { value : a, rest : Str } Str
An opaque Parser
type and a ParseResult
type alias. I now tried to define a simple parser like so:
parse_digit : Parser U8
parse_digit = |input|
Err("not implemented")
This fails with the following message:
── TYPE MISMATCH in Parser.roc ─────────────────────────────────────────────────
Something is off with the body of the parse_digit definition:
7│ parse_digit : Parser U8
8│> parse_digit = |input|
9│> Err("not implemented")
The body is an anonymous function of type:
(* -> [Err Str])
But the type annotation on parse_digit says it should be:
Parser U8
I'm not entirely sure why as I've annotated parse_digit
as Parser U8
, but ok. Next, after looking through the Zulip conversations I found out that you can @
prefix the lambda. Let's try that.
parse_digit : Parser U8
parse_digit = |@Parser input|
Err("not implemented")
But this also fail with a somewhat different message:
── TYPE MISMATCH in Parser.roc ─────────────────────────────────────────────────
Something is off with the body of the parse_digit definition:
7│ parse_digit : Parser U8
8│> parse_digit = |@Parser input|
9│> Err("not implemented")
The body is an anonymous function of type:
(Parser * -> [Err Str])
But the type annotation on parse_digit says it should be:
Parser U8
I'm sure I don't understand what's going on here, so any pointers would be much appreciated. I understand the problem must be related to how opaque types work, because if it's not opaque it compiles.
To create an opaque value, you wrap the value with the name of the tag prefixed with @
To take the inner data out of the opaque value, you do what you did with the function args
Where you make a pattern that matches on @Opaque(inner)
Sam Mohr said:
To create an opaque value, you wrap the value with the name of the tag prefixed with @
Right. What I don't understand is why parse_digit
isn't compatible with Parser U8
. My understanding is that the type is only opaque outside the module?
So to do what you want, you'll need to wrap the entire function in the opaque wrapper, not just the args or the return value
The type is the same inside and outside the module where the opaque type is defined
Sam Mohr said:
So to do what you want, you'll need to wrap the entire function in the opaque wrapper, not just the args or the return value
I'm pretty sure I tried that. Hold on.
The only difference is that you can't create or unwrap an opaque value outside of its defining module
This error message isn't super useful, but I'm not sure what specific improvement we could make
We could suggest moving where the opaque wrapper is if there's an opaque type on one side and a non-opaque type on the other side
When I wrap the entire lambda in @Parser
, I get an error message saying I can't call the function with an argument because the type is opaque. Which I guess makes sense? What I wanted to achieve was having Parser
be opaque as in you can't construct them yourself, you'll have to use the module interface, but perhaps I need some sort of constructor to achieve that then?
Yes, for others to construct a parser they'll need a constructor
Yeah, I figured. But from within the module itself I was hoping that I can construct my own parsers without having to do that, but I can't really make it work.
parse_digit : Parser U8
parse_digit = @Parser(|input| Err("not implemented"))
Does this not work?
Nope.
Getting a laptop...
Well, it compiles, but my expect
fails saying I can't call my parser. This is all done in the same module.
── TOO MANY ARGS in Parser.roc ─────────────────────────────────────────────────
The parse_digit value is an opaque type, so it cannot be called with
an argument:
11│ expect parse_digit("123") == Ok { value: 1, rest: "123" }
^^^^^^^^^^^
I can't call an opaque type because I don't know what it is! Maybe you
meant to unwrap it first?
────────────────────────────────────────────────────────────────────────────────
Oh yeah, you can't call it!
The hint at the bottom is right in that you need to get the inner function out of the opaque value first
There's no auto dereferencing in Roc
Oh, I see, because Parser
is opaque, you can’t really make assumptions about it. So I guess that means I need to change how parse_digit
works then. Because unwrapping only works within the module, right?
Yep, unless you provide a get_inner : Parser a -> (Str -> ParseResult a)
equivalent
But I'd recommend something like this
module []
Parser a := Str -> ParserResult a
ParserResult a : Result { data : a, rest : Str } Str
word : Parser Str
word = @Parser(
|input|
{ before, after } =
Str.split_last(input, " ")
? |_| "Failed to split string"
Ok({ data: before, rest: after }),
)
parse : Parser a, Str -> ParserResult a
parse = |@Parser(parser), input| parser(input)
expect
word
|> parse "abc def"
== Ok({ data: "abc", rest: "def" })
In that you provide an interface using the opaque value that doesn't leak the encapsulated impl
Ah, that makes sense! I test using the public interface. I have a run
equivalent.
Yeah, run
works!
Thanks for the help, @Sam Mohr. Appreciate it. :smile:
Of course!
And of course, expect minor changes to this with the new compiler
Namely, custom/nominal types will not need the @ in the future, but will otherwise work very similarly
But you can optionally expose the tags of a custom union to other modules...
It's a bit more complicated, but basically the same
We'll put out docs when they're implemented!
When/where is your parser combinator talk?
Sam Mohr said:
When/where is your parser combinator talk?
February 27, so Thursday next week. It's for a local meetup where I live, but fun nevertheless. I've never held one before. :sweat_smile: I'm going to walkthrough building a parser for arithmetic expressions, e.g., 1 * (1 + 3) + 2
.
Awesome! A very fun usage of FP
If you want to do idiomatic Roc, consider throwing on record builders right at the end for an advanced feature
Might be to complex for a short talk
Sam Mohr said:
If you want to do idiomatic Rust, consider throwing on record builders right at the end for an advanced feature
Roc you mean? I think I have roughly an hour allocated, so it could work.
https://github.com/roc-lang/examples/blob/main/examples/RecordBuilder/DateParser.roc
Yep, typo
It's a very clean interface, you just need to figure out how to explain it well enough
Yeah, that would actually be an interesting "alternative" to what I'll go through. Record builders completely slipped my mind.
They're an unusual feature, but they're really nice
Kevin Hovsäter said:
Sam Mohr said:
When/where is your parser combinator talk?
February 27, so Thursday next week. It's for a local meetup where I live, but fun nevertheless. I've never held one before. :sweat_smile: I'm going to walkthrough building a parser for arithmetic expressions, e.g.,
1 * (1 + 3) + 2
.
Does this meetup happen to be in/around Stockholm? I could be your cheerleader (outfit not included) :sweat_smile:
Niclas Ahden said:
Kevin Hovsäter said:
Sam Mohr said:
When/where is your parser combinator talk?
February 27, so Thursday next week. It's for a local meetup where I live, but fun nevertheless. I've never held one before. :sweat_smile: I'm going to walkthrough building a parser for arithmetic expressions, e.g.,
1 * (1 + 3) + 2
.Does this meetup happen to be in/around Stockholm? I could be your cheerleader (outfit not included) :sweat_smile:
Haha! it's happening in Växjö, so unfortunately not close by.
Crap, I never get to practice my moves -- next time!
Kevin Hovsäter has marked this topic as resolved.
Last updated: Jul 06 2025 at 12:14 UTC