Skip to content
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 5 commits into from
Sep 29, 2017
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions docs/src/main/resources/microsite/data/menu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ options:
url: typeclasses/show.html
menu_type: typeclasses

- title: Arrow
url: typeclasses/arrow.html
menu_type: typeclasses

###########################
# Data Types Menu Options #
###########################
Expand Down
118 changes: 118 additions & 0 deletions docs/src/main/tut/typeclasses/arrow.md
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.
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.


To create an `Arrow` instance for a type `F[A, B]`, the following abstract methods need to be implemented:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It might be helpful to show a few examples of using Arrow methods on instances that already exist (such as Function1 before diving into what's needed to create your own custom Arrow instance. It's very rare that you'll be creating your own custom Arrow instance, but more common that people will want to take advantage of some of the methods on existing types.


```
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.
Copy link
Contributor

Choose a reason for hiding this comment

The 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))
```