-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add docs for Arrow #1924
Merged
Merged
Add docs for Arrow #1924
Changes from 3 commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 something that behaves like functions. For example, `scala.Function1`(`A => B`), `cats.data.Kleisli`(wrapping an `A => F[B]`, also known as `ReaderT`), and `cats.data.Cokleisli`(wrapping an `F[A] => B`), have `Arrow` instances. | ||
|
||
Having an `Arrow` instance for a type `F[_, _]` means it supports a number of methods for composing and combining multiple `Arrow`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 `expAndVar`, that takes a `List[Int]` and returns the pair of expected value and variance. To do so, we first define a `combine` function that takes an input and processes two copies of it with two arrows, using `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 `expected: List[Int] => Double`, `variance: List[Int] => Double` and `expAndVar: List[Int] => (Double, Double)` using the `combine` method and `Arrow` operations: | ||
|
||
```tut:book:silent | ||
val expected: 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)) >>> expected, expected) >>> {case (x, y) => x - y * y} | ||
|
||
val expAndVar: List[Int] => (Double, Double) = combine(expected, variance) | ||
``` | ||
|
||
```tut:book | ||
expAndVar(List(1, 2, 3, 4)) | ||
``` | ||
|
||
Of course, a more natural way to implement `expected` and `variance` would be: | ||
|
||
```tut:book:silent | ||
val expected2: List[Int] => Double = xs => xs.sum.toDouble / xs.size | ||
|
||
val variance2: List[Int] => Double = xs => expected2(xs.map(x => x * x)) - scala.math.pow(expected2(xs), 2.0) | ||
``` | ||
|
||
However, `Arrow` methods are more general and provide a common structure for types 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]` 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`, 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)) | ||
``` |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry I am late in the game. This is kind of a nit but I am not so sure about this very first sentence.
I kind of feel that this is not the original intention of the type class.
Arrow
is, well, arrow in category theory. function is just one of the possible definitions. So a more precise way to put it might beWDYT?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sgtm. Thanks for the correction.