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 StateT and IndexedStateT #1970

Merged
merged 1 commit into from
Oct 15, 2017
Merged
Changes from all 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
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.