You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The goal would be to have a system where multiple components can be bundled together to denote common behavior. For example, it might be useful to group a web server with a database, or a web server with a bunch of default middleware or interceptors.
I've experimented with something like this in Duct, which builds on Integrant. This proposed system would be far more constrained than that of Duct. In Duct, the configuration can be modified by a chain of pure functions. In this system, only merging is permitted.
This follows on from the introduction of the prep-key multimethod, which had similar goals. The functionality of expand-key would be a superset of that of prep-key, and therefore prep-key can be deprecated.
Usage
First we define an expansion for a keyword. I'll use the term "module" to denote keywords with expansions:
What does this expand to? The proposed resolution mechanism is to attempt to merge values, giving precedence to the default expansion where possible, and raising an exception otherwise.
The port defined the example server takes priority over the default in the example module. This should hopefully feel intuitive, and allows a user to always override expanded values.
In the case of two modules there's no obvious precedence as to what the server port should be set to. Therefore two module with conflicting keys will generate an exception:
To make this work, there are two potentially breaking changes:
The existing integrant.core/expand function will be changed in purpose
The value of a top level key must be a map to be merged
I don't think anyone actually uses integrant.core/expand, so we should be okay to repurpose that, and in the worst case, use a different name.
The second point is more problematic, as while the majority of keys expect maps, I'm sure not all of them do, particularly in the case of constants:
{:example/server-name"alpha"}
We could change this, and set all values to be maps:
{:example/server-name {:value"alpha"}}
Or when expanding, we could have maps be a special case, retaining backward compatibility and existing functionality at the cost of making things a little more complex.
I think it's important to merge values that are maps, otherwise much of the benefits of this system is lost.
The text was updated successfully, but these errors were encountered:
Thanks for the feedback. The way I have it working at the moment is that maps are deep merged, and everything else is replaced. This ensures non-map values still work. Any conflicts between keys in the map will raise an error, unless one of the values has the ^:override metadata.
Goal
The goal would be to have a system where multiple components can be bundled together to denote common behavior. For example, it might be useful to group a web server with a database, or a web server with a bunch of default middleware or interceptors.
I've experimented with something like this in Duct, which builds on Integrant. This proposed system would be far more constrained than that of Duct. In Duct, the configuration can be modified by a chain of pure functions. In this system, only merging is permitted.
This follows on from the introduction of the
prep-key
multimethod, which had similar goals. The functionality ofexpand-key
would be a superset of that ofprep-key
, and thereforeprep-key
can be deprecated.Usage
First we define an expansion for a keyword. I'll use the term "module" to denote keywords with expansions:
When this keyword is added to a configuration, the return value will be merged with the configuration:
The default expansion is:
Therefore we can mix keywords with user-defined expansions, along with those without:
Handling conflicts
There is an obvious concern with this approach: what happens when two expansions have conflicting keys? For example:
What does this expand to? The proposed resolution mechanism is to attempt to merge values, giving precedence to the default expansion where possible, and raising an exception otherwise.
More formally, given two conflicting expansions:
The values, v1 and v2 are assumed to be maps. If v1 and v2 share no keys in common, the output is:
If the share keys, but k is equal to k1, then v1 takes priority:
Similarly, the mirror is true. If k is equal to k2, then v2 takes priority:
If they have keys in common, and neither k1 nor k2 is equal to k, then an error is raised. So in the above case:
The port defined the example server takes priority over the default in the example module. This should hopefully feel intuitive, and allows a user to always override expanded values.
Suppose instead we had a second module:
In the case of two modules there's no obvious precedence as to what the server port should be set to. Therefore two module with conflicting keys will generate an exception:
This can be resolved by setting the server port explicitly:
Problematic changes
To make this work, there are two potentially breaking changes:
integrant.core/expand
function will be changed in purposeI don't think anyone actually uses
integrant.core/expand
, so we should be okay to repurpose that, and in the worst case, use a different name.The second point is more problematic, as while the majority of keys expect maps, I'm sure not all of them do, particularly in the case of constants:
We could change this, and set all values to be maps:
Or when expanding, we could have maps be a special case, retaining backward compatibility and existing functionality at the cost of making things a little more complex.
I think it's important to merge values that are maps, otherwise much of the benefits of this system is lost.
The text was updated successfully, but these errors were encountered: