Stream: beginners

Topic: ✔ I need help understanding opaque types


view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 08:40):

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.

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:08):

To create an opaque value, you wrap the value with the name of the tag prefixed with @

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:09):

To take the inner data out of the opaque value, you do what you did with the function args

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:09):

Where you make a pattern that matches on @Opaque(inner)

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:10):

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?

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:10):

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

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:10):

The type is the same inside and outside the module where the opaque type is defined

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:11):

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.

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:11):

The only difference is that you can't create or unwrap an opaque value outside of its defining module

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:12):

This error message isn't super useful, but I'm not sure what specific improvement we could make

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:13):

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

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:14):

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?

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:16):

Yes, for others to construct a parser they'll need a constructor

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:17):

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.

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:17):

parse_digit : Parser U8
parse_digit = @Parser(|input| Err("not implemented"))

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:17):

Does this not work?

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:17):

Nope.

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:17):

Getting a laptop...

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:18):

Well, it compiles, but my expect fails saying I can't call my parser. This is all done in the same module.

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:18):

── 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?

────────────────────────────────────────────────────────────────────────────────

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:20):

Oh yeah, you can't call it!

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:21):

The hint at the bottom is right in that you need to get the inner function out of the opaque value first

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:21):

There's no auto dereferencing in Roc

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:23):

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?

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:26):

Yep, unless you provide a get_inner : Parser a -> (Str -> ParseResult a) equivalent

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:26):

But I'd recommend something like this

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:27):

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" })

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:28):

In that you provide an interface using the opaque value that doesn't leak the encapsulated impl

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:28):

Ah, that makes sense! I test using the public interface. I have a run equivalent.

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:29):

Yeah, run works!

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:30):

Thanks for the help, @Sam Mohr. Appreciate it. :smile:

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:31):

Of course!

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:32):

And of course, expect minor changes to this with the new compiler

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:32):

Namely, custom/nominal types will not need the @ in the future, but will otherwise work very similarly

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:33):

But you can optionally expose the tags of a custom union to other modules...

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:33):

It's a bit more complicated, but basically the same

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:33):

We'll put out docs when they're implemented!

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:33):

When/where is your parser combinator talk?

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:50):

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.

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:50):

Awesome! A very fun usage of FP

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:51):

If you want to do idiomatic Roc, consider throwing on record builders right at the end for an advanced feature

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:51):

Might be to complex for a short talk

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:52):

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.

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:53):

https://github.com/roc-lang/examples/blob/main/examples/RecordBuilder/DateParser.roc

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:53):

Yep, typo

view this post on Zulip Sam Mohr (Feb 22 2025 at 13:54):

It's a very clean interface, you just need to figure out how to explain it well enough

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 13:55):

Yeah, that would actually be an interesting "alternative" to what I'll go through. Record builders completely slipped my mind.

view this post on Zulip Sam Mohr (Feb 22 2025 at 14:05):

They're an unusual feature, but they're really nice

view this post on Zulip Niclas Ahden (Feb 22 2025 at 15:16):

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:

view this post on Zulip Kevin Hovsäter (Feb 22 2025 at 15:20):

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.

view this post on Zulip Niclas Ahden (Feb 22 2025 at 15:21):

Crap, I never get to practice my moves -- next time!

view this post on Zulip Notification Bot (Feb 22 2025 at 16:15):

Kevin Hovsäter has marked this topic as resolved.


Last updated: Jul 06 2025 at 12:14 UTC