20
votes

In Integrating State with Either (slide 88), given the pattern of State layered under Either, is there a recommended approach for adding another type of state, e.g., logging via something like Writer? It seems the new state has to live between the existing State and Either in order to take advantage of the fail-fast behavior of Either in flatMap.

Below is a runnable example of the code from the presentation, adjusted to work on 2.11.8 with Scalaz 7.2.8. Is there an approach that can cleanly add the new monad transformer on top of the existing behavior, simplifying refactoring? Stacking StateT in Scalaz applies to stacking but doesn't deal with the ordering problem created by the fail-fast flatMap behavior of Either.

// Based on slide 88+ in https://speakerdeck.com/mpilquist/scalaz-state-monad
// Adjusted for Scala 2.11 (invariant A), Scalaz 7.2 (Pointed->Applicative) and Throwable on lhs of Either
object IntegratingStateAndEither {
  import scalaz._
  import scalaz.Scalaz._
  import EitherT._
  import scalaz.StateT.stateMonad

  type QueryStateS[A] = State[QueryState, A]
  type ET[F[_], A] = EitherT[F, Throwable, A]
  type QueryStateES[A] = ET[QueryStateS, A]

  object QueryStateES {
    def apply[A](s: QueryStateS[Throwable \/ A]): QueryStateES[A] = EitherT(s)
    def liftE[A](e: Throwable \/ A): QueryStateES[A] = apply(Applicative[QueryStateS].point(e))
    def liftS[A](s: QueryStateS[A]): QueryStateES[A] = MonadTrans[ET].liftM(s)
  }

  def runQuery(s: String, m: Model): QueryStateES[QueryResult] = for {
    query <- parseQuery(s)
    res <- performQuery(query, m)
  } yield res

  def parseQuery(s: String): QueryStateES[StatsQuery] =
    QueryStateES.liftE(new Exception("TODO parse").left)

  def performQuery(q: StatsQuery, m: Model): QueryStateES[QueryResult] =
    QueryStateES.liftE(new Exception("TODO perform").left)

  // Just examples that do nothing
  case class Model()
  case class StatsQuery()
  case class QueryResult()
  case class QueryState()

  def test = runQuery("a + b", Model()).run.run(QueryState())
}
1

1 Answers

1
votes

To answer your specific example about logging you could do something like this:

object LayeringReaderWriterStateWithEither {
  // Based on slide 88+ in https://speakerdeck.com/mpilquist/scalaz-state-monad
  // Adjusted for Scala 2.11 (invariant A), Scalaz 7.2 (Pointed->Applicative) and Throwable on lhs of Either
  object IntegratingStateAndEither {
    import scalaz._
    import scalaz.Scalaz._
    import EitherT._

    type QueryStateS[A] = ReaderWriterState[List[String], String, QueryState, A]
    type ET[F[_], A] = EitherT[F, Throwable, A]
    type QueryStateES[A] = ET[QueryStateS, A]

    object QueryStateES {
      def apply[A](s: QueryStateS[Throwable \/ A]): QueryStateES[A] = EitherT(s)
      def liftE[A](e: Throwable \/ A): QueryStateES[A] = apply(Applicative[QueryStateS].point(e))
      def liftS[A](s: QueryStateS[A]): QueryStateES[A] = MonadTrans[ET].liftM(s)
      def log(msg: String): QueryStateES[Unit] = liftS {
        ReaderWriterState[List[String], String, QueryState, Unit] {
          case (r, s) => (msg.format(r, s), (), s).point[Id]
        }
      }
    }

    def runQuery(s: String, m: Model): QueryStateES[QueryResult] = for {
      _ ← log("Starting")
      query <- parseQuery(s)
      _ ← log(s"Got a query: $query")
      res <- performQuery(query, m)
    } yield res

    def log(msg: String): QueryStateES[Unit] =
      QueryStateES.log(msg)

    def parseQuery(s: String): QueryStateES[StatsQuery] =
      QueryStateES.liftE(new Exception(s"TODO parse $s").left)

    def performQuery(q: StatsQuery, m: Model): QueryStateES[QueryResult] =
      QueryStateES.liftE(new Exception(s"TODO perform $q in $m").left)

    // Just examples that do nothing
    case class Model()
    case class StatsQuery()
    case class QueryResult()
    case class QueryState()

    def test = runQuery("a + b", Model()).run.run(Nil, QueryState())
  }
}