Stream: API design

Topic: Designing a DSL in Roc


view this post on Zulip Aurélien Geron (Aug 19 2026 at 23:19):

Some languages such as Ruby are particularly good at DSLs (Domain-Specific Languages). For example, suppose I want to design a DSL to build DOT graphs such as this one:

graph {
    [foo=1];
    [title="Testing Attrs"];
    a [color=green, label="Alpha Node"];
    b [label="Beta!"];
    b -- c;
    a -- b -- d [color="#977de3", label="Main Path"];
    [bar=true];
}

In Ruby, it wouldn't be too hard to implement a DSL that would look like this:

my_graph = graph do
  attr foo: 1
  attr title: "Testing Attrs"
  node :a, color: :green, label: "Alpha Node"
  node :b, label: "Beta!"
  edge :b, :c
  edge :a, :b, :d, color: "#977de3", label: "Main Path"
  attr bar: true
end

Here's my first take in Roc:

my_graph = Graph.new
    .int_attr("foo", 1)
    .title("Testing Attrs")
    .node("a", Attr.new.color(Green).label("Alpha Node"))
    .node("b", Attr.new.label("Beta!"))
    .edge(["b", "c"], Attr.new)
    .edge(["a", "b", "d"], Attr.new.color(RGB(0x97, 0x7d, 0xe3)).label("Main Path"))
    .bool_attr("bar", True)

Notes:

I'd love to hear your thoughts on this. In particular:

  1. Can you think of ways to improve this DSL?
  2. What do you think I should recommend to users in the exercise instructions (e.g., keep it simple and clear; avoid overloading operators; ...).

view this post on Zulip Luke Boswell (Aug 19 2026 at 23:30):

Maybe you could use from_quote and from_interpolation to reduce some boilerplate? https://github.com/roc-lang/roc/blob/main/docs/langref/static-dispatch.md#literal-conversion

view this post on Zulip Luke Boswell (Aug 19 2026 at 23:31):

Maybe there is an API here where a record builder fits nicely?

view this post on Zulip Aurélien Geron (Aug 19 2026 at 23:32):

Yeah, I was wondering about record builders here, but I'm not sure what it would look like. I'll look into from_quote and from_interpolation, thanks.

Here's another design, using lists of unions, and the more idiomatic with:

my_graph =
    Graph.empty()
    .with_int_attr("foo", 1)
    .with_title("Testing Attrs")
    .with_node("a", [Color(Green), Label("Alpha Node")])
    .with_node("b", [Label("Beta!")])
    .with_edge(["b", "c"], [])
    .with_edge(["a", "b", "d"], [Color(RGB(0x97, 0x7d, 0xe3)), Label("Main Path")])
    .with_bool_attr("bar", True)

view this post on Zulip Aurélien Geron (Aug 19 2026 at 23:35):

This one feels clearer and more idiomatic without the attribute chaining. Perhaps insert_node and insert_edge would also be more idiomatic.

view this post on Zulip Aurélien Geron (Aug 20 2026 at 00:34):

I've looked into from_quote and from_interpolation. I think I understand them, but I'm not quite sure how I would use them in this DSL, since any part could come from a runtime argument, unknown at compilation time.

view this post on Zulip Luke Boswell (Aug 20 2026 at 01:42):

You could write a DOT parser and comptime parse that .. I don't know if that is more ergonomic though

view this post on Zulip Aurélien Geron (Aug 20 2026 at 01:43):

I think the goal is to define a DSL that could be used dynamically, not at compilation time. For example, the program might read names in a social network and build a dot graph using the DSL.

view this post on Zulip Luke Boswell (Aug 20 2026 at 01:46):

I have a feeling you could combine a template and a parser with record builders to do something like that -- and get the best of both worlds... I would need to experiment with it.

view this post on Zulip Aurélien Geron (Aug 20 2026 at 01:47):

Oh interesting!

view this post on Zulip Luke Boswell (Aug 20 2026 at 02:09):

Here's an idea

$ roc examples/dot_dsl.roc
graph {
    foo=1;
    title="Testing Attrs";
    a [color=green, label="Alpha Node"];
    b [label="Beta!"];
    b -- c;
    a -- b -- d [color="#977de3", label="Main Path"];
    bar=true;
}
digraph {
    ann [label="Ann \"the closer\" Lee"];
    bob [label="Bob"];
    cid [label="Cid"];
    ann -> bob [color=blue];
    bob -> cid [color=blue];
    cid -> ann [color=blue];
    subgraph cluster_legend { legend [label="who follows whom"]; }
}

https://gist.github.com/lukewilliamboswell/27082dbb4a81687d36f579ccf4cbcc72

view this post on Zulip Luke Boswell (Aug 20 2026 at 02:11):

Not saying its a good one

view this post on Zulip Luke Boswell (Aug 20 2026 at 02:29):

Ok I edited that with a much better implementation (performance wise) so now it looks like

# The graph from the exercise, statement for statement.
exercise : List(Dot.Stmt)
exercise = [
    Set("foo", 1),
    Set("title", "Testing Attrs"),
    Node({ id: "a", color: Green, label: "Alpha Node" }),
    Node({ id: "b", label: "Beta!" }),
    Edge({ from: "b", to: "c" }),
    Edge({ from: "a", to: "d", via: ["b"], color: "#977de3", label: "Main Path" }),
    Set("bar", True),
]

echo!(Dot.graph(exercise).render())

view this post on Zulip Aurélien Geron (Aug 20 2026 at 02:29):

Very cool, thanks Luke!

view this post on Zulip Aurélien Geron (Aug 20 2026 at 02:37):

Oh and it supports compile-time verification as well, I love it. The following does not compile because there's a missing closing bracket in the dot code. This is great!

    dot : Dot
    dot =
        \\graph {
        \\    a b [color=blue
        \\}

view this post on Zulip Aurélien Geron (Aug 20 2026 at 02:56):

I didn't know it was even possible to get this to work:

exercise = [
        Set("foo", 1),
        Set("title", "Testing Attrs"),
        ...
        Set("bar", True),
]

IIUC, it works because exercise has the type List(Dot.Stmt), Dot.Stmt is a union type that contains Set(Str, Value), and Value is a union type containing Int(I64), Text(Str), and Bool, so the compiler is able to call Value.from_numeral in the first case, Value.from_quote in the second case, and nothing in the third case. Wow.

view this post on Zulip Richard Feldman (Aug 20 2026 at 03:34):

huh, it never occurred to me until now, but you could do the same trick for like a JSON value

view this post on Zulip Dan G Knutson (Aug 21 2026 at 02:52):

This makes me really excited about comp-time DSLs for things like a render graph or a game engine scene format. You could have something like Flecs script but type-checked against your own gameplay codebase. If you had a taskflow-like API for specifying a parallel compute graph in a DSL, you could do "we have a borrow checker at home" style validations on it. This stuff works for all-in-roc dsls just as much as for parsers.


Last updated: Sep 03 2026 at 15:16 UTC