In the first Haskell Cast podcast Rein Henrichs and Chris Forno interviewed Edward Kmett in part about lens
and it was suggested that Prism
s don't have the same kind of introductory tutorial treatment. That's a shame, though. Prism
s arise naturally all the time when using sum types.
You could have invented Data.Aeson.Lens (and maybe you already have)
I learned to use Prism
s when using lens
[1] with aeson
so let us examine an extended use case to motivate using Prism
s.
It's fairly common in dynamic languages to consume JSON like this
> it = json.loads('[{"someObject" : { "version" : [1, 0, 0] }}]')
> it[0]["someObject"]["version"][1]
which might, say, be accessing the minor version number of some nested object. In Python here we use an isomorphism (i.e. a two-way mapping that establishes equivalence) between JSON types and fairly common Python types and then just destruct the JSON information as an "array of dictionaries of dictionaries of arrays".
Python users translating that line to Haskell with the JSON library aeson
can grow frustrated by the (pathological) example below showing how tedious the manual destructuring in Haskell is when you're forced to be explicit but don't know how to use Lens
s or Prism
s.
import Prelude hiding (lookup)
import Data.HashMap.Strict (lookup)
import Data.Vector ((!?))
case decode "[{\"someObject\" : { \"version\" : [1, 0, 0] }}]" of
Nothing -> error "Oh, I guess sometimes the JSON string might be invalid..."
Just (Array a) ->
case a !? 0 of
Nothing -> error "Oh, previously I assumed there was at least ONE object..."
Just (Object o) ->
case lookup "someObject" o of
...
Just _ -> error "Oh, what about if I don't actually have an object?"
Just _ -> error "Oh, what about if I don't actually have an array?"
Destructuring is far more explicit here and conflates getting the values with all kinds of "schema validation" errors.
Usually the next step is to learn to use the ToJSON
/FromJSON
machinery to fold all of your schema validation into decode
and then build a really robust, independent serialization adapter for whatever internal ADTs you're using to actually model your domain. Which is great when you've got time to kill, but it doesn't really represent that original Python snippet which is far more likely to be a one-off JSON-munging script than a robust program.
So can we do better if we just want to either get our value or fail?
Failure is a monad right?
Of course it is. Let's pick Maybe
and inject everything into it to consolidate those errors.
We'll need to extract a few helpers from the above
anObject :: Value -> Maybe Object
anObject (Object m) = Just m
anObject _ = Nothing
anArray :: Value -> Maybe Array
anArray (Array a) = Just a
anArray _ = Nothing
and then using do
notation the example is much clearer.
do json <- decode thatString
ary <- anArray json
zeroth <- ary !? 0
obj <- anObject zeroth
keys <- lookup "someObject" obj
keyObj <- anObject keys
ver <- lookup "version" key Obj
verAry <- anArray ver
verAry !? 1
which is much nicer and, for those with a touch of Monad-fu, can be further simplified by using (>=>)
into something that's almost as nice as the original Python code while being far more explicit about the kinds of assumptions and failure modes that exist.
decode >=> anArray >=> (!? 0)
>=> anObject >=> lookup "someObject"
>=> anObject >=> lookup "version"
>=> anArray >=> (!? 1)
One way to think of this chain is it's a method of traversing our JSON structure by repeated descent into sum types like Value
. Getting a Nothing
indicates that the descent we would have liked to take is actually not available. With all this talk of "repeated descent" and "traversing" you can begin to expect that there might be a way to achieve this from within the lens
ecosystem.
Basic Lens
es don't quite work because they don't have a good notion of failure, but there is a straightforward and powerful way to encode this into lens
and we'll see that it's essentially exactly what is done in Data.Aeson.Lens
.
A short diversion on the nature of Traversals
A Lens can usually be thought of as a first-class method of separating a piece of an object from its whole. In this light, we can think of _1
, suitably specialized, as the split
(a, b) --> a AND ( _ , b )
which then lets us operate by either retreiving the piece or updating the whole. An obvious generalization is the Traversal
which lets us generalize our target to 0-or-more parts at once. This takes shape in both :: Traversal' (a, a) a
which targets both the fst
and snd
element simultaneously or each :: Traversal' [a] a
which targets all of the elements of a list at once.
Traversal
s are much more powerful than Lens
es but, as usual, with more power comes less certainty. Lens
es can be view
ed or (^.)
'd easily since they embody the guarantee that we're focused on exactly one part of the whole. Traversal
s on the other hand can target subparts which may not exist. Consider what would happen if you tried to view each
of a list. If each
were a lens then view elements
would have type [a] -> a
but there are two things that can go wrong:
If the list is empty we'll have to throw an error
If the list has multiple elements we'll have to "summarize" them somehow.
In order to implement view
for a traversal, we'll need support from a Monoid
instance for a
so that we can handle case (1) with mempty
and case (2) with mappend
. This is why you can sometimes get weird errors in lens such as what happens when we try view each "foo"
<interactive>:42:22:
No instance for (Data.Monoid.Monoid Char)
arising from a use of `each'
as view
is trying to combine the multiple subparts with a non-existent Monoid
instance. If we try it instead like view each ["f", "o", "o"]
we can use the (free) Monoid
instance for [a]
> view each ["f", "o", "o"]
"foo"
For convenience, lens
gives us two variants on view
/(^.)
which pre-wrap our subparts in useful monoids. We have preview
/(^?)
which prewraps the subparts in First
and toListOf
/(^..)
which prewraps the subparts in []
.
> "foo" ^? each
Just 'f'
> "foo" ^.. each
"foo"
We'll make use of (^?)
especially in the parts to come.
Traversing JSON with Prisms
Remember that the trouble with using Lens
es to peel off layers of our JSON structure is that our expectations about whether or not a layer can actually be peeled away may be incorrect and Lens
es assume that we have exactly one valid subpart to target.
More generally, you might say that Lens
es target product types well, picking one of several pieces to focus on, but have trouble with sums (coproducts) since we may not have any pieces at all for them to target.
Traversal
s solve this problem by using a Monoid
instance to handle failure and multiple results well, but they do so at the cost of knowing anything at all about how many pieces are being targeted. This is sufficient for JSON, but a little bit of overkill. We know that there will be exactly 0 or 1 Array
s in any Value
. That notion is the first part of a Prism
.
The haddocks say that a Prism
is a Traversal that can also be turned around with re to obtain a Getter in the opposite direction. It turns out that "turning around" is a very powerful property that lets us rebuild entire structures from only their piece and the knowledge that they ought to be accessible via our prismatic Traversal
. To drive the distinction home, consider a Traversal
over the 3rd element in a list (it's called ix 2
). It's 0-or-1 targeted, but given only a piece of the list and that Traversal
you cannot rebuild the origin list uniquely. The Traversal laws hold for every Prism and we traverse at most 1 element.
0-or-1 target Traversal
s that can be "Reviewed" like this form exactly the Prism
s. For instance, lens
has an entire module and class devoted to Cons
-ing things on to the front of lists—an operation that, unlike ix 2
can be "reviewed".
The place you typically will see Prism
s is when desconstructing a sum type. Each disjoint constructor will get its own Prism
as any value of the sum has either 0 or 1 of those constructors being used. This is exactly the notion we need to "lensify" the aeson
Value
type, and, indeed, that's exactly what Data.Aeson.Lens
from lens
provides.
_Object :: Prism' Value Object
_Object = prism' anObject Object
_Array :: Prism' Value Array
_Array = prism' anArray Array
_String :: Prism' Value String
_String = prism' aString String
_Number :: Prism' Value Number
_Number = prism' aNumber Number
Which is not much more than an introduction of our "failing deconstructors" from the first section.
Data.Aeson.Lens
also provides a few Traversal
s based upon ix
but specialized to JSON types
key :: Text -> Traversal' Object Value
nth :: Int -> Traversal' Array Value
Using these Prism
s and Traversal
s we can pick apart our JSON object as we please.
case decode theString of
Nothing -> Nothing
Just it -> it ^? _Array . nth 0
. _Object . key "someObject"
. _Object . key "version"
. _Array . nth 1
Or, even better, by using the built-in Data.Aeson.Lens
isomorphisms and type classes we can write this directly.
theString ^? nth 0 . key "someObject" . key "version" . nth 1
and have the 1-or-0 target nature of Prism
s work its magic in the background. Finally, we're looking at a Haskell example which rivals the original Python's terseness without sacrificing nearly as much type safety.
Want to play with the example? Here's a bit of preamble to get you going in ghci.
-- to play with this example from ghci,
-- install lens 4 or newer and :set -XOverloadedStrings
-- OverloadedStrings is so it can automatically turn the String literals into Text
-- values which is the type used for indexing into Object.
import Data.Aeson
import Data.Aeson.Lens
import Control.Lens
-- this code is intended for ghci experimentation, so we use "let" to bind a value to a name
let jsonBlob = "[{\"someObject\": {\"version\": [1, 0, 3]}}]"
-- example from above
let myVal = jsonBlob ^? nth 0 . key "someObject" . key "version" . nth 1
-- What progressively composing the prisms looks like, note the composition order:
-- λ> jsonBlob ^? nth 0
Just (Object fromList
[("someObject",
Object fromList
[("version",Array
(fromList [Number 1.0, Number 0.0, Number 3.0]))])])
-- λ> jsonBlob ^? nth 0 . key "someObject"
Just (Object fromList
[("version",
Array (fromList [Number 1.0, Number 0.0, Number 3.0]))])
-- λ> jsonBlob ^? nth 0 . key "someObject" . key "version"
Just (Array (fromList [Number 1.0, Number 0.0, Number 3.0]))
-- λ> jsonBlob ^? nth 0 . key "someObject" . key "version" . nth 1
Just (Number 0.0)
(Thanks for commentary and corrections: acow, Effnote, edwardk, shachaf. Thanks for updating this to be current for the lens-4.*
series to Chris Allen (@bitemyapp))
[1] Note that that isn't aeson-lens
, which is a similar package that looks like a bunch of Lens
es but is actually invalid, in no small part because it doesn't invoke any Prism
s. Also, lens-aeson
has been deprecated and its support for Aeson has been merged into the main lens
library.