Skip to content

Commit 4ba21c8

Browse files
Add a dev doc for option modifiers
1 parent 93163b9 commit 4ba21c8

1 file changed

Lines changed: 112 additions & 0 deletions

File tree

docs/Developer/option-modifiers.md

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
# Option Modifiers
2+
3+
Instead of exposing internal record structures, our best practice is
4+
to use **Option Modifiers**: functions of type `Config -> Config`
5+
(endomorphisms) that are composed to build a final configuration.
6+
7+
## Advantages
8+
9+
* **Clean Composition:** Use the standard `.` operator to chain multiple
10+
options together seamlessly.
11+
* **Encapsulation:** The internal record structure and field names
12+
remain hidden, preventing breaking changes if the implementation shifts.
13+
* **Order Independence:** Unlike positional arguments, the order of
14+
modifiers in a chain does not affect the final result (unless a specific
15+
option is overridden).
16+
* **Unified Interface:** Removes the need for record syntax or multiple
17+
constructor patterns. There is only one way to specify options: through
18+
functions.
19+
* **Bundling:** Common configurations can be pre-composed into a single
20+
named modifier (e.g., `withForce Force . recursive True`).
21+
* **Discipline:** By limiting the "surface area" of the API, we reduce
22+
the possibility of misuse or invalid state transitions.
23+
24+
## Disadvantages
25+
26+
* Deserialization is problematic unless the module provides a way to turn the
27+
composed functions to a record.
28+
* Enumeration of all options at one place is not possible.
29+
30+
To mitigate these the internal implementation using a record can be exposed, we
31+
can dump the composed options to a record or read options from a record.
32+
33+
## Modifiers and Setters
34+
35+
A combination of independent options usually represented by a record is
36+
represented by different functions, one for each record field.
37+
38+
```
39+
data RmOptions = RmOptions
40+
{ rmForce :: RmForce
41+
, rmRecursive :: Bool
42+
}
43+
44+
withForce :: RmForce -> RmOptions -> RmOptions
45+
recursive :: Bool -> RmOptions -> RmOptions
46+
```
47+
48+
When an option has a choice it is represented by constructors of a sum
49+
type so that only one state is representable. For example, here we can
50+
choose one of the three choices:
51+
52+
```
53+
data RmForce
54+
= NoForce
55+
| Force
56+
| FullForce
57+
58+
withForce :: RmForce -> RmOptions -> RmOptions
59+
```
60+
61+
## Naming
62+
63+
For toggling options like `recursive` we can name them like attributes which
64+
can be true or false. For example, `recursive True`.
65+
66+
For options that have multiple choices we prefer the `with` prefix e.g.
67+
`withForce`. Another alternative is `set` prefix but `with` is usually
68+
clearer. Unlike `set`, which implies an imperative "action" or a binary
69+
toggle, `with` conveys a functional transformation. It suggests that
70+
the resulting operation will be performed *with* a specific property or
71+
value, regardless of the previous state.
72+
73+
## Expressing All Possible Values
74+
75+
A modifier must be **total**. It should not merely "toggle" a default;
76+
it must allow the user to explicitly define the desired state.
77+
78+
If a user receives a pre-composed bundle of modifiers, they may not know
79+
the current state of a specific option. To ensure predictable behavior,
80+
the modifier must allow them to force a value (e.g., `withVerbose True`
81+
or `withVerbose False`), ensuring the final config matches their intent
82+
regardless of the input chain.
83+
84+
For example, if default is not-recursive we may be tempted to use
85+
`recursive` without an argument, but it is better to use `recursive`
86+
with a Bool argument so we can say `recursive True` or `recursive
87+
False`. This gives us the ability to set recursive to any value we want.
88+
89+
## Reset to Default (Optional)
90+
91+
While the base configuration starts with library defaults, we can
92+
optionally provide `resetOption` functions (e.g., `resetRecursive`).
93+
94+
These are useful when you want to "neutralize" an option within a
95+
specific composition. For example, if you have a `standardConfig` bundle
96+
that includes recursion, but for one specific call you want to ensure it
97+
is disabled, you can simply append `. resetRecursive` to the chain.
98+
99+
## Backing Record (Optional)
100+
101+
The internal implementation invariably uses a record for all options which is
102+
passed around. If serialization of options is needed we can expose the record
103+
and associated types such that we can dump the composed functions to the record
104+
or build a options function from a record. This will provide a way to serialize
105+
the options if needed.
106+
107+
## Summary
108+
109+
By using composed endomorphisms, we provide a declarative DSL for
110+
filesystem operations. This approach balances the flexibility of a
111+
record with the safety and elegance of functional composition, covering
112+
all use cases while maintaining a strict, predictable API.

0 commit comments

Comments
 (0)