开发者

How can I define a filter function for arrows?

I'm currently reading through the paper Programming with Arrows by John Hughes and I'm already stumped开发者_如何学Go on the first exercise, in section 2.5, on pg 20.

We have the Arrow and ArrowChoice typeclasses at our disposal, as well as instances for functions, stream functions [a] -> [b], and monadic functions a -> m b via the Kleisli type.

A mapA example was given:

mapA f = arr listcase >>>
         arr (const []) ||| (f *** mapA >>> arr (uncurry (:)))

Here's an attempt:

listcase :: [a] -> (Either () (a,[a]))
listcase [] = Left ()
listcase (x:xs) = Right (x,xs)

helper :: (Bool,a) -> [a] -> Either (a,[a]) [a]
helper (True,x)  y = Left (x,y)
helper (False,x) y = Right y

test :: Arrow a => (b -> Bool) -> a (b,c) ((Bool,b), c)
test p = first (arr p &&& arr id)

filterA :: Arrow a => (b -> Bool) -> a [b] [c]
filterA p = f >>> (g ||| (h >>> (j ||| (filterA p))))
   where f = arr listcase
         g = arr (const [])
         h = test p >>> (uncurry helper)
         j = (arr id *** (filterA p)) >>> (arr (uncurry (:)))

The (bruteforce) rationale behind this futile attempt is as follows: There are two choices to be made with filterA: the listcase like map, and the result of applying the predicate p. It starts off like map, checking the list and returning an Either value using listcase. In the case of an empty list g is applied, otherwise everything to the right of ||| is applied to a value of type (a,[a]), containing the head and tail respectively. The h function is applied first, which applies the predicate while keeping the head, returning a value of type ((Bool, head),tail). This is passed to (uncurry helper), which decides whether to keep the head or not depending on the Bool value. It returns the result as an Either value so that we may apply the choice method (|||) to it. This value is passed to the next choice: (j ||| (filterA p)) such that if the predicate held True then j is applied to a pair containing the head and tail. The head is filtered through using id while the filter p is applied to the tail. Both results are returned as a pair. This pair is then reconciled using arr (uncurry (:)) like map. Otherwise the tail is passed to filterA p alone.

I doubt it's as difficult as I'm making it out to be, I assume I'm missing something quite obvious.


Sorry I'm not quite following your logic, but let's see what the non-arrow code does. It

  • Checks to see if the list is empty, if so returns that
  • Otherwise, pulls the head of the list off, call the element x
  • Recurses on the rest of the list, call the result ys
  • If the predicate p is true on the head, then we append x to ys.
  • Otherwise, we return ys

The listcase function [to implement the first 2 tasks] looks good, though remember you're returning a list, so might as well return that instead of unit and remapping through const [].

You have the recursion code for the third bullet buried in the two last cases, whereas I expose it directly, but that's okay.

For the last merging, you write it with a |||, but since you don't need to compose any other arrows in your target category, you might as well just lift a function to do all of the work. In my code below, that's rejoin.

filterA :: forall arr a. ArrowChoice arr => arr a Bool -> arr [a] [a]
filterA p = arr lstcase >>> (filterRest ||| id) where
    -- left if has a head, right otherwise
    lstcase :: [a] -> (Either (a, [a]) [a])
    lstcase (x:xs) = Left (x, xs)
    lstcase [] = Right []

    -- if we got a head, then
    --     for the head ("first" in code), map this to itself and p's result on it
    --     recurse on the rest of the list ("second" part)
    --     append the element to the recursion result if p's result is true
    filterRest :: arr (a, [a]) [a]
    filterRest = (first (id &&& p) >>> second (filterA p) >>> arr rejoin)

    rejoin :: ((a, Bool), [a]) -> [a]
    rejoin ((x, True), rest) = x:rest
    rejoin ((x, False), rest) = rest

Certainly it takes time to express your ideas clearly with the likes of ***, &&&, |||, first, etc.

A little critique.

  • Make sure you're implementing the function they're asking for!! If you just take a raw function p, then you might as well declare filterA = arr filter. You really want to take a lifted arrow p. That said, the change would be to simply type p instead of arr p, so your code has the right idea.
  • (uncurry helper) is not something in the arrow space, it's just a raw function.

When developing this stuff, I usually write a skeleton, and declare the types. That helps me figure out what's going on. For example, I started with

filterA :: ArrowChoice arr => arr a Bool -> arr [a] [a]
filterA p = arr lstcase >>> (filterRest ||| id) where
    -- left if has a head, right otherwise
    lstcase :: [a] -> (Either (a, [a]) [a])
    lstcase = undefined

    filterRest :: arr (a, [a]) [a]
    filterRest = undefined

When you add fa into the filterRest declaration however, you need to tell it that arr for filterRest is the same as the one for filterA (type variable scoping), so use the forall arr a. as above.


Here's a slight simplification:

test :: Arrow a => (b -> Bool) -> a b ([b] -> [b])
test p = arr $ \b -> if p b then (b:) else id
         
filterA :: Arrow a => (b -> Bool) -> a [b] [b]
filterA p = f >>> (g ||| h)
  where f = arr listcase
        g = arr id
        h = (test p *** filterA p) >>> (arr (uncurry ($)))

filterA' :: Arrow a => a b Bool -> a [b] [b]
filterA' p = f >>> (g ||| h)
  where f = arr listcase
        g = arr id
        h = (i *** filterA p) >>> (arr (uncurry ($)))
        i = proc x -> do 
              b <- p -< x
              returnA -< if b then (x:) else id
0

上一篇:

下一篇:

精彩评论

暂无评论...
验证码 换一张
取 消

最新问答

问答排行榜