Set'em: Elm record setter generator
"Getters just get, setters just set!"
setem
generates all possible record setters from your Elm source files into a single module.
- Reasonably new
nodejs
npm install --save-dev setem
# OR
# yarn add --dev setem
# Generates for an Elm project (including dependencies)
npm run setem --output src/ # For Elm project cwd. "elm.json" file must exist
npm run setem --output src/ --elm-json sub_project/elm.json # For non-cwd Elm project, targeted by "elm.json" file path
# Only generates from specific files (NOT including dependencies)
npm run setem --output src/ src/Main.elm # From a single source file
npm run setem --output src/ src/Main.elm src/Data/User.elm # From multiple source files
npm run setem --output src/ src/**/*.elm # From multiple source files resolved by glob in your shell
npm run setem --output src/ src/ # From multiple source files in a specific directory, expanded recursively
(npx
or global install also work)
It generates src/RecordSetter.elm
file like this:
-- This module is generated by `setem` command. DO NOT edit manually!
module RecordSetter exposing (..)
s_f1 : a -> { b | f1 : a } -> { b | f1 : a }
s_f1 value__ record__ =
{ record__ | f1 = value__ }
s_f2 : a -> { b | f2 : a } -> { b | f2 : a }
s_f2 value__ record__ =
{ record__ | f2 = value__ }
...
All s_<field name>
setter function works for any records with matching field names!
As you all Elm developers have probably known, Elm innately provides getters for any record fields:
$ elm repl
---- Elm 0.19.1 ----------------------------------------------------------------
Say :help for help and :exit to exit! More at <https://elm-lang.org/0.19.1/repl>
--------------------------------------------------------------------------------
> .name
<function> : { b | name : a } -> a
These getters are concise, pipeline-friendly (i.e. you can x |> doSomething |> .name |> doElse
), therefore composition-friendly (there are packages with "lift"-ing functions that work in tandem with getters, such as elm-form-decoder or elm-monocle).
On the other hand, it does not provide setters of the same characteristics. Standard record updating syntax is:
{ record | name = "new name" }
...which is,
-
Not pipeline-friendly. You have to combine it with anonymous function like:
x |> doSomething |> (\v -> { record | name = v }) |> doElse
-
Not nest-friendly. You have to combine either
let in
or pattern matches:let innerRecord = record.inner in { record | inner = { innerRecord | name = doSomething innerRecord.name } }
Now, as discussed many many times in the community, it is somewhat deliberate choice in the language design, to NOT provide "record setter/updater" syntax.
- It encourages to create nicely named top-level functions rather than relying on verbose anonymous functions.
- Also it encourages to design flatter, simpler data structures (and possibly using custom types) to more precisely illustrate our requirements.
- For unavoidable situations where setters are in strong demand, we can create "data" module with necessary setters exposed, which actually leads us to think about proper boundary of data and concerns. Never a bad thing!
But. A big BUT.
In our everyday programming we yearn for setters time to time.
- When we work with foreign record data structures from, say, packages
- When we have tons of records to work with, from code generation facilities such as elm-graphql
- When it is more natural to work with nested records as-is. For example when we use external/existing JSON APIs.
- Simply when we do not have much time.
- Requirement of writing many setters in order to leverage composition-centric logics, is tedious.
- It is a discouragement for us before writing proper data modules, when we forsee many boilerplate works. Even if it is one time thing.
For that, setem
is born.
As the slogan says, "setters just set!"
setem
just generates setters, and setters only- It is up to you how you utilize those setters.
For instance, use
setem
-generated setters as building blocks for Monocle definitions!
- It is up to you how you utilize those setters.
For instance, use
- It does not provide "updaters" in the sense of
(a -> a) -> { b | name : a } -> { b | name : a }
- It might prove useful, though my current intuition says it sees less usages than setters.
- A single importable module. Just
import RecordSetter exposing(..)
in your code and that's it! All setters are always available.
With generated setters it is possible to:
-
Pipeline-d set:
x |> doSomething |> s_name v |> s_anotherField (updater x.anotherField) |> doElse
-
Nested set:
{ record | inner = record.inner |> s_name (doSomething record.inner.name) |> s_anotherField v , anotherInner = record.anotherInner |> s_number 1 |> s_moreNest (record.anotherInner.moreNest |> s_yetAnotherField vv |> s_howFarCanWeGet [ 2, 3, 4 ] ) }
Setters are ordinary functions. Pass them to any high-order functions as needed!
A. Up to you. Personally I do. If you are irritated by cluttered diffs every time you generated setem
,
create a .gitattributes
file with a following entry:
src/RecordSetter.elm linguist-generated=true
Paths set as linguist-generated=true
are collapsed by default on GitHub pull request diffs, reducing visual clutter.
See https://github.com/github/linguist/blob/master/docs/overrides.md#generated-code
It is based on tree-sitter-elm. Quite fast!
The generator looks for both record type definitions and record data expressions from your source files.
It generates setters of not yet used fields (or even, ones you are not going to use at all.)
Unused ones are expected to be sorted out by Dead-Code Elimination feature of elm make --optimize
.
If you do not give explicit paths
as command line arguments, setem
reads your elm.json
file
and generates setters not only from your "source-directories"
but from your dependencies as well.
In this scenario, tokens from your dependencies are cached in your elm-stuff/setem/
directory.
Install reasonably new Bun. If you are using mise
(successor of asdf
, rtx
), do the following:
git clone git@github.com:ymtszw/setem.git
cd setem/
git submodule update --init --recursive
mise install # See .tool-versions for reference versions
bun run lint
bun run test # `bun test` still not supported; we use jest under the hood
bun run test:cli
In GitHub Actions, sanity checks are performed against multiple runtimes such as recent LTS Node.js versions and Bun!
Or, you can still use Node.js
mise install
npm run lint
npm run test
npm run test:cli
MIT License (c) Yu Matsuzawa
Give a ⭐️ if this project helped you!
This README was generated with ❤️ by readme-md-generator