Lenses/Optics Possibly Applicable to PICK/POKE

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, Prisms 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…!

1 Like

Thanks for giving some concrete examples. I read about these once, in cartoon format, but didn't really absorb it:

Lenses In Pictures - adit.io

Where things are going with tuple picking, it's fairly specialized code, e.g.:

map.^field: x  =>  (unlift map.field: lift x)

^map.field: x  =>  (unlift map).field: x

map.(^field): x  =>  map.(unlift field): x

map.('^field): x  =>  map.(the ^field): x

map.'^field: x  =>  map.(the ^field): x

(Hopefully this makes sense, including the META=>LIFT rename, speak up if not.)

Other nuanced behavior will likely apply to @ and $ tuple-members. So sitting on top of any reading/writing facility is some behavior that will probably be quite hard to generalize.

Also, this is really central to the system's performance, as it's all being interpreted. Needs tricks to make it perform adequately. So it's probably going to be just a shadow of a generalization.

But maybe some prototyping of lens-like things in usermode would be worth doing. Now that there's a pretty good method for adding (and typechecking) arbitrary extension types, dabbling with a persistent vector extension could be interesting...to see if there could be some lensing that generalizes across mutable and immutable structures.