Stream: API design

Topic: Html API in new compiler


view this post on Zulip Jasper Woudenberg (Jun 21 2026 at 19:28):

Hey everyone! I've been lurking the past months watching the new compiler development. Exciting stuff! Now that things are coming together I'm looking to pick up the Jay again, the static site generator platform I worked on for the previous version of the compiler.

I've been thinking a bit about how to design the Html-builder API with the new Roc syntax, was wondering if anyone else has thoughts on that.

Here's one idea I have for an API, this is assuming strings can be interpreted as custom types at compile time (which I think is planned, right?):

page : Html
page =
  "body"
    .child("h1".text("Header text!"))
    .child("a".href("./other-page.html").text("click me!"))

Any other designs already out there?

view this post on Zulip Romain Lepert (Jun 21 2026 at 19:42):

I thought you'd have to name the custom type like "h1".Tag.text("Header text!")

view this post on Zulip Jasper Woudenberg (Jun 21 2026 at 20:23):

If I don't misunderstand the explicit type is to force interpretation as a certain type, but if I understand Richard correctly here it's not necessary:
#ideas > Standardized HTTP interface for Roc @ 💬

view this post on Zulip Richard Feldman (Jun 21 2026 at 22:49):

so this is very interesting timing because I've been thinking about giving [ ... ] the same treatment as what we've done for number and string literals

view this post on Zulip Richard Feldman (Jun 21 2026 at 22:51):

more specifically, in the same way that you can now define your own nominal type with a from_numeral : ... method, and that method will get called at compile time if a number literal unifies with your type, and the same for from_quote : ... and string literals, I was thinking about having bracket literals have from_brackets : ...

view this post on Zulip Luke Boswell (Jun 21 2026 at 22:55):

I was about to send you a DM @Richard Feldman asking if that was possible... the usecase I had in mind was our unicode package for grapheme segmentation. Here is a snippet from the current impl -- which from memory I thought was a list pattern match, but it appears it's a tuple

# Used internally to implement the [UNICODE TEXT SEGMENTATION](https://www.unicode.org/reports/tr29/)
# algorithm.
split_help : _, List CodePoint, List GBP, Tokens -> Tokens
split_help = |state, code_points, break_points, acc|
    next_c_ps = List.drop_first(code_points, 1)
    next_b_ps = List.drop_first(break_points, 1)

    when (state, code_points, break_points) is
        # Special handling for empty list
        (Next, [], _) -> acc
        # Special handling for last codepoint
        (Next, [cp], _) -> List.concat(acc, [CP(cp), BR(GB2)])
        (AfterHangulL(prev), [cp], [bp]) if bp == L or bp == V or bp == LV or bp == LVT -> List.concat(acc, [CP(prev), NB(GB6), CP(cp), BR(GB2)])
        (AfterHangulLVorV(prev), [cp], [bp]) if bp == V or bp == T -> List.concat(acc, [CP(prev), NB(GB7), CP(cp), BR(GB2)])
        (AfterHangulLVTorT(prev), [cp], [bp]) if bp == T -> List.concat(acc, [CP(prev), NB(GB8), CP(cp), BR(GB2)])
        (AfterHangulL(prev), [_], [_]) -> split_help(LastWithPrev(prev), code_points, break_points, acc)
        (AfterHangulLVorV(prev), [_], [_]) -> split_help(LastWithPrev(prev), code_points, break_points, acc)
        (AfterHangulLVTorT(prev), [_], [_]) -> split_help(LastWithPrev(prev), code_points, break_points, acc)
        (LastWithPrev(prev), [cp], [bp]) if bp == Control or bp == CR or bp == LF -> List.concat(acc, [CP(prev), BR(GB5), CP(cp), BR(GB2)])
        (LastWithPrev(prev), [cp], [bp]) if bp == Extend -> List.concat(acc, [CP(prev), NB(GB9), CP(cp), BR(GB2)])
        (LastWithPrev(prev), [cp], [bp]) if bp == ZWJ ->
            if prev |> CodePoint.to_u32 |> InternalEmoji.is_pictographic then
                List.concat(acc, [CP(prev), NB(GB11), CP(cp), BR(GB2)])
            else
                List.concat(acc, [CP(prev), NB(GB9), CP(cp), BR(GB2)])

view this post on Zulip Richard Feldman (Jun 21 2026 at 22:55):

yeah we could do the same thing for tuples if desired

view this post on Zulip Richard Feldman (Jun 21 2026 at 22:59):

I'm also not saying the following is a good idea, just exploring, but we've also talked about a subscript operator - e.g. foo[bar] desugars to foo.subscript(bar) - we could combine these in an arguably silly way:

page : Html
page =
    body[[
        h1[["Header text!"]],
        a[["click me!"]].href("./other-page.html")
    ]]

view this post on Zulip Richard Feldman (Jun 21 2026 at 22:59):

so that would be where you'd implement subscript and have it accept a collection of child elements etc.

view this post on Zulip Richard Feldman (Jun 21 2026 at 23:04):

(I don't think a "varargs subscript" would work, e.g. a[b, c, d] desugars to a.subscript(b, c, d) because we don't have a concept of "function that accepts varargs" and I don't think adding one would be a good idea)

view this post on Zulip Jasper Woudenberg (Jun 22 2026 at 15:12):

I think a .subscript method like that could have all manner of uses, good and ill. If I understand it correctly, it could be used a bit like a callable trait, to turn any nominal type into something function-like. You might use it to do something constructor-like for instance: Person[{ name: "Fred", age: 54 }].

This got me thinking, suppose you'd build the Html API around a different operator:

page : Html
page =
    body + [
        h1 + ["Header text!"],
        a.href("./other-page.html") + ["click me!"],
    ]

I think this has the same main downside as (ab)using .subscript: being maybe a bit too cute. But otherwise I think I might like this API better than the subscript-based one (which isn't to say I love it necessarily), because:

view this post on Zulip Jasper Woudenberg (Jun 22 2026 at 15:15):

Upside of the original design I posted compared to both operator-based designs: Using separate .child and .text methods for adding different types of Html nodes frees .from_str for creating tags ("hi"). That in turn means you don't need to qualify or expose a bunch of functions from Html.

view this post on Zulip Dan G Knutson (Jun 22 2026 at 19:11):

I like the original method chaining / builder API.

view this post on Zulip Jasper Woudenberg (Jun 22 2026 at 19:46):

I've realized the operator-based approach could be made to work with string-based tags using a different operator for text and non-text nodes, like this:

page : Html
page =
    "body"
        + ("h1" * "Header text!")
        + ("a".href("./other-page.html") * "click me!")

This currently doesn't compile, because the compiler doesn't realize "a" isn't a Str here, but I think that might be a bug?

view this post on Zulip Richard Feldman (Jun 23 2026 at 02:37):

unfortunately that's not a bug, it's a limitation

view this post on Zulip Richard Feldman (Jun 23 2026 at 02:37):

if you look at the expression "a".href("./other-page.html") in isolation

view this post on Zulip Richard Feldman (Jun 23 2026 at 02:38):

the only clue the compiler has about whether or not "a" is a Str is the return type of href

view this post on Zulip Richard Feldman (Jun 23 2026 at 02:38):

but it can't know what href is a method on :smile:

view this post on Zulip Richard Feldman (Jun 23 2026 at 02:42):

so basically the type of "a".href("./other-page.html" is

val where [
    val.from_quote : _ -> _,
    val.href : _ -> _,
]

view this post on Zulip Richard Feldman (Jun 23 2026 at 02:43):

and even if the return type of href is statically known, that still doesn't give the compiler any clues about what val is - it could be anything with an href and a from_quote method, as long as the return type lined up!

view this post on Zulip Richard Feldman (Jun 23 2026 at 02:44):

so it defaults to Str and then says "hey Str doesn't have href"

view this post on Zulip Richard Feldman (Jun 23 2026 at 02:45):

I don't think the string literal idea necessarily works for this DSL unfortunately :sweat_smile:

view this post on Zulip Jasper Woudenberg (Jun 23 2026 at 07:03):

Ah, got it. I guess it's for the better, the whole thing was a bit magical anyhow.

view this post on Zulip Jasper Woudenberg (Jun 28 2026 at 10:33):

For anyone interested, this is where I'm at currently with this design:

import Html exposing [html, attr]

page : Html
page =
    html.body(
        attr,
        html.h1(attr, "Header text!")
            .a(attr.href("./other-page.html"), "click me!"),
    )

For context: I think for Jay Html behind the scenes is best represented as a stream of tokens. This because most of the Html is produced by the host when it reads source files, and the platform will for the most part embed this content from the platform in some wrapper structures. I'm hoping I'll be able to stream most of this Html through the host without too much allocating.

Builder-style API's that construct an Html tag with something like Html.a.href("link.html").text("click!") are tricky if Html is represented as a stream behind the scenes, for a couple of reasons:

The above factors pushed me to a design where a single function call produces an entire node similar to what Elm does. I quite like the result by itself though:

view this post on Zulip Lukas Juhrich (Jun 28 2026 at 11:11):

Jasper Woudenberg said:

[…] needing to pass attr to nodes without attributes

Just so I understand, what precisely is attr, technically, in this design? The default (empty) instance of an AttributeSet type or whatever it's named, which can be populated using the fluent builder chain?
Does it depend in any way on the “element”, like .h1(), it got placed in?

view this post on Zulip Jasper Woudenberg (Jun 28 2026 at 11:39):

Lukas Juhrich said:

Just so I understand, what precisely is attr, technically, in this design? The default (empty) instance of an AttributeSet type or whatever it's named, which can be populated using the fluent builder chain?

Yep, that's it exactly, an empty attribute-set!

There's no relation to the element it's placed in, so currently it's allowed to use any attribute with any html element.

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

I'm not sure if this is a good idea, but something you could explore is doing the attributes in the same builder style - so like you can do .a().href(...)

view this post on Zulip Jasper Woudenberg (Jun 28 2026 at 15:42):

Richard Feldman said:

I'm not sure if this is a good idea, but something you could explore is doing the attributes in the same builder style - so like you can do .a().href(...)

I think that would either require a structural Html implementation (rather then the tokenstream I'm using now), because each call of a builder method would need to modify a tag-under-construction.

view this post on Zulip Richard Feldman (Jun 28 2026 at 16:07):

not sure about the tradeoffs but fwiw the way GPUI does this is the other way around: nested elements get added with .child(...) or .children(...) and then attributes are builder-style modifying the element after the fact


Last updated: Jul 23 2026 at 13:15 UTC