Brandon.Si(mmons)

code / art / projects

Announcing Yet Another Lens Library

I just uploaded the first version of a lens library I’ve been working on, called yall. You can get it with a

cabal install yall

or check it out on github. There will be a Template Haskell library for automatically deriving lenses at some point in the future.

I was motivated primarily by the desire for a lens that is acceptable for pez (existing libs such as the excellent fclabels or data-lens didn’t fit the bill for assorted reasons), and to explore some abstractions and generalizations re lenses more deeply.

The result is fairly rough at this point, but I’m interested in feedback.

Distinguishing features

This is a bit of copy/paste from the docs I just wrote.

  • Lenses are parameterized over two Monads (by convention m and w), and look like a -> m (b -> w a, b). this lets us define lenses for sum types, that perform validation, that do IO (e.g. persist data to disk), etc., etc.

  • a module Data.Yall.Iso that complements Lens powerfully

  • a rich set of category-level class instances (for now from categories) for Lens and Iso. These along with the pre-defined primitive lenses and combinators give an interface comparable to Arrow

Examples

And here is a little showcase of functionality. First, an illustration of partial lenses, appropriate for multi-constructor types.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
-- First, lenses for a sum type. This is something like what we'll generate
-- with template haskell.
data Test a = C1 { _testString :: String, _testA :: a }
            | C2 { _testString :: String, _testRec :: Test a }

-- pure lens, polymorphic in Monad for composability:
testString :: (Monad w, Monad m)=> Lens w m (Test a) String
testString = Lens $ \t-> return (\s-> return t{ _testString = s }, _testString t)

-- lenses that can fail. For now, use Maybe. In the TH library, I'll probably
-- generate lenses polymorphic in <http://hackage.haskell.org/package/failure>
testA :: LensM Maybe (Test a) a
testA = Lens f where
    f (C1 s a) = return (return . C1 s, a)
    f _ = Nothing

testRec :: LensM Maybe (Test a) (Test a)
testRec = Lens f where
    f (C2 s i) = return (return . C2 s, i)
    f _        = Nothing

-- conposing a pure and partial lens:
demo0 :: Maybe String
demo0 = getM (testString . testRec . testRec) (C2 "top" (C1 "lens will fail" True))

Now an example of a more creative use of monadic getter, allowing us to define a lens on the “Nth” element in a list, returning our results in the [] monad environment.

1
2
3
4
5
6
7
8
9
10
-- Here we have a lens with a getter in the list monad, defining a mutable view
-- on the Nth element of the list:
nth :: LensM [] [a] a
nth = Lens $ foldr nthGS []
    where nthGS n l = (return . (: map snd l), n) : map (prepend n) l
          prepend = first . fmap . liftM . (:)

-- This composes nicely. Set the Nth element of our list to 0:
demo1 :: [ [(Char,Int)] ]
demo1 = setM (sndL . nth) 0 [('a',1),('b',2),('c',3)]

Finally here’s a bit of a silly example illustrating a lens with Monadic setter (w) that does IO, in this case persisting a serialized version of the data we’re operating on to a text file.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
-- persist modifications to a type to a given file. An effect-ful identity lens.
persistL :: (Monad m) => FilePath -> Lens IO m String String
persistL nm = Lens $ \s-> return (\s'-> writeFile nm s' >> return s', s)

-- we'll use this one:
tmpFile = "/tmp/yall-test"
printFileContents = putStrLn . ("file contents: " ++) =<< readFile tmpFile

-- build a lens with some pre-defined Iso's that offers a [Int] view on a
-- string that looks like, e.g. "1 2 3 4 5":
unserializedL :: (Monad w, Monad m) => Lens w m String [Int]
unserializedL = isoL $ ifmap (inverseI showI) . wordsI

-- now add "persistence" effects to the above lens so everytime we do a "set"
-- we update the file "yall-test" to redlect the new type.
unserializedLP :: (Monad m) => Lens IO m String [Int]
unserializedLP = unserializedL . persistL tmpFile

demo2 :: IO ()
demo2 = do
    -- apply the lens setter to `mempty` for some Monoid ([Char] in this case)
    str <- setEmptyW unserializedLP [1..5]

    -- LOGGING: the string we got above (by setting [Int]) was written to a file:
    print str
    printFileContents

    str' <- modifyW unserializedLP (map (*2) . (6 :) . reverse) str

    -- LOGGING: now the file was modified to reflect the changed value:
    print str'
    printFileContents

Future

I still need to create TH deriving functionality for the package, and will announce when that happens. Been busy lately so I’m not sure when I’ll get to it, but let me know your questions/comments/concerns and I’ll try to address them promptly.

Comments