(Note: this post is a literate-haskell file. You can copy-paste it into a text
buffer, save it as someFile.lhs
, and then run it using ghc.)
Problem description: I want ot create a graph with two different node types that
reference each other. The example below is very simplified. The two data types
A
and B
, are virtually identical here, but there's a reason for them to be
different in the original program.
We'll get the boring stuff out of the way.
> {-# LANGUAGE RecursiveDo, UnicodeSyntax #-}
>
> import qualified Data.HashMap.Lazy as M
> import Data.HashMap.Lazy (HashMap)
> import Control.Applicative ((<*>),(<$>),pure)
> import Data.Maybe (fromJust,catMaybes)
The data type definitions are themselves trivial:
> data A = A String B
> data B = B String A
In order to symbolize a difference between the two, we'll give them a different
Show
instance.
> instance Show A where
> show (A a (B b _)) = a ++ ":" ++ b
>
> instance Show B where
> show (B b (A a _)) = b ++ "-" ++ a
And then tying the knot is of course trivial.
> knot ∷ (A,B)
> knot = let a = A "foo" b
> b = B "bar" a
> in (a,b)
This results in:
ghci> knot
(foo:bar,bar-foo)
That's exactly what I want!
Now the tricky part. I want to create this graph at runtime from user input. This means I need error handling. Let's simulate some (valid but nonsensical) user input:
> alist ∷ [(String,String)]
> alist = [("head","bot"),("tail","list")]
>
> blist ∷ [(String,String)]
> blist = [("bot","tail"),("list","head")]
(the user would of course not input these lists directly; the data would first be massaged into this form)
It is trivial to do this without error handling:
> maps ∷ (HashMap String A, HashMap String B)
> maps = let aMap = M.fromList $ makeMap A bMap alist
> bMap = M.fromList $ makeMap B aMap blist
> in (aMap,bMap)
>
> makeMap ∷ (String → b → a) → HashMap String b
> → [(String,String)] → [(String,a)]
> makeMap _ _ [] = []
> makeMap c m ((a,b):xs) = (a,c a (fromJust $ M.lookup b m)):makeMap c m xs
This will obviously fail as soon as the input list of String
s references
something that isn't found in the respective maps. The "culprit" is fromJust
;
we just assume that the keys will be there. Now, I could just ensure that the
user input is actually valid, and just use the above version. But this would
require two passes and wouldn't be very elegant, would it?
So I tried using the Maybe
monad in a recursive do binding:
> makeMap' ∷ (String → b → a) → HashMap String b
> → [(String,String)] → Maybe (HashMap String a)
> makeMap' c m = maybe Nothing (Just . M.fromList) . go id
> where go l [] = Just (l [])
> go l ((a,b):xs) = maybe Nothing (\b' → go (l . ((a, c a b'):)) xs) $
> M.lookup b m
>
> maps' ∷ Maybe (HashMap String A, HashMap String B)
> maps' = do rec aMap ← makeMap' A bMap alist
> bMap ← makeMap' B aMap blist
> return (aMap, bMap)
But this ends up looping indefinitely: aMap
needs bMap
to be defined, and bMap
needs aMap
. However, before I can even begin to access the keys in either map,
it needs to be fully evaluated, so that we know whether it is a Just
or a
Nothing
. The call to maybe
in makeMap'
is what bites me here, I think. It
contains a hidden case
expression, and thus a refutable pattern.
The same would be true for Either
so using some ErrorT
transformer won't
help us here.
I don't want to fall back to run-time exceptions, as that would bounce me back
to the IO
monad, and that would be admitting defeat.
The minimal modification to the above working example is to just remove
fromJust
, and only take the results that actually work.
> maps'' ∷ (HashMap String A, HashMap String B)
> maps'' = let aMap = M.fromList . catMaybes $ makeMap'' A bMap alist
> bMap = M.fromList . catMaybes $ makeMap'' B aMap blist
> in (aMap, bMap)
>
> makeMap'' ∷ (String → b → a) → HashMap String b → [(String,String)] → [Maybe (String,a)]
> makeMap'' _ _ [] = []
> makeMap'' c m ((a,b):xs) = ((,) <$> pure a <*> (c <$> pure a <*> M.lookup b m))
> :makeMap'' c m xs
This doesn't work either, and, curiously, results in slightly different behaviour!
ghci> maps' -- no output
^CInterrupted.
ghci> maps'' -- actually finds out it wants to build a map, then stops.
(fromList ^CInterrupted
Using the debugger showed that these aren't even infinite loops (as I would have expected) but execution just stops. With maps'
I get nothing, with the second attempt, I at least get to the first lookup, and then stall.
I'm stumped. In order to create the maps, I need to validate the input, but in order to validate it, I need to create the maps! The two obvious answers are: indirection, and pre-validation. Both of these are practical, if somewhat inelegant. I would like to know whether it is possible to do the error-catching in-line.
Is what I'm asking possible with Haskell's type system? (It
probably is, and I just can't find out how.) It is obviously possible by
percolating exceptions to the toplevel at fromJust
, then catching them in IO
, but that's not how I'd like to do it.
ReaderT maps (Writer (Either errs assocs)) a
monad. The tricky bit is that when youtell
the writer about an association (or error) you have to unconditionally tell (ie, tell a thunk that later will decide if it's an error or an association). Then when you tie the knot, run the writer and at that point force the log while constructing the maps that will be fed back into the reader. The whole thing turned out to be quite fragile, so while it was an interesting exercise, it's worth rewriting as two separate passes, IMO. – IndifferenceWriter
and one pass making themaps for the
ReaderT`. – EweNothing
orLeft error
or similar for the whole calculation? – EweLeft error
, but since bothMaybe
andEither
fall under the same abstraction classes, I went with the simpler one in the reduced minimal example here. – RaddledST
. I do fear that you're correct, and the entire thing might turn out to be too fragile. I'll have to see if it works well within the context of the program I'm writing (an interpreter, and this is the type graph, whereA
are like records, andB
are the actual types.) – Raddled