Using lens to set a value based on another
I started writing a small tool for work that consumes YAML files and combines
the data into a single YAML file. To be specific it consumes YAML files
containing snippets of service specification for Docker Compose and it produces
a YAML file for use with docker-compose
. Besides being useful to me, I thought
it'd also be a good way to get some experience with lens.
The first transformation I wanted to write was one that puts in the correct image name. So, only slightly simplified, it is transforming
panda: x-image: panda goat: x-image: goat tapir: image: incorrent x-image: tapir
into
panda: image: panda:latest x-image: panda goat: image: goat:latest x-image: goat tapir: image: tapir:latest x-image: tapir
That is, it creates a new key/value pair in each object based on the value of
x-image
in the same object.
First approach
The first approach I came up with was to traverse the sub-objects and apply a
function that adds the image
key.
setImage :: Value -> Value setImage y = y & members %~ setImg where setImg o = o & _Object . at "image" ?~ String (o ^. key "x-image" . _String <> ":latest")
It did make me wonder if this kind of problem, setting a value based on another value, isn't so common that there's a nicer solution to it. Perhaps coded up in a combinator that isn't mentioned in Optics By Example (or mabye I've forgot it was mentioned). That lead me to ask around a bit, which leads to approach two.
Second approach
Arguably there isn't much difference, it's still traversing the sub-objects and
applying a function. The function makes use of view
being run in a monad and
ASetter
being defined with Identity
(a monad).
setImage' :: Value -> Value setImage' y = y & members . _Object %~ (set (at "image") . (_Just . _String %~ (<> ":latest")) =<< view (at "x-image"))
I haven't made up my mind on whether I like this better than the first. It's disappointingly similar to the first one.
Third approach
Then I it might be nice to split the fetching of x-image
values from the
addition of image
key/value pairs. By extracting with an index it's possible
to keep track of what sub-object each x-image
value comes from. Then two steps
can be combined using foldl
.
setImage'' :: Value -> Value setImage'' y = foldl setOne y vals where vals = y ^@.. members <. key "x-image" . _String setOne y' (objKey, value) = y' & key objKey . _Object . at "image" ?~ String (value <> ":latest")
I'm not convinced though. I guess I'm still holding out for a brilliant combinator that fits my problem perfectly.
Please point me to "the perfect solution" if you have one, or if you just have some general tips on optics that would make my code clearer, or shorter, or more elegant, or maybe just more lens-y.