-
-
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
Add docs for Arrow #1924
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,118 @@ | ||
--- | ||
layout: docs | ||
title: "Arrow" | ||
section: "typeclasses" | ||
source: "core/src/main/scala/cats/arrow/Arrow.scala" | ||
scaladoc: "#cats.arrow.Arrow" | ||
--- | ||
# Arrow | ||
|
||
`Arrow` is a useful type class for modeling something that behaves like functions, such as `Function1`: `A => B`, `Kleisli`: `A => F[B]` (also known as `ReaderT`), `Cokleisli`: `F[A] => B`, etc. So useful, that Haskell provides special syntax (the `proc` notation) for composing and combining arrows, similar to the `do` notation for sequencing monadic operations. | ||
|
||
To create an `Arrow` instance for a type `F[A, B]`, the following abstract methods need to be implemented: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It might be helpful to show a few examples of using |
||
|
||
``` | ||
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)] | ||
``` | ||
|
||
Once a type `F` has an `Arrow` instance, it gets for free a number of methods for composing and combining multiple `Arrow`s. You will be able to do things like: | ||
- Composing `fab: F[A, B]` and `fbc: F[B, C]` into `fac: F[A, C]` with `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 *** fbc`. The resulting arrow takes two inputs and processes them with two arrows, one for each input. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is related to the previous comment, but I think that some concrete examples might help these explanations. |
||
- Taking two arrows `fab: F[A, B]` and `fac: F[A, C]` and combining them into `F[A => (B, C)]` with `fab &&& fbc`. The resulting arrow takes an input, duplicates it and processes each copy with a different arrow. | ||
- Taking an arrow `fab: F[A, B]` and turning it into `F[(C, A), (C, B)]` with `fab.second`. The resulting arrow takes two inputs, processes the second input and leaves the first input as it is. | ||
|
||
## Example | ||
|
||
As an example, let's 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. For example, 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. | ||
|
||
|
||
```tut:book:silent | ||
import cats.arrow.Arrow | ||
|
||
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) | ||
} | ||
``` | ||
|
||
Now we can try it out: | ||
|
||
```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.implicits._ | ||
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 | ||
``` | ||
|
||
Then we define a `combine` function that takes an input and processes two copies of it with two arrows, and finally, the `avg` arrow in terms of the arrows we have so far: | ||
|
||
```tut:book:silent | ||
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) | ||
|
||
def avg: FancyFunction[Int, Double] = | ||
combine(sum[Int], count[Int]) >>> Arrow[FancyFunction].lift{case (x, y) => x.toDouble / y} | ||
``` | ||
|
||
And try it out: | ||
|
||
```tut:book | ||
runList(avg, List(1, 10, 100, 1000)) | ||
``` |
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.
I think that we should avoid bringing Haskell into the explanation this early in the docs (if at all). I know that for people who are interested in functional programming, Haskell is the go-to language to describe concepts, but in my general experience people who already know haskell will already be familiar with concepts such as arrows, and mentioning Haskell won't be helpful for people who don't already know it.
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.
Good point.