clone

an offline sliding-block puzzle game in gleam and lustre, with an endless ladder of puzzles ranked by search

main
klot.ski / docs / style.md
style.md 5.0K source

Style

Conventions this repository actually follows, and the reasons, so that a change that breaks one is a decision rather than an accident.

The formatter is the authority

gleam format --check src test passes and is meant to keep passing. Nothing here is exempt and nothing is annotated to be skipped, because gleam format has no configuration and no escape hatch — there is no pragma that makes it leave a region alone. A file that argues with it loses on whichever day someone runs it.

So the code is written to want what the formatter does. Where the two would disagree, the shape of the code changes rather than the rule. Three of its behaviours are worth knowing before writing anything it will touch, because each one has already decided how something here is written.

It reflows a list of scalars to fill the width. A list of integers written as a grid comes back as a paragraph. It does not reflow a list whose elements are themselves lists — those stay one to a line, however short they are. There is no magic trailing comma; a comma at the end changes nothing, and a comment inside the list pins nothing but itself.

It strips trailing zeros from floats. 0.40 becomes 0.4. A column of weights cannot be aligned on the decimal point and should not be attempted; see score in difficulty.gleam, where the four coefficients are simply written out and the comment carries what the alignment would have shown.

A nested call in the last argument explodes every argument. A constructor whose final argument is a bare list literal keeps its other arguments hugged on one line and hangs the list beneath it. Wrap that same list in a function call and all seven arguments go one per line. Data tables are therefore written with bare literals and any transformation happens at the reading end, not the writing end.

Data that has a shape keeps it

A board is a picture and is written as one. In families.gleam a packing is a list of rows, not a flat list of cells, and every cell is a one-letter constant:

Family("classic", "CLASSIC", 4, 5, 1, 3, [
  [u, g, g, u],
  [u, g, g, u],
  [u, r, r, u],
  [u, s, s, u],
  [s, e, e, s],
]),

e empty, s single, u upright, r recumbent, g goal. One letter rather than a word because every cell has to be the same width or the rows stop lining up, and a packing that does not line up is a list of numbers again. The values are the shape codes domain encodes with, so this is a spelling of the wire format and not a private one.

The nesting is what survives the formatter, and families.grid flattens at the point of use. Flatten one of these back out to save the list.flatten and the next gleam format turns the board into a paragraph. That is the general rule: when a literal's layout carries meaning, give it a structure the formatter already keeps, rather than a comment asking to be spared.

Comments say why

The code says what it does. A comment that repeats it is noise. What a comment is for is the reasoning that cannot survive in the code: what was tried, what broke, what the alternative was and why it lost.

Written declaratively and without hedging. Not "we probably want to avoid this because it might cause issues" but "activation waits for every page running the previous version to go, so nothing is still reading what is being thrown away." A reader should be able to tell a measured fact from a judgement, and neither should be apologised for.

A decision that has already gone wrong once gets the incident, briefly, because that is the part nobody reconstructs: the version was a number kept by hand and by the tenth deploy it had drifted. Numbers earn their place this way — prefer the number and what it counts to an adjective.

Gleam decides, FFI executes

Every _ffi.mjs file is named for the one platform gap it fills — 32-bit words, font metrics, rounding, the pixel pipeline, the platform — and none of them knows what a theme, a family or a rung is. Logic that could live in Gleam does. An FFI file that starts making a decision is a sign the boundary moved and should be moved back.

Names carry digests

Everything under /assets/ is fingerprinted, so a URL never holds different bytes and two builds cannot contend for one address. Four things keep their names on purpose and the reasons are in tools/bundle.mjs. Anything derived — a cache version, an asset name — is computed from content rather than incremented by hand, because a number kept by a person is a number that eventually disagrees with what it names.

Commit messages

A declarative one-line subject that states the idea, not the diff:

The shell was written once, and once was not enough

not fix(sw): re-cache index.html on navigate. The body is prose, wrapped, and explains what was wrong, why it was wrong, and why the fix takes the shape it does — the same job the comments do, at the scale of a change. Present tense. No prefixes, no ticket numbers, no trailers.

Prose

The · middot is never used. Em dashes are, and so are numerals: a figure with a caption beats a sentence that describes the figure.