Hi,
I've been obsessed about applicative composition since I read The Essence of Iterator Pattern (EIP) a few weeks ago. My original motivation was to learn about Traversable, but as I dug deeper I came to think Scalaz is missing a detail that can make it more useful. Let me know if I am on to something.
Here are two applicative functions f an g:
scala> val f = { (x: Int) => x + 1 }
f: Int => Int = <function1>
scala> val g = { (x: Int) => List(x, 5) }
g: Int => List[Int] = <function1>
We can use them to traverse List(1, 2, 3) for example:
scala> List(1, 2, 3) traverseU f
res0: Int = 9
scala> List(1, 2, 3) traverseU g
res1: List[List[Int]] = List(List(1, 2, 3), List(1, 2, 5), List(1, 5, 3), List(1, 5, 5), List(5, 2, 3), List(5, 2, 5), List(5, 5, 3), List(5, 5, 5))
EIP mentions operators that can be used to compose two applicative functions that returns another. These are provided so we can traverse the data structure once and get composed results:
data (m ⊠ n) a = Prod { pfst::m a, psnd::n a }
(⊗) :: (Functor m, Functor n) ⇒ (a → m b) → (a → n b) → (a → (m ⊠ n) b)
(f ⊗ g) x = Prod(f x)(g x)
and
data (m n) a = Comp { unComp::m (n a) }
(⊙) :: (Functor n, Functor m) ⇒ (b → n c) → (a → m b) → (a → (m n) c)
f ⊙ g = Comp ◦ fmap f ◦ g
Just looking at the signature, these look similar to &&& and <<< defined for Arrow, but the way it behaves as functor is different. For example, suppose we use &&& to compose f and g.
scala> List(1, 2, 3) traverseU (f &&& g)
res2: (Int, List[List[Int]]) = (9,List(List(1, 5), List(2, 5), List(3, 5)))
We get different results. I am guessing this is why Product and Composition were added.
It seems like we have a solution, but not quite. The private ProductApplicative is a typeclass instance, but it doesn't have a way of retaining itself as a value because it uses pair to hold the data as Applicative[({type λ[α] = (F[α], G[α])})#λ]. So we have half of ⊠, but we are missing the (⊗) operator.
Here's the from current [WorldCount][1] example:
// To count, we traverse with a function returning 0 or 1, and sum the results
// with Monoid[Int], packaged in a constant monoidal applicative functor.
val Count = Monoid[Int].applicative
// Compose the applicative instance for [a]State[Int,a] with the Count applicative
val WordCount = StateT.stateMonad[Int].compose[({type λ[α] = Int})#λ](Count)
// Fuse the three applicatives together in parallel...
val A = Count
.product[({type λ[α] = Int})#λ](Count)
.product[({type λ[α] = State[Int, Int]})#λ](WordCount)
// ... and execute them in a single traversal
val ((charCount, lineCount), wordCountState) = A.traverse(text)((c: Char) => ((1, test(c == '\n')), atWordStart(c)))
First, the typeclass instances are composed, then implementation is manually put into pairs again.
The main problem I think is coming from the fact that there's no data type that's holding on to the typeclass instanes. Jason said this predates Unapply, so we can get some help from it too.
Similar to Kleisli arrows for monads, I've defined a data type called [AppFunc][2] in my topic/appfunc branch:
trait AppFunc[F[_], A, B] { self =>
def runA(a: A): F[B]
implicit def F: Applicative[F]
...
}
In there I define @>>>, <<<@, @&&&, and &&&@ operators that does the applicative composition. Let's see the f and g example first:
scala> val f = AppFuncU { (x: Int) => x + 1 }
f: scalaz.AppFunc[scalaz.Unapply[scalaz.Applicative,Int]{type M[X] = Int; type A = Int}#M,Int,Int] = scalaz.AppFuncFunctions$$anon$8@2ad4a8e3
scala> val g = AppFuncU { (x: Int) => List(x, 5) }
g: scalaz.AppFunc[scalaz.Unapply[scalaz.Applicative,List[Int]]{type M[X] = List[X]; type A = Int}#M,Int,Int] = scalaz.AppFuncFunctions$$anon$8@51cd32b5
scala> (f @&&& g) traverse List(1, 2, 3)
res3: (scalaz.Unapply[scalaz.Applicative,Int]{type M[X] = Int; type A = Int}#M[List[Int]], scalaz.Unapply[scalaz.Applicative,List[Int]]{type M[X] = List[X]; type A = Int}#M[List[Int]]) = (9,List(List(1, 2, 3), List(1, 2, 5), List(1, 5, 3), List(1, 5, 5), List(5, 2, 3), List(5, 2, 5), List(5, 5, 3), List(5, 5, 5)))
Now we get the same result as traversing f and g independently. Thanks to Unapply, no type annotations here either. And here's my rewrite of the [WordCount][3]:
// To count characters, treat Int as monoidal applicative
val countChar = AppFuncU { (c: Char) => 1 }
// To count lines, treat Int as monoidal applicative
val countLine = AppFuncU { (c: Char) => test(c === '\n') }
// To count words, we need to detect transitions from whitespace to non-whitespace.
val countWord = AppFuncU { (c: Char) =>
for {
x <- get[Boolean]
val y = c =/= ' '
_ <- put(y)
} yield test(y && !x)
} @>>> AppFuncU { (x: Int) => x }
// Compose applicative functions in parallel
val countAll = countWord @&&& countLine @&&& countChar
// ... and execute them in a single traversal
val ((wordCountState, lineCount), charCount) = countAll traverse text
As you can see countAll, the composition is done once at the value level.
The type inference works for @&&& as long as the applicative function on the rhs are simple enough.
So, the above example works, but it's not perfect. Thoughts?
-eugene