View Source Information hiding
I start from two beliefs:
If you have complicated data, there's a reasonable chance your first attempt at structuring it will be wrong. Or adding new features will force restructuring.
This is as true for functional languages (with their CRUD emphasis) as it is for OO languages. (After all, the languages where ideas about modularity and information hiding took root are at least arguably closer to modern functional languages than to modern OO languages.)
You want the inevitable restructuring to affect client code as little as possible.
The fact that lenses are functions and functional programming loves it some function composition suggests that lenses make information hiding more idiomatic than
Access
(which requires concatenating lists).
My own practice (which is still evolving) is to give each struct their own lenses. Here's an example.
I use TypedStruct
to define
structs, because it's prettier than defstruct
. Here's an example.
defmodule Cluster do
typedstruct enforce: true do
field :name, atom
field :downstream, %{atom => MapSet.t(atom)}
end
...
It seems reasonable to have a downstream
lens:
defmaker downstream, do: Lens.key!(:downstream)
I find this preferable to having client code flaunt its knowledge of
Cluster
structure with code like this:
Deeply.get_only(cluster, Lens.key!(:downstream))
... or the equivalent:
get_in(cluster, [Access.key(:downstream)])
Code like this:
Deeply.get_only(cluster, Cluster.downstream)
... makes it less disruptive to change the Cluster
structure. Again: not a big deal with a shallowly nested container, but
lenses would be a waste of time if all structures were shallow and
predictable.
In fact, I like the CRUD abstraction enough that I've implemented shorthand for cases where lens maker functions applied to structs take no arguments:
Deeply.get_only(cluster, :downstream)
After all, cluster
contains its type, so the function call can be constructed easily enough.
There's something appealing to me about client code that says "within a cluster, there's a downstream. Fetch that, no matter where it is."
In this style, containers that contain smaller containers will use the
smaller containers' lens makers to make their own lens makers. So suppose
we have a Network
that contains Clusters
:
defmodule Network do
typedstruct enforce: true do
field :clusters_by_name, %{atom => Cluster.t}
field :other_fields, any, default: :just_for_show
end
...
Clients of Network
will want to extract a particular named cluster's
downstream, which can be done like this:
defmaker downstream_of(cluster_name) do
Lens.key!(:clusters_by_name) |> Lens.key!(cluster_name) |> Cluster.downstream
end
The important bit is that Network
declines to make any guesses about
the structure of Cluster
. It works from the lens API.
In the code I'm thinking of as I write, it turned out
that a Cluster
will have not just one downstream
list, but a
variety of them. You see, Clusters send "pulses" to their downstream
clusters. Originally, all pulses were the same, but later I needed to
give pulses "types", so that there's a distinction between a
default pulse and a "control pulse". So suddenly, the downstream
of
a cluster depends on the type of a pulse, so the code to retrieve the
downstream cluster names has to look like this:
Deeply.get_all(network, Network.downstream_of(originating_cluster_name, pulse_type))
The important thing here is that, once we have the cluster_names
function, client
code needn't care whether clusters contain a map of pulse types to
downstream names, meaning the path would be described like this:
defmaker downstream_of(originating_name, pulse_type do
Lens.key!(:clusters_by_name)
|> Lens.key!(originating_name)
|> Lens.key(:downstream_by_pulse_type)
|> Lens.key(pulse_type)
end
... or whether Network
maintains a mapping from {cluster, pulse_type
} to downstream names – removing from a Cluster
any
notion of its "downstream" clusters. That would mean this path:
defmaker downstream_of(originating_name, pulse_type) do
Lens.key!(:downstreams)
|> Lens.key!({originating_name, pulse_type})
end
The point is: having lens makers be module functions makes change easier.