Rays - Lensing the Outside World

Posted on November 13, 2014

Lenses are great. Lenses are particularily great at inspecting, modifying, folding and traversing pure data structures by providing an abstraction akin to pointers into these data structures. Pointers that may be read only or pointers that may point to multiple or no data structure at all, etc. But when you want a pointer into a file, a database or a pure datastructure on another computer, you are out of luck. We have pipes, but pipes don’t let you focus on a field in a parsed data structure in a file and write the changes back.

This is where a neat idea comes into play: Rays are essentially lenses that take their values from monadic actions. But first, let us start with imports, since this is a literate Haskell file.

import Prelude         hiding (readFile, mapM)
import Control.Lens
import Control.Applicative
import Control.Monad.Identity (Identity, runIdentity)
import Control.Monad.State    (State, get, put)
import Data.Traversable       (Traversable, traverse, mapM)
import System.IO.Strict       (readFile)
import Data.Char

Encoded in a way pretty similar to van Laarhoven lenses rays are defined as:

type Ray m f t a b = (a -> f b) -> m (f t)

This is a form of continuation passing style: We take a function a -> f b that is applied on the values the ray points to, and transform it to an action in a monad m. The choice of a functor f allows us to define a variety of operations on rays, similar to lenses:

viewR :: Functor m => Ray m (Const a) t a b -> m a
viewR e = fmap getConst $ e Const

overR :: Functor m => Ray m Identity t a b -> (a -> b) -> m t
overR e f = fmap runIdentity $ e $ Identity . f

infixr 4 .%~
(.%~) :: Functor m => Ray m Identity t a b -> (a -> b) -> m t
(.%~) = overR

As an aside, if we instantiate m to (->) s in a ray, we recover van Laarhoven lenses. So viewR and overR work on lenses too: overR _Just (+1) (Just 3) = Just 4

Example: Files

For demonstration purposes, let us define a ray for files:

fileR :: Traversable f => FilePath -> Ray IO f () String String
fileR path f = traverse (writeFile path) . f =<< readFile path

Say we have a file lorem.txt in out working directory, we could write its contents. Or read it. Or make the first letter of all words in the file upper case:

fileExample :: IO ()
fileExample = do
  fileR "lorem.txt" .%~ const "foo bar"
  putStrLn =<< viewR (fileR "lorem.txt")
  fileR "lorem.txt" . worded . _head .%~ toUpper

But wait! worded and _head are traversals! This is what I meant, when I said that rays are half lenses and half actions. The one half, a -> f b, looks just like one half of a lens, and can be composed with lenses, traversals and the like, since (.) only needs one side of each of the composed functions to match. This means that we can reuse all the lens goodness to focus into our files.

Example: State

The lens library provides a couple of functions for applying lenses to the state in a state monad. There is a ray for that:

stateR :: (Traversable f, MonadState s m) => Ray m f () s s
stateR f = mapM put . f =<< get

This way we can use overR and viewR to access the state; no extra combinators needed. Beware, this example is as pointless as the one before.

stateExample :: State (Int, String) Int
stateExample = do
  stateR . _1 .%~ (+1)
  stateR . _2 .%~ reverse
  viewR (stateR . _1)

Future Work

I can imagine quite a few scenarios, in which rays might become useful. There is still a lot to discover, since the idea came fresh out of my head and I wanted to write it down as soon as possible, before I procrastinate publishing it so long that I forget about it. Maybe its not even new. I don’t know, but I haven’t seen anything quite like it yet. Future work might include: