34
votes

After reading this article on Clojure (http://blog.podsnap.com/ducers2.html) introducing transducers, I'm confused on what a transducer is. Is a partially applied map in Haskell, such as map (+1) a transducer? At first I thought this was a Clojure way of using partial application, but then the article goes on to implement it in Haskell with an explicit type. What use does it have in Haskell?

1
This might help with the Clojure context, if not with the translation to Haskell: stackoverflow.com/questions/26317325/…user100464
Not really a duplicate. This question is more specific than the cited question. Although many people seem to find transducers easy to understand, and find some of the presentations, such as R.H.'s easy to understand, they can easily seem weird or puzzling. Asking about the relationship to existing, well understood concepts is worthwhile, and goes beyond the cited question. Aleš Roubíček's answer to the cited question does partially answer OP's questiona above, but does not provide a full answer to it.Mars
In Clojure map (+1) is a transducer, but not in Haskell, because an untyped Lisp can break those sorts of conventions. A transducer is a function (r -> a -> r) -> r -> b -> r which leaves r as a type variable but may specialize on a and b. For example, tfilter pred f r b = if pred b then f r b else r is a transducer filter (when partially applied to pred) as it applies the reducer f only if the element matches the predicate. This gives composable map/filter semantics for any 'transducable' function: one map/filter fn can work for multiple contexts.CR Drost
Honestly I just wanted to vote for my belief that it was duplicate. I didn't think it would actually close it.J. Abrahamson
Chris's answer gets to the heart of things effectively: it's just overloaded syntax.J. Abrahamson

1 Answers

44
votes

In Clojure (map inc) is a transducer, but not in Haskell, because Haskell has to obey currying but an untyped Lisp can break that curry-by-default convention. The type of transducers is instead:

type Transducer a b = forall r . (b -> r -> r) -> (a -> r -> r)

We say that the transducer 'turns a into b'. Yes, the a and the b seem "backwards" on the right hand side. The forall means that a Transducer must leave r as a general type variable but is totally allowed to specialize on a and b.

Let me reverse two of the arguments in foldr:

-- cf. with foldr :: (x -> r -> r) -> r -> [x] -> r
ffoldr :: (x -> r -> r) -> [x] -> r -> r
ffoldr = flip . foldr

(we can also use foldl but it will tend to reverse things later). This means that a Transducer can be used to transform the first argument of ffoldr from x to y, so that we can instead process an [x] with a y -> r -> r using foldr. Transducers 'sit between' the inputs ([x], r) and the final processor (y, r) -> r.

I've flipped the second two arguments to emphasize also that, surprisingly, ffoldr :: Transducer [x] x. By using the symmetry of the arguments we also have a generic composition of transducers, which happens to be just function composition:

(.) :: Transducer a b -> Transducer b c -> Transducer a c

(If you think it's cool that giving these forall r terms lets us reverse how you normally use ., you can do it arbitrarily via a technique called "continuation passing".)

For example, here is the filter transducer:

tfilter :: (a -> Bool) -> (a -> r -> r) -> a -> r -> r 
    -- or: (a -> Bool) -> Transducer a a
tfilter predicate f a = if predicate a then f a else id

This applies the reduction function f to a and r only if the predicate holds. There is also a mapping transducer:

tmap :: (a -> b) -> (b -> r -> r) -> a -> r -> r 
tmap ba f a = f (ba a)

This gives composable map/filter semantics for any 'transducable' type: one map/filter fn can work for multiple contexts.

The Transducer type has a cute isomorphism: it turns out that the foldr of a list forall r. (x -> r -> r) -> r -> r is perfectly equivalent to that list [x] (it is the "Church encoding" of that list), and therefore swapping the argument a to the very front of the transducer definition gives us the (IMO much easier to understand!) type type TransL a b = a -> [b]. And this is much easier to understand:

tl_map f = \a -> [f a]
tl_filter predicate = \a -> if predicate a then [a] else []

To run these on a list, use concatMap... which happens to be just >>=! So you just write collection >>= transducer and you have the transduced collection. The meaning of TransL a b is then, "take each element of the original list of a, and give me 0 or more elements of type b to splice into my outgoing list." It filters by splicing 0 elements when the predicate doesn't work; it maps by yielding 1 output element for each input element; another operation tl_dupe = \a -> [a, a] is a transducer which duplicates the elements in a list, [1,2,3] >>= tl_dupe becomes [1,1,2,2,3,3].

Where foldr appears to be a Transducer [x] x it is now seen to be identical to id :: TransL [x] x which has a way of simply performing a concat operation in the middle of a computation; the identity function in this algebra is actually return = \a -> [a], also written (:[]). The only loss is that we can no longer use . to compose these, but in fact the same composition is provided in Control.Monad as the Kleisli composition operator >=>.

So long story short, transducers are functions a -> [b] cleverly transformed with a bit of Church encoding so that the Kleisli composition operator for these Kleisli arrows of the list monad, becomes simply (.).