2
votes

It's very difficult to give good title to this question… I'm stuck with HXT again. I understand what I want to do, but I'm not sure how to make it play nicely with arrows. Here I give simplified description of the problem.

Function foo takes an Int and returns an arrow:

foo :: ArrowXml a => Int -> a XmlTree XmlTree

Function bar extracts value of some attribute:

bar :: ArrowXml a => a XmlTree String

Now, I need to write baz that takes a map from Strings to Ints and returns an arrow:

import qualified Data.Map.Lazy as M

baz :: ArrowXml a => M.Map String Int -> a XmlTree XmlTree

Logic of baz: extract value of the attribute with bar and look up it in the map. If M.lookup returns Just x, invoke foo x, otherwise don't do anything (input of the arrow goes through unchanged).

AFAIK every such an arrow works as a filter, so in reality ArrowXml a => a XmlTree String type means that it takes a XmlTree and returns (possibly empty) list of Strings. This makes me reformulate logic of baz. For given input XmlTree there may be many strings, every string should be used to look up an integer and first found integer should be passed to foo. If all of them result in Nothing, don't do anything.

Here what I've come up with:

baz :: ArrowXml a => M.Map String Int -> a XmlTree XmlTree
baz m = this &&& (bar >>> arr (`M.lookup` m)) >>> arr (uncurry f)
    where f xml Nothing  = xml
          f xml (Just x) = foo x xml
-- compiler says:          ^^^ not so fast, boy

Could not deduce (ArrowXml (->)) arising from a use of ‘foo’
from the context (ArrowXml a)
  bound by the type signature for
             baz :: ArrowXml a => M.Map String Int -> a XmlTree XmlTree

Not only compiler doesn't like it, but it's difficult to reason about too.

2
It might be better to reformulate it as baz :: M.Map String Int -> a (String, XmlTree) (Maybe Int, XmlTree) and foo :: a (Maybe Int, XmlTree) XmlTree, or possibly some extra arrow between that gets rid of the Maybe problem. Then you just have bar &&& arr id >>> baz m >>> foo, which is much more readable. Basically, if your inputs are coming from arrows, pass those values around using arrows. At this point it's probably going to be worth learning the proc notation or it'll be even more difficult.bheklilr
@bheklilr, I've read HXT wiki, a paper called "Programming with Arrows", but it's still difficult to come up with idiomatic arrow code sometimes. Maybe proc notation can help. Thanks for the comment, feel free to add it as an answer if you like.Mark Karpov
Arrows are not particularly easy to work with in my experience. They're weird, there aren't as many good tutorials for them, and they make the most intuitive sense when treated as functions, but all the really interesting arrows aren't. On top of that there's a lot of Arrow* classes that implement more functionality on top of the basic arrows. I can't say that I feel confident working with arrows yet, but I know the basics of how they work and how to use them. While my answer below looks nice, it took me more than a few minutes to get it working!bheklilr

2 Answers

2
votes

If you reformulate your type signatures a bit you can get this to line up pretty well. Since you have values coming from the result of an arrow in baz affecting the behavior of foo, those values need to be fed to foo using the arrow instead of as a typical argument. This actually simplifies things a lot, but I'll recommend creating a foo, then a fooWrapper that handles the decision itself. With the correct types you'd have

{-# LANGUAGE Arrows, NoMonomorphismRestriction #-}

import qualified Data.Map as M
import Control.Arrow.ArrowTree
import Text.XML.HXT.Core

foo :: ArrowXml a => a (Int, XmlTree) XmlTree
foo = undefined

bar :: ArrowXml a => a XmlTree String
bar = undefined

Then for baz, it should be expecting both an XmlTree and the String input from bar, so it's arrow type needs to be a (String, XmlTree) something, and here I found it simplest to implement it as

baz :: ArrowXml a => M.Map String Int -> a (String, XmlTree) (Maybe Int, XmlTree)
baz m = first $ arr $ flip M.lookup m

All this arrow is doing is converting the String into a lookup on the passed in M.Map (assuming this is given in the general environment already). Then we need a wrapper to feed (Maybe Int, XmlTree) into (Int, XmlTree) if and only if the Maybe Int is a Just something. Here is where the arrow syntax really comes in handy. Since we're making a decision here it also requires that our arrow be an ArrowChoice, so

fooWrapper :: (ArrowXml a, ArrowChoice a) => a (Maybe Int, XmlTree) XmlTree
fooWrapper = proc (lkup, tree) -> do
    case lkup of
        Nothing -> returnA -< tree
        Just v  -> foo -< (v, tree)

Now we can tie everything together into an overall application with nothing more than the built-in combinators (I also discovered that returnA = arr id, so you can use that instead, I just think it's easier to understand with arr id)

program :: (ArrowXml a, ArrowChoice a) => M.Map String Int -> a XmlTree XmlTree
program m =
    bar &&& arr id >>> -- First split the input between bar and arr id
    baz m          >>> -- Feed this into baz m
    fooWrapper         -- Feed the lookup into fooWrapper so it can make the
                       -- decision on how to route the XmlTree

You don't need to worry about the ArrowChoice constraint, all the ArrowXml instances brought in scope from Text.XML.HXT.Core also implement ArrowChoice.

If you're curious as to what this would look like without proc notation, even this simple case statement would get turned into (I think)

fooWrapper :: (ArrowXml a, ArrowChoice a) => a (Maybe Int, XmlTree) XmlTree
fooWrapper =
    arr (\(lkup, tree) -> case lkup of
        Nothing -> Left tree
        Just v  -> Right (v, tree)) >>>
    (returnA ||| foo)

The use of ||| is what forces it to implement ArrowChoice. While this isn't too bad, I wouldn't exactly call it readable and there is too much going on that doesn't really have anything to do with the actual business logic. Once you move on to more complex situations this is going to explode in complexity as well, while the proc notation should remain relatively simple.

1
votes

I took me some time to understand how to accomplish this, because when your arrow has type like a (b, XmlTree) XmlTree, you can really use it, because of type conflicts with the rest of the API.

Here is another solution that seems to be more idiomatic:

baz :: ArrowXml a => M.Map String Int -> a XmlTree XmlTree
baz m = maybe this foo $< (bar >>> arr (`M.lookup` m))

All the magic happens because of ($<) function. From the documentation:

compute the parameter for an arrow with extra parameters from the input and apply the arrow for all parameter values to the input

See also this section of HXT Wiki: 8.2 Transform external references into absolute references.