-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* Add docs for Arrow * Address comments * Address more comments * Repharse first paragraph * Minor rewording of the examples
- Loading branch information
Showing
2 changed files
with
183 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,179 @@ | ||
--- | ||
layout: docs | ||
title: "Arrow" | ||
section: "typeclasses" | ||
source: "core/src/main/scala/cats/arrow/Arrow.scala" | ||
scaladoc: "#cats.arrow.Arrow" | ||
--- | ||
# Arrow | ||
|
||
`Arrow` is a type class for modeling composable relationships between two types. One example of such a composable relationship is function `A => B`; other examples include `cats.data.Kleisli`(wrapping an `A => F[B]`, also known as `ReaderT`), and `cats.data.Cokleisli`(wrapping an `F[A] => B`). These type constructors all have `Arrow` instances. An arrow `F[A, B]` can be thought of as representing a computation from `A` to `B` with some context, just like a functor/applicative/monad `F[A]` represents a value `A` with some context. | ||
|
||
Having an `Arrow` instance for a type constructor `F[_, _]` means that an `F[_, _]` can be composed and combined with other `F[_, _]`s. You will be able to do things like: | ||
- Lifting a function `ab: A => B` into arrow `F[A, B]` with `Arrow[F].lift(ab)`. If `F` is `Function1` then `A => B` is the same as `F[A, B]` so `lift` is just the identity function. | ||
- Composing `fab: F[A, B]` and `fbc: F[B, C]` into `fac: F[A, C]` with `Arrow[F].compose(fbc, fab)`, or `fab >>> fbc`. If `F` is `Function1` then `>>>` becomes an alias for `andThen`. | ||
- Taking two arrows `fab: F[A, B]` and `fbc: F[B, C]` and combining them into `F[(A, C) => (B, D)]` with `fab.split(fbc)` or `fab *** fbc`. The resulting arrow takes two inputs and processes them with two arrows, one for each input. | ||
- Taking an arrow `fab: F[A, B]` and turning it into `F[(A, C), (B, C)]` with `fab.first`. The resulting arrow takes two inputs, processes the first input and leaves the second input as it is. A similar method, `fab.second`, turns `F[A, B]` into `F[(C, A), (B, A)]`. | ||
|
||
## Examples | ||
|
||
### `Function1` | ||
|
||
`scala.Function1` has an `Arrow` instance, so you can use all the above methods on `Function1`. The Scala standard library has the `compose` and `andThen` methods for composing `Function1`s, but the `Arrow` instance offers more powerful options. | ||
|
||
Suppose we want to write a function `meanAndVar`, that takes a `List[Int]` and returns the pair of mean and variance. To do so, we first define a `combine` function that combines two arrows into a single arrow, which takes an input and processes two copies of it with two arrows. `combine` can be defined in terms of `Arrow` operations `lift`, `>>>` and `***`: | ||
|
||
```tut:book:silent | ||
import cats.arrow.Arrow | ||
import cats.implicits._ | ||
def combine[F[_, _]: Arrow, A, B, C](fab: F[A, B], fac: F[A, C]): F[A, (B, C)] = | ||
Arrow[F].lift((a: A) => (a, a)) >>> (fab *** fac) | ||
``` | ||
|
||
We can then create functions `mean: List[Int] => Double`, `variance: List[Int] => Double` and `meanAndVar: List[Int] => (Double, Double)` using the `combine` method and `Arrow` operations: | ||
|
||
```tut:book:silent | ||
val mean: List[Int] => Double = | ||
combine((_: List[Int]).sum, (_: List[Int]).size) >>> {case (x, y) => x.toDouble / y} | ||
val variance: List[Int] => Double = | ||
// Variance is mean of square minus square of mean | ||
combine(((_: List[Int]).map(x => x * x)) >>> mean, mean) >>> {case (x, y) => x - y * y} | ||
val meanAndVar: List[Int] => (Double, Double) = combine(mean, variance) | ||
``` | ||
|
||
```tut:book | ||
meanAndVar(List(1, 2, 3, 4)) | ||
``` | ||
|
||
Of course, a more natural way to implement `mean` and `variance` would be: | ||
|
||
```tut:book:silent | ||
val mean2: List[Int] => Double = xs => xs.sum.toDouble / xs.size | ||
val variance2: List[Int] => Double = xs => mean2(xs.map(x => x * x)) - scala.math.pow(mean2(xs), 2.0) | ||
``` | ||
|
||
However, `Arrow` methods are more general and provide a common structure for type constructors that have `Arrow` instances. They are also a more abstract way of stitching computations together. | ||
|
||
|
||
### `Kleisli` | ||
|
||
A `Kleisli[F[_], A, B]` represents a function `A => F[B]`. You cannot directly compose an `A => F[B]` with a `B => F[C]` with functional composition, since the codomain of the first function is `F[B]` while the domain of the second function is `B`; however, since `Kleisli` is an arrow (as long as `F` is a monad), you can easily compose `Kleisli[F[_], A, B]` with `Kleisli[F[_], B, C]` using `Arrow` operations. | ||
|
||
|
||
Suppose you want to take a `List[Int]`, and return the sum of the first and the last element (if exists). To do so, we can create two `Kleisli`s that find the `headOption` and `lastOption` of a `List[Int]`, respectively: | ||
|
||
```tut:book:silent | ||
import cats.data.Kleisli | ||
val headK = Kleisli((_: List[Int]).headOption) | ||
val lastK = Kleisli((_: List[Int]).lastOption) | ||
``` | ||
|
||
With `headK` and `lastK`, we can obtain the `Kleisli` arrow we want by combining them, and composing it with `_ + _`: | ||
|
||
```tut:book:silent | ||
val headPlusLast = combine(headK, lastK) >>> Arrow[Kleisli[Option, ?, ?]].lift(((_: Int) + (_: Int)).tupled) | ||
``` | ||
|
||
```tut:book | ||
headPlusLast.run(List(2, 3, 5, 8)) | ||
headPlusLast.run(Nil) | ||
``` | ||
|
||
### `FancyFunction` | ||
|
||
In this example let's create our own `Arrow`. We shall create a fancy version of `Function1` called `FancyFunction`, that is capable of maintaining states. We then create an `Arrow` instance for `FancyFunction` and use it to compute the moving average of a list of numbers. | ||
|
||
```tut:book:silent | ||
case class FancyFunction[A, B](run: A => (FancyFunction[A, B], B)) | ||
``` | ||
|
||
That is, given an `A`, it not only returns a `B`, but also returns a new `FancyFunction[A, B]`. This sounds similar to the `State` monad (which returns a result and a new `State` from an initial `State`), and indeed, `FancyFunction` can be used to perform stateful transformations. | ||
|
||
To run a stateful computation using a `FancyFunction` on a list of inputs, and collect the output into another list, we can define the following `runList` helper function: | ||
|
||
```tut:book:silent | ||
def runList[A, B](ff: FancyFunction[A, B], as: List[A]): List[B] = as match { | ||
case h :: t => | ||
val (ff2, b) = ff.run(h) | ||
b :: runList(ff2, t) | ||
case _ => List() | ||
} | ||
``` | ||
|
||
In `runList`, the head element in `List[A]` is fed to `ff`, and each subsequent element is fed to a `FancyFunction` which is generated by running the previous `FancyFunction` on the previous element. If we have an `as: List[Int]`, and an `avg: FancyFunction[Int, Double]` which takes an integer and computes the average of all integers it has seen so far, we can call `runList(avg, as)` to get the list of moving average of `as`. | ||
|
||
Next let's create an `Arrow` instance for `FancyFunction` and see how to implement the `avg` arrow. To create an `Arrow` instance for a type `F[A, B]`, the following abstract methods need to be implemented: | ||
|
||
``` | ||
def lift[A, B](f: A => B): F[A, B] | ||
def id[A]: F[A, A] | ||
def compose[A, B, C](f: F[B, C], g: F[A, B]): F[A, C] | ||
def first[A, B, C](fa: F[A, B]): F[(A, C), (B, C)] | ||
``` | ||
|
||
Thus the `Arrow` instance for `FancyFunction` would be: | ||
|
||
|
||
```tut:book:silent | ||
implicit val arrowInstance: Arrow[FancyFunction] = new Arrow[FancyFunction] { | ||
override def lift[A, B](f: A => B): FancyFunction[A, B] = FancyFunction(lift(f) -> f(_)) | ||
override def first[A, B, C](fa: FancyFunction[A, B]): FancyFunction[(A, C), (B, C)] = FancyFunction {case (a, c) => | ||
val (fa2, b) = fa.run(a) | ||
(first(fa2), (b, c)) | ||
} | ||
override def id[A]: FancyFunction[A, A] = FancyFunction(id -> _) | ||
override def compose[A, B, C](f: FancyFunction[B, C], g: FancyFunction[A, B]): FancyFunction[A, C] = FancyFunction {a => | ||
val (gg, b) = g.run(a) | ||
val (ff, c) = f.run(b) | ||
(compose(ff, gg), c) | ||
} | ||
} | ||
``` | ||
|
||
Once we have an `Arrow[FancyFunction]`, we can start to do interesting things with it. First, let's create a method `accum` that returns a `FancyFunction`, which accumulates values fed to it using the accumulation function `f` and the starting value `b`: | ||
|
||
```tut:book:silent | ||
def accum[A, B](b: B)(f: (A, B) => B): FancyFunction[A, B] = FancyFunction {a => | ||
val b2 = f(a, b) | ||
(accum(b2)(f), b2) | ||
} | ||
``` | ||
|
||
```tut:book | ||
runList(accum[Int, Int](0)(_ + _), List(6, 5, 4, 3, 2, 1)) | ||
``` | ||
|
||
To make the aformentioned `avg` arrow, we need to keep track of both the count and the sum of the numbers we have seen so far. To do so, we will combine several `FancyFunction`s to get the `avg` arrow we want. | ||
|
||
We first define arrow `sum` in terms of `accum`, and define arrow `count` by composing `_ => 1` with `sum`: | ||
|
||
```tut:book:silent | ||
import cats.kernel.Monoid | ||
def sum[A: Monoid]: FancyFunction[A, A] = accum(Monoid[A].empty)(_ |+| _) | ||
def count[A]: FancyFunction[A, Int] = Arrow[FancyFunction].lift((_: A) => 1) >>> sum | ||
``` | ||
|
||
Finally, we create the `avg` arrow in terms of the arrows we have so far: | ||
|
||
```tut:book:silent | ||
def avg: FancyFunction[Int, Double] = | ||
combine(sum[Int], count[Int]) >>> Arrow[FancyFunction].lift{case (x, y) => x.toDouble / y} | ||
``` | ||
|
||
```tut:book | ||
runList(avg, List(1, 10, 100, 1000)) | ||
``` |