…also known as synergized orchestration of federated functional
paradigms via lattice-theoretic constraint-based asset dissemination
;-)
or “how I generate X YAML files with Cue to keep my sanity intact”.
I like modularity. When working on my project I made every diagram (or parts of it) into separate module. E.g. something like this:
Unfortunately - Haskell’s Stack gets confused and then (for example) testing
mod2 makes it rebuild whole application; slow and annoying.
There is known solution for this problem and it’s called multi-package
project. It removes main package.yaml file from the root and instead
it puts one in every single sub-directory (which is package in itself).
Tis’ all great and dandy but when it means I have 8 package.yaml 1 files to maintain
which is no longer that great. Plus, shifting between structures is
hard, isn’t it?
Thankfully, Cuelang excels at working with multiple files, so I
thought about it immediately (especially since I was already managing
my single package.yaml with it..).
Cue has this feature called “cue command” that makes it somewhat of a command runner. The cool thing about it is that it actually can write files in parallel which makes it great for this particular case.
It requires specific “tool” syntax, which is not very discoverable, but hopefully you can make some sense out of it after reading it:
// make_tool.cue
package main
import (
"tool/file"
"tool/exec"
)
// Command is called make
command: make: {
// packages are defined in other file
// but it has structure [name]: packageData
for k, v in packages {
// Inform that package is to processed
b = "\(k)-start": exec.Run & {
cmd: "echo \(k) START."
}
// Run export for specified package
// AFAIK there is no easier way to export
// to YAML than this
c = "\(k)-cnt": exec.Run & {
$after: b
cmd: "cue export --out yaml -e packages.\(k)"
stdout: string
}
// Create specific files
// Note that I'm using previous step (c) stdout as input
f = "\(k)-file": file.Create & {
filename: "packages/\(k)/package.yaml"
contents: c.stdout
}
// This is required after creating files because
// there might be need to recreate Cabal configuration file
//
// Could be done in Makefile but since we're at it already...
s = "\(k)-setup": exec.Run & {
$after: f
cmd: "stack setup"
dir: "packages/\(k)"
}
// Finally notify about processing end
"\(k)-done": exec.Run & {
$after: s
cmd: "echo \(k) DONE."
}
}
}
In case it’s your first encounter with Cuelang:
"\(k)-something" is a string interpolation, $after is weirdly named
attribute that has meaning in context of Cue commands and this “c
equals …” is assigning alias so I don’t have to break my keyboard
when referencing previous task.
As mentioned in the header comment, packages struct is in a separate file under same package name (main). It’s accessible to the tool like it’d be same file, but isolated code wise and thus (in my opinion) readability friendly.
Now for the meat, the definitions:
// package.cue
package main
import (
"list"
)
// Note: In Cue order doesn't matter, I've shuffled blocks for
// reading experience
// Most important part - it takes already defined packages, iterate
// over them and...
packages: {
for key, _ in packages {
"\(key)": {
// ...enriches with "default" struct
default
// ...and sets the default name
// (star-prefix means that this value is default in none given)
name: *"diagrams-\(key)" | string
}
}
}
// Default mixin, it doesn't allow overrides by default
default: {
version: "0.1.0.0"
license: "OtherLicense"
author: "Przemysław Alexander Kamiński"
maintainer: "[email protected]"
// Why write something twice if I can interpolate?
copyright: "2025 \(author)"
// Underscore-started vars are hidden, i.e. they won't be exported
_base_deps: [
"base >= 4.7 && < 5",
"text",
"bytestring >= 0.12",
// ... rest of the dependencies
]
// Most apps needs diagrams-internal anyway, but that's default
_extra_deps: *["diagrams-internal"] | _
// Final dependencies concats _base_deps (shared)
// and _extra_deps (per package)
dependencies: list.Concat([_base_deps, _extra_deps])
"ghc-options": [
"-Wall",
"-Wcompat",
// ... rest of GHC options
]
library: "source-dirs": ["src"]
executables: {}
verbatim: {}
}
// Another partial to use for building apps
execOpts: {
"source-dirs": "app"
"ghc-options": [
"-threaded",
"-rtsopts",
"-with-rtsopts=-N",
]
}
// Finally - manual definitions. In my file this is the first block.
// Not all packages are specified.
packages: {
// This is diagrams-internal, it cannot depend on itself
diagrams: {
name: "diagrams-internal"
_extra_deps: []
}
piano: {} // Defaults only
main: {}
// Timeline package has extra test specs"
timeline: {
tests: spec: {
main: "Spec.hs"
"source-dirs": "test"
"ghc-options": [
"-threaded",
"-rtsopts",
"-with-rtsopts=-N",
]
dependencies: [
diagrams.name,
"diagrams-timeline",
"QuickCheck",
"hspec",
]
}
}
// Webserver
web: {
// Needs diagrams
_extra_deps: [
"diagrams-internal",
"diagrams-piano",
"diagrams-timeline",
]
// Builds executable
executables: {
"server": execOpts & {
execOpts
main: "Webserver.main"
dependencies: [
diagrams.name, // it finds nearest "diagrams" to resolve,
// i.e. "diagrams-internal"
"diagrams-piano",
"diagrams-timeline",
"diagrams-web",
]
}
}
"extra-source-files": [
"README.md",
"CHANGELOG.md",
"assets/*.scss",
]
}
}
I didn’t type-check it because it was a quickie
and I’m the only person at the project. I’m un-fireable by structure
;) When this becomes multi-million-in-revenue project I’ll add types,
I promise.
For running this I also have Makefile (which is slightly more
convenient to use than Cue command which, as presented, is boilerplate
heavy). Make calls cue cmd make which - in turn - generates 8
package.yaml in sub-directories files with consistent naming and
dependencies.
Because it is Haskell, process was fairly painless: build, check
errors, fix dependencies, rinse and repeat. In fact it took around 30
minute to change between structures with 8 packages, 4 tests
executable, 3 executable executables). I believe it’d be approx. 3
hours if I had to make every single package.yaml file by hand.
Cue is hard, but I don’t know any other solution that would keep things AS consistent with so minimal effort 2.