Stream: beginners

Topic: How to deconstruct wrapper type?


view this post on Zulip Jasper Woudenberg (Jun 28 2026 at 09:36):

Suppose I have an opaque Xml type like so:

Xml :: Str.{
    concat : Xml, Str => Xml
    concat = |prev, next| prev.concat(next)
}

And I want to define an Html := Xml type on top, how do I unwrap Html to obtain the underlying Xml?

Some things I tried that don't work:

# Doesn't unwrap the `Html` and returns a Html != Xml type-error
body : Html -> Html
body = |prev| match prev {
    xml => Html.(xml.concat("<body/>"))
}

# Syntax error
body : Html -> Html
body = |prev| match prev {
  Html.(xml) => Html.(xml.concat("<body/>"))
}

I can work around this by defining instead Html := { xml: Xml } and destructuring the record.

view this post on Zulip Richard Feldman (Jun 28 2026 at 15:18):

I'd like for that last example to work (and also destructuring right in the argument, e.g. |Html.(xml)| but I don't think @Jared Ramirez has gotten to that yet :smile:

view this post on Zulip Jared Ramirez (Jun 28 2026 at 20:24):

destructuring via Nominal.(val) just landed in https://github.com/roc-lang/roc/pull/9851

should now be able to do:

Html :: Str. {
  body : Html -> Html
  body = |Html.(xml)| Html.(xml.concat("<body/>")
}

view this post on Zulip Bryce Miller (Jun 30 2026 at 03:02):

I'm having a bit of trouble with something similar while porting roc-pg.

I have the following opaque type:

Decode(value, err) :: List(U8) -> Try(
    {
        decoded : value,
        remaining : List(U8),
    },
    err,
)

and in the associated items block I have this function:

    await : Decode(a, err), (a -> Decode(b, err)) -> Decode(b, err)
    await = |Decode.(decoder_a), callback| Decode.(
        |bytes| {
            a = decoder_a(bytes)?
            Decode.(decoder_b) = callback(a.decoded)
            decoder_b(a.remaining)
        },
    )

So I'm trying to unwrap the function within the opaque wrapper and then call it. Is there a different syntax I should be using for this?

(Here's the old Roc code for reference)

await : Decode a err, (a -> Decode b err) -> Decode b err
await = |@Decode(decoder_a), callback|
    @Decode(
        |bytes|
            Result.try(
                decoder_a(bytes),
                |a|
                    @Decode(decoder_b) = callback(a.decoded)
                    decoder_b(a.remaining),
            ),
    )

view this post on Zulip Bryce Miller (Jun 30 2026 at 03:15):

It also doesn't work if i skip the unwrap attempt decoder_b = callback(a.decoded) or if I attempt to remove the outer wrapping.

view this post on Zulip Richard Feldman (Jun 30 2026 at 03:29):

that looks right to me - any ideas @Jared Ramirez? :thinking:

view this post on Zulip Aurélien Geron (Jun 30 2026 at 09:01):

It looks like the issue is with this line:

Decode.(decoder_b) = callback(a.decoded)

Using a match instead works fine:

await : Decode(a, err), (a -> Decode(b, err)) -> Decode(b, err)
await = |Decode.(decoder_a), callback| {
    Decode.(
        |bytes| {
            a = decoder_a(bytes)?
            match callback(a.decoded) {
                Decode.(decoder_b) => decoder_b(a.remaining)
            }
        }
    )
}

view this post on Zulip Bryce Miller (Jun 30 2026 at 12:35):

Ok, I can use a match for now and revisit if/when the other syntax becomes possible :slight_smile:

view this post on Zulip Jared Ramirez (Jun 30 2026 at 12:45):

looks like a parser issue! Will check it out

view this post on Zulip Jared Ramirez (Jun 30 2026 at 23:16):

fix is up here: https://github.com/roc-lang/roc/pull/9900


Last updated: Jul 23 2026 at 13:15 UTC