Stream: beginners

Topic: Designing DSLs


view this post on Zulip blu (Jul 31 2026 at 18:23):

In my embedded Roc DSL project, config is pretty darn minimal! But I'm wondering, can we do even better? :D

And what have other Roc DSL designers done? To me this is one of the biggest selling points of the language, that it is a full-fledged lang. while being optimized for spawning scoped languages.

Check out this code, which puts you in a shell containing program pokemonsay when you run kai shell:

app [config] {
    kai: platform "../../platform/config.roc",
}

import kai.Kai

config : Kai.Config
config = {
    name: "pokemonsay shell",
    shell: {
        pkgs: ["pokemonsay"],
    },
}

Now let's say someone is trying to just learn my DSL without needing to know Roc. There are three roc-isms here:

  1. app platform import
  2. import Kai.Config type
  3. config : Kai.Config typing

Are these always necessary? Part of the point of a DSL is that should only have to learn the things needed for the domain you're operating the DSL in, and all the rest there is unnecessary as far as an end user is concerned. Compare caddy's Caddyfile, for example:

example.com {
    reverse_proxy localhost:5000
}

Nothing except the actual thing you're trying to do! This is wonderful ergonomics and I think a huge reason for Caddy's success. I run servers with these tiny configs and saved many a headache.

Now the reason I chose to do this in Roc is because I wanted:

  1. Logic capabilities (e.g. if system == linux then install <pkg>) that I wouldn't get in e.g. TOML.
  2. Inherit all the benefits of upstream Roc work as opposed to inventing and maintaining my own language (Linter/format/compile/LSP etc.).
  3. Functional determinism

But the downside is that it is also constrained within the semantics of the Roc language, so maybe the above concerns are just intractable from first principles given the nature of Roc, and I should just accept that the above is as simple as it could possibly get?

view this post on Zulip Luke Boswell (Jul 31 2026 at 20:42):

If im not mistaken this should still work without the import statement and the annotation.

view this post on Zulip blu (Aug 02 2026 at 01:47):

I'm pretty sure I tried this and it didn't work, but investigating this further in prep for another release and will report back more concretely soon :salute:

view this post on Zulip Luke Boswell (Aug 02 2026 at 01:50):

@Richard Feldman -- this should work without the import and annotation right? we can see the types from the platform?

view this post on Zulip Richard Feldman (Aug 02 2026 at 02:07):

@Luke Boswell yeah you definitely shouldn't need either of those, and you should still get the same type safety (and error messages, in theory)

view this post on Zulip Richard Feldman (Aug 02 2026 at 02:07):

because the app [config] connects it to the platform

view this post on Zulip blu (Aug 02 2026 at 02:25):

Oh sorry I misunderstood, yes it worked for me without the annotation and import, but I was also asking about the app [config] platform specifier. It's a piece of roc-specific code that is (probably) essential to keep in there. I'm in this weird spot of wanting to give users the power and tooling of roc while requiring as little boilerplate unrelated to what they're trying to do as possible.

One solution suggested by codex was to write in the roc config "around it" in the cli and then have the CLI invoke roc on that constructed file. So the user writes:

config = {
...
}

But then the CLI turns it into:

app [config] {
...
}

config = {
...
}

and then it invokes roc build on that. But then the DSL isn't really roc anymore, which defeats the purpose. It sounds like including the platform header is a lower bound on how minimal I can make the config, and with the type inference I'm still pretty happy with this. Just wanted to see what limits I could push while retaining the benefits.

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

I think keeping the app header is a good idea

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

Another alternative is bundling the compiler into kai and making these "plugins" instead (similar to glue) -- but I still think you would need the app header.

Unless we somehow extended Roc's headerless design to support this use-case

view this post on Zulip blu (Aug 02 2026 at 02:30):

I actually just now got a caddy-like plugin system working! Writeup incoming soon...

view this post on Zulip blu (Aug 02 2026 at 02:30):

And yeah I do need the app header still haha.

view this post on Zulip blu (Aug 02 2026 at 02:32):

Unless we somehow extended Roc's headerless design to support this use-case

Yeah I would love this in theory, just not sure how this could actually work, since for roc to know the custom DSL language it would have to reference the platform somehow.

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

@blu this is cool! So you read the .roc file at runtime from your program directly?

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

or do you invoke the roc cli separately

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

the latter, he runs kai script.roc

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

well kai shell which delegates to roc I think

view this post on Zulip blu (Aug 02 2026 at 02:46):

I compile it separately so no runtime uncertainty! I was reading about caddy and they basically have a program called xcaddy which they use to compile their caddy binary with whatever plugins they want given a scoped schema of capabilities. It's really a beautiful design! So I copied their setup with my own xkai binary which means I can do this:

xkai build -> reads a plugin.roc file (StdPlugin.roc being the "standard library")

This produces a binary called kai. This binary has the command set as defined in StdPlugin.roc. But StdPlugin.roc also defines what's exposed via the platform in kai.roc!

So if StdPlugin.roc has a definition for say, shell, then all users will get:

config = {
  shell: [ pkgs: "<pkg>" ]
}

but if someone writes their own Plugin.roc which exposes a command called deploy, then creates their kai binary with xkai, the platform which corresponds to it will be in sync with the CLI. So that kai binary will have a deploy command which reads it's instructions from this new kai.roc which will look like:

config = {
  shell: [ pkgs: "<pkg>"]
  deploy: <deploy config>
}

view this post on Zulip blu (Aug 02 2026 at 02:50):

I literally just got this working so I've got a lot of kinks to iron out and may be explaining poorly but I'll try to have something demoable by tomorrow.

But for example here's my actual kai.roc that when I run kai shell, it puts me into a shell with the program cowsay:

app [config] {
    kai: platform "./platform/main.roc",
    std: "./plugins/main.roc",
}

import std.StdPlugin

config = [
    StdPlugin.shell({
        pkgs: [

      "cowsay"
        ],
    }),
]

view this post on Zulip Richard Feldman (Aug 02 2026 at 02:50):

ah I see, so xkai is calling roc to produce the kai binary?

view this post on Zulip blu (Aug 02 2026 at 02:54):

Yup exactly! So we can create many different kai binaries with different behaviors, built for use with different backends (e.g. nix or guix), different deployment actions (e.g. DigitalOcean, or generic, etc)

view this post on Zulip blu (Aug 02 2026 at 02:55):

I want to just get 90% of the features people would want out of the box, caddy style, but since there's so many decision points I figured it was better to allow an ecosystem approach where anyone can modify it. The modularity aspect seems especially important these days for making something lasting.

view this post on Zulip Richard Feldman (Aug 02 2026 at 02:57):

super cool! :smiley:

view this post on Zulip Richard Feldman (Aug 02 2026 at 02:58):

do you have any ambitions to make it so that you can use roc to specify your nix dependencies instead of .nix files? I don't even know if that's possible, but it sounds appealing to me in the abstract if it were possible :smile:

view this post on Zulip blu (Aug 02 2026 at 02:59):

My whole ambition is essentially to replace at least the UX of Nix :D

view this post on Zulip Richard Feldman (Aug 02 2026 at 03:00):

cooooool

view this post on Zulip Richard Feldman (Aug 02 2026 at 03:00):

yeah I was just watching the most recent Software Should Work talk, which mentioned that apparently at large enough scale the fact that Nix's interpreter is slow has been a pain point

view this post on Zulip blu (Aug 02 2026 at 03:00):

But you quickly realize when looking into this that many of Nix's problems are not just one thing. It's everything: the language, the monolithic CLI, NixOS, flakes, documentation. My view is Nix is one of those incredible ideas/foundations implemented very poorly

view this post on Zulip blu (Aug 02 2026 at 03:02):

yeah I was just watching the most recent Software Should Work talk, which mentioned that apparently at large enough scale the fact that Nix's interpreter is slow has been a pain point

It is, there's a project that actually set out to solve just this exact problem, but they realized the code was so monolithic that it would be easier for them to create their own, modular implementation of nix! haha

https://tvl.fyi/blog/rewriting-nix

view this post on Zulip blu (Aug 02 2026 at 03:05):

My thought is: Nix is the largest package manager in the world and it has proven the value of reproducible systems -- if you can figure out how to use it which is a big if (even with AI!). Therefore a solution which allows: A) backward compat with Nix ecosystem that B) makes it way easier for average end-users that C) is modular enough that people can come in and rewrite poorly functioning/designed/slow pieces of it while retaining that backward-compat the whole time would be very valuable.

I essentially trying to think what do we have to do to get the avg. computer user to get all these awesome benefits of reproducible/determinate systems, because they truly are enormous if done right!

view this post on Zulip blu (Aug 04 2026 at 06:53):

Re-the caddy-inspired plugin system for kai:

https://blu.cx/posts/blog/2026-08-04-kai-devlog-2-modularity/

view this post on Zulip blu (Aug 11 2026 at 03:13):

Went a new direction with this and decided to do a custom DSL. There are several reasons outlined here, but the biggest are max simplicity being a core goal, the ability to remove roc compiler as a dependency and shrink the resulting binary to the KB range, and the realization that the actual logic needed by a system like this is minimal. Three lines and you have a devshell now:

shell {
    pkgs: ["cowsay", "fortune"]
}

Last updated: Aug 12 2026 at 12:35 UTC