Skip to content

Commit

Permalink
Merge branch 'master' into add-sorted-instances
Browse files Browse the repository at this point in the history
  • Loading branch information
Luka Jacobowitz committed Oct 16, 2017
2 parents 2ec0b0a + 167d5b4 commit fef41c1
Show file tree
Hide file tree
Showing 39 changed files with 275 additions and 143 deletions.
113 changes: 111 additions & 2 deletions docs/src/main/tut/datatypes/state.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,115 @@ val robot = createRobot.runA(initialSeed).value

This may seem surprising, but keep in mind that `b` isn't simply a `Boolean`. It is a function that takes a seed and _returns_ a `Boolean`, threading state along the way. Since the seed that is being passed into `b` changes from line to line, so do the returned `Boolean` values.

## Fine print
## Interleaving effects

TODO explain StateT and the fact that State is an alias for StateT with Eval.
Let's expand on the above example; assume that our random number generator works asynchronously by fetching results from a remote server:
```tut:book
import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits.global
final case class AsyncSeed(long: Long) {
def next = Future(AsyncSeed(long * 6364136223846793005L + 1442695040888963407L))
}
```

Using a proper `IO` data type would be ideal, but `Future` serves our purpose here.

We now need to redefine our `nextLong` action:
```tut:book:fail
val nextLong: State[AsyncSeed, Future[Long]] = State { seed =>
(seed.next, seed.long)
}
```

Oops! That doesn't work: `State[S, A]` requires that we return a pure next `S` in the `State.apply` constructor. We could define `nextLong` as `State[Future[AsyncSeed], Future[Long]]`, but that would get us into even more trouble:
```tut:book:fail
val nextLong: State[Future[AsyncSeed], Future[Long]] = State { seedF =>
seedF.map { seed =>
(seed.next, seed.long)
}
}
```

The `seed` that `State.apply` passes in is now a `Future`, so we must map it. But we can't return a `Future[(Future[S], [A])]`; so what do we do?

Luckily, `State[S, A]` is an alias for `StateT[Eval, S, A]` - a monad transformer defined as `StateT[F[_], S, A]`. This data type represents computations of the form `S => F[(S, A)]`.

If we plug in our concrete types, we get `AsyncSeed => Future[(AsyncSeed, A)]`, which is something we can work with:
```tut:book
import cats.data.StateT
import cats.instances.future._
import scala.concurrent.ExecutionContext.Implicits.global
val nextLong: StateT[Future, AsyncSeed, Long] = StateT { seed =>
seed.next zip Future.successful(seed.long)
}
```

Now, what do we get back if we invoke `run` on our `nextLong` action?
```tut:book
nextLong.run(AsyncSeed(0))
```

Since every intermediate computation returns a `Future`, the composite computation returns a `Future` as well. To summarize, `StateT[F[_], S, A]` allows us to interleave effects of type `F[_]` in the computations wrapped by it.

It should be noted that different combinators on `StateT` impose different constraints on `F`; for example, `map` only requires that `F` has a `Functor` instance, but `flatMap` naturally requires `F` to have a `FlatMap` instance. Have a look at the method signatures for the details.

## Changing States

More complex, stateful computations cannot be easily modeled by a single type. For example, let's try to model a door's state:
```tut:book
sealed trait DoorState
case object Open extends DoorState
case object Closed extends DoorState
case class Door(state: DoorState)
def open: State[DoorState, Unit] = ???
def close: State[DoorState, Unit] = ???
```

We would naturally expect that `open` would only operate on doors that are `Closed`, and vice-versa. However, to implement these methods currently, we have to pattern-match on the state:
```tut:book
def open: State[DoorState, Unit] = State { doorState =>
doorState match {
case Closed => (Open, ())
case Open => ??? // What now?
}
}
```

This puts us in the awkward situation of deciding how to handle invalid states; what's the appropriate behavior? Should we just leave the door open? Should we return a failure? That would mean that our return type needs to be modified.

The most elegant solution would be to model this requirement statically using types, and luckily, `StateT` is an alias for another type: `IndexedStateT[F[_], SA, SB, A]`.

This data type models a stateful computation of the form `SA => F[(SB, A)]`; that's a function that receives an initial state of type `SA` and results in a state of type `SB` and a result of type `A`, using an effect of `F`.

So, let's model our typed door:
```tut:book
import cats.Eval
import cats.data.IndexedStateT
def open: IndexedStateT[Eval, Open.type, Closed.type, Unit] = IndexedStateT.set(Closed)
def close: IndexedStateT[Eval, Closed.type, Open.type, Unit] = IndexedStateT.set(Open)
```

We can now reject, at compile time, sequences of `open` and `close` that are invalid:
```tut:book:fail
val invalid = for {
_ <- open
_ <- close
_ <- close
} yield ()
```

While valid sequences will be accepted:
```tut:book
val valid = for {
_ <- open
_ <- close
_ <- open
} yield ()
```

Note that the inferred type of `valid` correctly models that this computation can be executed only with an initial `Closed` state.
4 changes: 2 additions & 2 deletions docs/src/main/tut/typeclasses/lawtesting.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,10 @@ Then we can again test the instance inside our class extending `CatsSuite`:

```tut:book
import cats.laws.discipline.FunctorTests
import cats.kernel.laws.discipline.SemigroupLawTests
import cats.kernel.laws.discipline.SemigroupTests
class TreeLawTests extends CatsSuite {
checkAll("Tree[Int].SemigroupLaws", SemigroupLawTests[Tree[Int]].semigroup)
checkAll("Tree[Int].SemigroupLaws", SemigroupTests[Tree[Int]].semigroup)
checkAll("Tree.FunctorLaws", FunctorTests[Tree].functor[Int, Int, String])
}
```
2 changes: 1 addition & 1 deletion js/src/test/scala/cats/tests/FutureTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cats
package js
package tests

import cats.kernel.laws.discipline.{MonoidLawTests, SemigroupLawTests}
import cats.kernel.laws.discipline.{MonoidTests => MonoidLawTests, SemigroupTests => SemigroupLawTests}
import cats.laws.discipline._
import cats.js.instances.Await
import cats.js.instances.future.futureComonad
Expand Down
2 changes: 1 addition & 1 deletion jvm/src/test/scala/cats/tests/FutureTests.scala
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package cats
package jvm
package tests

import cats.kernel.laws.discipline.{MonoidLawTests, SemigroupLawTests}
import cats.kernel.laws.discipline.{MonoidTests => MonoidLawTests, SemigroupTests => SemigroupLawTests}
import cats.laws.discipline._
import cats.laws.discipline.arbitrary._
import cats.tests.{CatsSuite, ListWrapper}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package discipline
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

trait BandTests[A] extends SemigroupLawTests[A] {
trait BandTests[A] extends SemigroupTests[A] {

def laws: BandLaws[A]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package discipline

import org.scalacheck.{Arbitrary, Prop}

trait CommutativeGroupTests[A] extends CommutativeMonoidTests[A] with GroupLawTests[A] {
trait CommutativeGroupTests[A] extends CommutativeMonoidTests[A] with GroupTests[A] {
def laws: CommutativeGroupLaws[A]

def commutativeGroup(implicit arbA: Arbitrary[A], eqA: Eq[A]): RuleSet =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ package discipline

import org.scalacheck.{Arbitrary, Prop}

trait CommutativeMonoidTests[A] extends CommutativeSemigroupTests[A] with MonoidLawTests[A] {
trait CommutativeMonoidTests[A] extends CommutativeSemigroupTests[A] with MonoidTests[A] {
def laws: CommutativeMonoidLaws[A]

def commutativeMonoid(implicit arbA: Arbitrary[A], eqA: Eq[A]): RuleSet =
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package discipline
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

trait CommutativeSemigroupTests[A] extends SemigroupLawTests[A] {
trait CommutativeSemigroupTests[A] extends SemigroupTests[A] {

def laws: CommutativeSemigroupLaws[A]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll
import org.typelevel.discipline.Laws

trait EqLawTests[A] extends Laws {
trait EqTests[A] extends Laws {
def laws: EqLaws[A]

def eqv(implicit arbA: Arbitrary[A], arbF: Arbitrary[A => A], eqA: Eq[A]): RuleSet =
Expand All @@ -21,8 +21,8 @@ trait EqLawTests[A] extends Laws {
"eq transitivity" -> forAll(laws.transitivityEq _))
}

object EqLawTests {
def apply[A: Eq]: EqLawTests[A] =
new EqLawTests[A] { def laws: EqLaws[A] = EqLaws[A] }
object EqTests {
def apply[A: Eq]: EqTests[A] =
new EqTests[A] { def laws: EqLaws[A] = EqLaws[A] }
}

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ package discipline
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

trait GroupLawTests[A] extends MonoidLawTests[A] {
trait GroupTests[A] extends MonoidTests[A] {

def laws: GroupLaws[A]

Expand All @@ -20,7 +20,7 @@ trait GroupLawTests[A] extends MonoidLawTests[A] {

}

object GroupLawTests {
def apply[A: Group]: GroupLawTests[A] =
new GroupLawTests[A] { def laws: GroupLaws[A] = GroupLaws[A] }
object GroupTests {
def apply[A: Group]: GroupTests[A] =
new GroupTests[A] { def laws: GroupLaws[A] = GroupLaws[A] }
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import org.scalacheck.Prop.forAll

import scala.util.hashing.Hashing

trait HashTests[A] extends EqLawTests[A] {
trait HashTests[A] extends EqTests[A] {

def laws: HashLaws[A]

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cats.kernel.instances.boolean._
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

trait MonoidLawTests[A] extends SemigroupLawTests[A] {
trait MonoidTests[A] extends SemigroupTests[A] {

def laws: MonoidLaws[A]

Expand All @@ -24,7 +24,7 @@ trait MonoidLawTests[A] extends SemigroupLawTests[A] {

}

object MonoidLawTests {
def apply[A: Monoid]: MonoidLawTests[A] =
new MonoidLawTests[A] { def laws: MonoidLaws[A] = MonoidLaws[A] }
object MonoidTests {
def apply[A: Monoid]: MonoidTests[A] =
new MonoidTests[A] { def laws: MonoidLaws[A] = MonoidLaws[A] }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cats.kernel.instances.boolean._
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

trait OrderLawTests[A] extends PartialOrderLawTests[A] {
trait OrderTests[A] extends PartialOrderTests[A] {

def laws: OrderLaws[A]

Expand All @@ -23,7 +23,7 @@ trait OrderLawTests[A] extends PartialOrderLawTests[A] {

}

object OrderLawTests {
def apply[A: Order]: OrderLawTests[A] =
new OrderLawTests[A] { def laws: OrderLaws[A] = OrderLaws[A] }
object OrderTests {
def apply[A: Order]: OrderTests[A] =
new OrderTests[A] { def laws: OrderLaws[A] = OrderLaws[A] }
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import cats.kernel.instances.boolean._
import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll

trait PartialOrderLawTests[A] extends EqLawTests[A] {
trait PartialOrderTests[A] extends EqTests[A] {

def laws: PartialOrderLaws[A]

Expand All @@ -29,7 +29,7 @@ trait PartialOrderLawTests[A] extends EqLawTests[A] {

}

object PartialOrderLawTests {
def apply[A: PartialOrder]: PartialOrderLawTests[A] =
new PartialOrderLawTests[A] { def laws: PartialOrderLaws[A] = PartialOrderLaws[A] }
object PartialOrderTests {
def apply[A: PartialOrder]: PartialOrderTests[A] =
new PartialOrderTests[A] { def laws: PartialOrderLaws[A] = PartialOrderLaws[A] }
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import org.scalacheck.Arbitrary
import org.scalacheck.Prop.forAll
import org.typelevel.discipline.Laws

trait SemigroupLawTests[A] extends Laws {
trait SemigroupTests[A] extends Laws {
def laws: SemigroupLaws[A]

def semigroup(implicit arbA: Arbitrary[A], eqA: Eq[A]): RuleSet =
Expand All @@ -21,7 +21,7 @@ trait SemigroupLawTests[A] extends Laws {
"semigroup combineAllOption" -> forAll(laws.combineAllOption _))
}

object SemigroupLawTests {
def apply[A: Semigroup]: SemigroupLawTests[A] =
new SemigroupLawTests[A] { def laws: SemigroupLaws[A] = SemigroupLaws[A] }
object SemigroupTests {
def apply[A: Semigroup]: SemigroupTests[A] =
new SemigroupTests[A] { def laws: SemigroupLaws[A] = SemigroupLaws[A] }
}
Loading

0 comments on commit fef41c1

Please sign in to comment.