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:
add_int_attr, set_title, add_node, add_edges, and set_bool_attr? But I feel like it would be less readable and not much clearer. Wdyt?Dict.empty(), so should I use Graph.empty() and Attr.empty() instead of Graph.new and Attr.new?edge([], ...) would do nothing at all, and edge(["a"]) would be equivalent to edge(["a", "a"]), creating a self-loop attached to node a. Perhaps both these cases should return errors instead?+ or - in the DSL, but I resisted because it does not feel like The Roc Way™. Plus it complicates chaining.title, label, or color, are standard in DOT, so I've provided dedicated functions. It makes the DSL look cleaner, and it also allows compile-time checks (e.g., it guarantees that we can only provide valid colors).I'd love to hear your thoughts on this. In particular:
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
Maybe there is an API here where a record builder fits nicely?
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)
This one feels clearer and more idiomatic without the attribute chaining. Perhaps insert_node and insert_edge would also be more idiomatic.
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.
You could write a DOT parser and comptime parse that .. I don't know if that is more ergonomic though
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.
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.
Oh interesting!
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
Not saying its a good one
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())
Very cool, thanks Luke!
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
\\}
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.
huh, it never occurred to me until now, but you could do the same trick for like a JSON value
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