setem
v0.6.1
Published
Set'em: Elm record setter generator
Downloads
168
Readme
🔸 setem 🔸
Set'em: Elm record setter generator
What is this?
"Getters just get, setters just set!"
setem
generates all possible record setters from your Elm source files into a single module.
Prerequisites
- Reasonably new
nodejs
Install
npm install --save-dev setem
# OR
# yarn add --dev setem
Usage
# 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!
But Why?
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.
What you get
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!
Tips
Q. Should we add setem-generated file to git?
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
Implementation note
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.
Development
Install reasonably new Node.js. If you are using mise
,
git clone [email protected]:ymtszw/setem.git
cd setem/
git submodule update --init --recursive
mise install
npm install
npm run test
npm run test:cli
In GitHub Actions, sanity checks are performed against recent LTS Node.js versions (14,16,18)
Author & License
MIT License (c) Yu Matsuzawa
Show your support
Give a ⭐️ if this project helped you!
This README was generated with ❤️ by readme-md-generator