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:
app platform importKai.Config typeconfig : Kai.Config typingAre 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:
if system == linux then install <pkg>) that I wouldn't get in e.g. TOML.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?
If im not mistaken this should still work without the import statement and the annotation.
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:
@Richard Feldman -- this should work without the import and annotation right? we can see the types from the platform?
@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)
because the app [config] connects it to the platform
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.
I think keeping the app header is a good idea
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
I actually just now got a caddy-like plugin system working! Writeup incoming soon...
And yeah I do need the app header still haha.
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.
@blu this is cool! So you read the .roc file at runtime from your program directly?
or do you invoke the roc cli separately
the latter, he runs kai script.roc
well kai shell which delegates to roc I think
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>
}
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"
],
}),
]
ah I see, so xkai is calling roc to produce the kai binary?
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)
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.
super cool! :smiley:
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:
My whole ambition is essentially to replace at least the UX of Nix :D
cooooool
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
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
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
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!
Re-the caddy-inspired plugin system for kai:
https://blu.cx/posts/blog/2026-08-04-kai-devlog-2-modularity/
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