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 appendx
toys
. - 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 declarefilterA = arr filter
. You really want to take a lifted arrowp
. That said, the change would be to simply typep
instead ofarr 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
精彩评论