Reading this, I am reminded very strongly of functional optics, as implemented in e.g. Haskell’s lens
or optics
. These are, essentially, first-class encodings of composable ‘locations’ at which you can get or set. For instance:
ghci> import Control.Lens
ghci> value = (5,10,("nested","values"))
ghci> view _1 value
5
ghci> view (_3 . _1) value
"nested"
ghci> set _1 6 value
(6,10,("nested","values"))
ghci> set (_3 . _2) "new" value
(5,10,("nested","new"))
Here _1
, _2
and _3
are predefined ‘lenses’ for getting elements of a tuple. All lenses can be composed: _3 . _2
is a lens pointing to the second element of the third element of a tuple.
You can define your own lenses, of course:
ghci> data MyRecord = MyRecord { field1 :: Int, field2 :: String } deriving (Show)
ghci> field1Lens = lens field1 (\r v -> r { field1 = v })
ghci> view field1Lens $ MyRecord { field1=10, field2="test" }
10
ghci> set field1Lens 20 $ MyRecord { field1=10, field2="test" }
MyRecord {field1 = 20, field2 = "test"}
…although there are macros to do it for you (which most people use).
Incidentally, lens
also comes about as close to dialecting as is possible in Haskell. There are operator versions of everything: value ^. lens
gets, value & lens .~ newvalue
sets. Many operators effectively let you simulate imperative programming: e.g. value & lens +~ 1
increments the value at the lens
.
There are various ways of actually implementing these things. The simplest is simply by storing a getter and a setter together, defining appropriate functions to manipulate them self-consistently. lens
uses the van Laarhoven encoding, which represents them as a higher-order function parameterised over a typeclass. optics
uses a variation on this, the ‘profunctor encoding’ (for which see the Glassery).
The latter two encodings are in fact immensely powerful, allowing for a number of different generalisations on the basic theme. For instance, a Traversal
points to multiple values at once:
ghci> [1,2,3,4,5] & traverse +~ 10
[11,12,13,14,15]
ghci> [1,2,3,4,5] & taking 3 traverse +~ 10
[11,12,13,4,5]
ghci> [1,2,3,4,5] & dropping 3 traverse +~ 10
[1,2,3,14,15]
ghci> [(1,2),(3,4),(5,6),(7,8),(9,10)] & (dropping 3 traverse . both) +~ 10
[(1,2),(3,4),(5,6),(17,18),(19,20)]
ghci> [(1,2),(3,4),(5,6),(7,8),(9,10)] & dropping 3 (traverse.both) +~ 10
[(1,2),(3,14),(15,16),(17,18),(19,20)]
ghci> [(1,2),(3,4),(5,6),(7,8),(9,10)] & dropping 3 (traverse._1) +~ 10
[(1,2),(3,4),(5,6),(17,8),(19,10)]
As you can see, they combine nicely with other traversals and lenses, with a great deal of specificity. Similarly, Prism
s let you select things which may be present or absent:
ghci> Left 1 & _Left +~ 10
Left 11
ghci> Right 1 & _Left +~ 10
Right 1
ghci> isn't _Left (Left 1)
False
ghci> isn't _Left (Right 1)
True
Of course they too can combine with lenses, traversals, and all the other things defined by lens
(e.g. the operator +~
, as above).
I could go on like this for a while but I’ll stop here…!