4. CATS….
To use cats effectively, understand what each
construct does:
Functor
Monads
Applicatives
Monoids
Semigroups
SO MANY ACRONYMS !!!
Validated
5.
6. Building blocks …
Understand that they are building blocks
so that you can write code that is pure and
code that has side-effects — separation of
concerns.
7. Typeclasses …
Each of the type class (e.g. functors,
monoids, monads etc) are governed by laws.
Typeclasses!
they are behaviours that can be “inherited”
by your code.
8. Semigroups - what are they?
trait Semigroup[A] {
def combine(x: A, y: A) : A
}
general structure to define things
that can be combined.
*Cats provides “default” implementations; developers
(like you & me) need to provide implementations that conform to the traits. *
9. Monoids - what are they?
trait Monoid[A] extends Semigroup[A] {
def empty: A
def combine(x: A, y: A) : A
}
general structure to define things
that can be combined and has a “default”
element.
*Cats provides “default” implementations; developers
(like you & me) need to provide implementations that conform to the traits. *
11. Use case for Monoids/Semigroups
They’re good for combining 2 or more things of a similar
nature
data-type-a data-type-b
data-stream end-
point
parser
collector
of either data-type-a or
data-type-b
12. Use case #1 - Monoids for “smashing” values
* all names used here do not reflect the actuals *
// Monoid[DataTypeAB] defined somewhere else
def buildDataFromStream(datatypeA : DataTypeA,
datatypeB : DataTypeB,
accumulator: DatatypeAB) =
validateData(datatypeA, datatypeB).fold(
onError => {
// `orError` is lifted into the datatype
val errors = Monoid[DatatypeAB].empty.copy(lift(onError))
Monoid[DatatypeAB].combine(accumulator, errors)
},
parsedValue => {
// `parsedValue` is lifted into the datatype
val newValue = Monoid[DatatypeAB].empty.copy(lift(parsedValue))
Monoid[DatatypeAB].combine(accumulator, newValue)
}
)
13. Functors - what are they?
trait Functor[F[_]] {
def map[A,B](fa: F[A])(f: A => B) : F[B]
}
general structure to represent something
that can be mapped over. If you’ve been using Lists
, Options, Eithers, Futures in Scala, you’ve been using
functors.
!!! They are very common structures indeed ☺ !!!
* functors are used in clever things like recursion-schemes *
14. Functors - what are they?
> import cats._, data._, implicits._
> Functor[List].lift((x:Int) => x + 1)
// res0: List[Int] => List[Int]
> res0(List(1))
// res1: List[Int] = List(2)
* Nugget of info: Functors preserve “structure” *
18. Monads - a quick summary?
Writers - information can be carried along with
the computation
Readers - compose operations that depend
on some input.
State - allows state to be “propagated”
Eval - abstracts over eager vs lazy evaluation.
19. Monads - examples
> List(1,2,3) >>= (value => List(value+1))
> res0: List[Int] = List(2,3,4)
def >>=[A,B](fa: F[A])(f:A => F[B]): F[B] =
flatMap(fa)(f)
“>>=“ is also known as “bind” (in Cats, its really “flatMap”)
20. Monads - examples
> Monad[List].lift((x:Int) => x + 1)(List(1,2,3))
> res1: List[Int] = List(2,3,4)
Typeclasses allows you to define re-usable code by
lifting functions.
21. Monads - examples
> Monad[List].pure(4)
> res2: List[Int] = List(4)
This is to lift values into a context, in this case
Monadic context.
23. Writer Monad
“Writers” are typically used to carry not only the value
of a computation but also some other information (typically, its
used to carry logging info).
source: http://eed3si9n.com/herding-cats/Writer.html
25. Reader Monad
“Readers” allows us to compose operations which depend
on some input.
source: http://eed3si9n.com/herding-cats/Reader.html
26. Reader Monad
case class Config(setting: String, value: String)
def getSetting = Reader {
(config: Config) => config.setting
}
def getValue = Reader {
(config: Config) => config.value
}
for {
s <- getSetting
v <- getValue
} yield Config(s, v)
Compose Readers
FP-style to abstract and
encapsulate.
27. State Monad
Allows us to pass state-information around in a computation.
http://eed3si9n.com/herding-cats/State.html
28. Use case #3 - Reader + State Monad
def process: Reader[Elem, Seq[Mapping]] = Reader {
(xml: Elem) =>
for {
groups <- extractGroups(dataXml).toOption
group <- groups
grpCfg <- loadGroupConfig(group).toOption
stateObj <- ConfigState(grpCfg).pure[Option]
records <- loadRecords(group).toOption
record <- records
row <- processRecord(i)(stateObj)(record).pure[Option]
} yield {
// processing …
}
}
case class ConfigState(init: Config) {
private[this] var currentState: Config = init
def storeCfg : State[Config, Config] =
State{ (cfg: Config) =>
val prevState = currentState
currentState = cfg
(currentState, prevState) }
def loadCfg : Config =
( for {
s <- State.get[Config]
} yield s ).runA(currentState).value
}
29. Use case #3 - Reader + State Monad
def process: Reader[Elem, Seq[Mapping]] = Reader {
(xml: Elem) =>
for {
groups <- extractGroups(dataXml).toOption
group <- groups
grpCfg <- loadGroupConfig(group).toOption
stateObj <- ConfigState(grpCfg).pure[Option]
records <- loadRecords(group).toOption
record <- records
row <- processRecord(i)(stateObj)(record).pure[Option]
} yield {
// processing …
}
}
case class ConfigState(init: Config) {
private[this] var currentState: Config = init
def storeCfg : State[Config, Config] =
State{ (cfg: Config) =>
val prevState = currentState
currentState = cfg
(currentState, prevState) }
def loadCfg : Config =
( for {
s <- State.get[Config]
} yield s ).runA(currentState).value
}
Separation of concerns
State management
30. Applicative
Applicatives allows for functions to be
lifted over a structure (Functor).
Because the function and the value it’s being applied
to both have structures, hence its needs to be
combined.
31. Applicative - examples
scala> Applicative[List].lift((x:Int) => x + 1)
res1: List[Int] => List[Int] = <function1>
scala> Applicative[List].lift(
| (x:List[Int=>Int]) =>
| x.map(f => f(2)))
| (List( List((x:Int) => x + 1 )))
res7: List[List[Int]] = List(List(3))
scala> val fs = List(List((x:Int) => x + 1))
fs: List[List[Int => Int]] = List(List(<function1>))
scala> fs.map(_(2))
res15: cats.data.Nested[List,List,Int] =
Nested(List(List(3)))
Applicative is like a Functor
32. Applicative - examples
scala> Applicative[List].lift((x:Int) => x + 1)
res1: List[Int] => List[Int] = <function1>
scala> Applicative[List].lift(
| (x:List[Int=>Int]) =>
| x.map(f => f(2)))
| (List( List((x:Int) => x + 1 )))
res7: List[List[Int]] = List(List(3))
scala> val fs = List(List((x:Int) => x + 1))
fs: List[List[Int => Int]] = List(List(<function1>))
scala> fs.map(_(2))
res15: cats.data.Nested[List,List,Int] =
Nested(List(List(3)))
Applicative is like a Functor
Applying a function which is nested.
33. Applicative - examples
scala> Applicative[List].lift((x:Int) => x + 1)
res1: List[Int] => List[Int] = <function1>
scala> Applicative[List].lift(
| (x:List[Int=>Int]) =>
| x.map(f => f(2)))
| (List( List((x:Int) => x + 1 )))
res7: List[List[Int]] = List(List(3))
scala> val fs = List(List((x:Int) => x + 1))
fs: List[List[Int => Int]] = List(List(<function1>))
scala> fs.map(_(2))
res15: cats.data.Nested[List,List,Int] =
Nested(List(List(3)))
Applicative is like a Functor
Applying a function which is nested.
Cat has a “Nested” to achieve the same.
34. Applicative - examples
A typical application is to leverage Applicatives in writing
Logic to validate configurations, forms etc
36. package xxx.config
import scala.concurrent.duration.{Duration,FiniteDuration}
import cats._
import cats.data._
import cats.implicits._
import cats.data.Validated
import cats.data.Validated.{Invalid, Valid}
// code that needs to remain hidden
sealed abstract class ConfigError
final case class MissingConfig(field : String) extends ConfigError
final case class ParseError(field: String) extends ConfigError
case class Config(map : Map[String,String])
case class HuffConfig(
clusterName: String,
clusterPort : Int,
clusterAddress : String,
hostname: String,
listeningPort: Int)
object Validator {
def getHuffConfig(config: Config) : ValidatedNel[ConfigError, HuffConfig] =
Apply[ValidatedNel[ConfigError, ?]].map5(
config.parse[String] ("DL_CLUSTER_NAME").toValidatedNel,
config.parse[Int] ("DL_CLUSTER_PORT").toValidatedNel,
config.parse[String] ("DL_CLUSTER_ADDRESS").toValidatedNel,
config.parse[String] ("DL_HTTP_ADDRESS").toValidatedNel,
config.parse[Int] ("DL_HTTP_PORT").toValidatedNel) {
case (clusterName, clusterPort, clusterAddress, httpAddr, httpPort) =>
HuffConfig(clusterName, clusterPort, clusterAddress, httpAddr, httpPort)
}
}
37. package xxx.config
import scala.concurrent.duration.{Duration,FiniteDuration}
import cats._
import cats.data._
import cats.implicits._
import cats.data.Validated
import cats.data.Validated.{Invalid, Valid}
// code that needs to remain hidden
sealed abstract class ConfigError
final case class MissingConfig(field : String) extends ConfigError
final case class ParseError(field: String) extends ConfigError
case class Config(map : Map[String,String])
case class HuffConfig(
clusterName: String,
clusterPort : Int,
clusterAddress : String,
hostname: String,
listeningPort: Int)
object Validator {
def getHuffConfig(config: Config) : ValidatedNel[ConfigError, HuffConfig] =
Apply[ValidatedNel[ConfigError, ?]].map5(
config.parse[String] ("DL_CLUSTER_NAME").toValidatedNel,
config.parse[Int] ("DL_CLUSTER_PORT").toValidatedNel,
config.parse[String] ("DL_CLUSTER_ADDRESS").toValidatedNel,
config.parse[String] ("DL_HTTP_ADDRESS").toValidatedNel,
config.parse[Int] ("DL_HTTP_PORT").toValidatedNel) {
case (clusterName, clusterPort, clusterAddress, httpAddr, httpPort) =>
HuffConfig(clusterName, clusterPort, clusterAddress, httpAddr, httpPort)
}
}
Define types to represent “errors"
38. package xxx.config
import scala.concurrent.duration.{Duration,FiniteDuration}
import cats._
import cats.data._
import cats.implicits._
import cats.data.Validated
import cats.data.Validated.{Invalid, Valid}
// code that needs to remain hidden
sealed abstract class ConfigError
final case class MissingConfig(field : String) extends ConfigError
final case class ParseError(field: String) extends ConfigError
case class Config(map : Map[String,String])
case class HuffConfig(
clusterName: String,
clusterPort : Int,
clusterAddress : String,
hostname: String,
listeningPort: Int)
object Validator {
def getHuffConfig(config: Config) : ValidatedNel[ConfigError, HuffConfig] =
Apply[ValidatedNel[ConfigError, ?]].map5(
config.parse[String] ("DL_CLUSTER_NAME").toValidatedNel,
config.parse[Int] ("DL_CLUSTER_PORT").toValidatedNel,
config.parse[String] ("DL_CLUSTER_ADDRESS").toValidatedNel,
config.parse[String] ("DL_HTTP_ADDRESS").toValidatedNel,
config.parse[Int] ("DL_HTTP_PORT").toValidatedNel) {
case (clusterName, clusterPort, clusterAddress, httpAddr, httpPort) =>
HuffConfig(clusterName, clusterPort, clusterAddress, httpAddr, httpPort)
}
}
Define types to represent “errors"
Validate and read into configuration object.
39. package xxx.config
import scala.concurrent.duration.{Duration,FiniteDuration}
import cats._
import cats.data._
import cats.implicits._
import cats.data.Validated
import cats.data.Validated.{Invalid, Valid}
// code that needs to remain hidden
sealed abstract class ConfigError
final case class MissingConfig(field : String) extends ConfigError
final case class ParseError(field: String) extends ConfigError
case class Config(map : Map[String,String])
case class HuffConfig(
clusterName: String,
clusterPort : Int,
clusterAddress : String,
hostname: String,
listeningPort: Int)
object Validator {
def getHuffConfig(config: Config) : ValidatedNel[ConfigError, HuffConfig] =
Apply[ValidatedNel[ConfigError, ?]].map5(
config.parse[String] ("DL_CLUSTER_NAME").toValidatedNel,
config.parse[Int] ("DL_CLUSTER_PORT").toValidatedNel,
config.parse[String] ("DL_CLUSTER_ADDRESS").toValidatedNel,
config.parse[String] ("DL_HTTP_ADDRESS").toValidatedNel,
config.parse[Int] ("DL_HTTP_PORT").toValidatedNel) {
case (clusterName, clusterPort, clusterAddress, httpAddr, httpPort) =>
HuffConfig(clusterName, clusterPort, clusterAddress, httpAddr, httpPort)
}
}
Define types to represent “errors"
Validate and read into configuration object.
Validation logic
40. How does anyone create a stack of Monads ?
Monad Transformers
41. How does anyone create a stack of Monads ?
Monad Transformers
42. Let’s take a closer look
scala> case class Cat(name: String, alive: Boolean)
defined class Cat
scala> def isAlive = Reader{ (u:User) => if (u.alive) u.asRight[Throwable].toOption:: Nil
| else scala.util.Try(throw new Exception("Dead!")).asLeft[User].toOption::Nil }
isAlive2: cats.data.Reader[User,List[Option[User]]]
scala> def lookup = Cat("cat", true).some::Nil
lookup: List[Option[Cat]]
scala> for {
| someCat <- lookup
| } yield {
| for {
| cat <- someCat
| } yield isAlive(cat)
|}
res47: List[Option[cats.Id[List[Option[Cat]]]]] = List(Some(List(User(cat,true))))
Let’s say we like to look up a cat and find out whether its alive.
We would use Option[Cat] to say whether we can find one, and perhaps
Either[Throwable,Cat] to represent when cat is dead, we throw an exception
else we return the Cat
First Attempt
43. Let’s take a closer look
scala> case class Cat(name: String, alive: Boolean)
defined class Cat
scala> def isAlive =
| Reader{ (u: Cat) => if (u.alive) OptionT( u.asRight[Throwable].toOption:: Nil)
| else OptionT( scala.util.Try(throw new Exception("Dead!")).asLeft[Cat].toOption::Nil) }
isAlive: cats.data.Reader[Cat,cats.data.OptionT[List, Cat]]
scala> def lookup = OptionT(Cat("cat", true).some::Nil)
lookup: cats.data.OptionT[List, Cat]
scala> for {
| cat <- lookup
| checked <- isAlive(cat)
| } yield checked
res32: cats.data.OptionT[List, Cat] = OptionT(List(Some(Cat(cat,true))))
The nested-yield loops can quickly get very confusing ….
that’s where Monad Transformers help!
Second Attempt
44. Effectful Monads aka Eff-Monads
Effectful Monads
An alternative to Monad Transformers
http://atnos-org.github.io/eff/
45. Use-case #4
Putting in the type-definitions: making use of the
Reader, Writer, Either Effects from Eff !
import xxx.workflow.models.{WorkflowDescriptor, Service}
import scala.language.{postfixOps, higherKinds}
import org.atnos.eff._, all._, syntax.all._
import com.typesafe.config._
import com.typesafe.scalalogging._
class LoadWorkflowDescriptorEff {
import cats._, data._, implicits._
import io.circe._, io.circe.generic.auto._, io.circe.parser._, io.circe.syntax._
lazy val config = ConfigFactory.load()
lazy val logger = Logger(getClass)
type WorkflowIdReader[A] = Reader[String, A]
type WriterString[A] = Writer[String,A]
type DecodeFailure[A] = io.circe.DecodingFailure Either A
type ParseFailure[A] = io.circe.ParsingFailure Either A
// ...
}
46. import java.time._
type LoadDescStack =
Fx.fx6[WorkflowIdReader, WriterString, DecodeFailure, ParseFailure, Throwable Either ?, Eval]
def loadDescriptor : Eff[LoadDescStack, WorkflowDescriptor] =
for {
workflowId <- ask[LoadDescStack,String]
_ <- tell[LoadDescStack,String](s"[${Instant.now()}] About to load data about workflow: $workflowId")
contents <- fromEither[LoadDescStack,java.lang.Throwable,String](loadContents(workflowId))
_ <- tell[LoadDescStack,String](s"[${Instant.now()}] Data is loaded from storage: $contents")
json <- fromEither[LoadDescStack,io.circe.ParsingFailure,io.circe.Json](parse(contents))
_ <- tell[LoadDescStack, String](s"[${Instant.now()}] Workflow descriptor parsed successfully")
desc <- fromEither[LoadDescStack, io.circe.DecodingFailure, WorkflowDescriptor](json.as[WorkflowDescriptor])
_ <- tell[LoadDescStack, String](s"[${Instant.now()}] Workflow descriptor hydrated into object.")
} yield desc
// Below is a test and you can choose either runEval or attemptEval
// attemptEval is a better option as it captures any errors met during the
// computation.
//println(loadDescriptor.runReader("1").runWriter.runEither.runEither.runEither.runPure)
lazy val result = {
val a = loadDescriptor.runReader("1").runWriter.runEither.runEither.runEither.runPure
val t = a.get
t.joinRight
}
// the logging version
lazy val result2 = {
val a = loadDescriptor.runReader("1").runWriterLog.runEither.runEither.runEither.runPure
val t = a.get
t.joinRight
}
}
Use-case #4
47. import java.time._
type LoadDescStack =
Fx.fx6[WorkflowIdReader, WriterString, DecodeFailure, ParseFailure, Throwable Either ?, Eval]
def loadDescriptor : Eff[LoadDescStack, WorkflowDescriptor] =
for {
workflowId <- ask[LoadDescStack,String]
_ <- tell[LoadDescStack,String](s"[${Instant.now()}] About to load data about workflow: $workflowId")
contents <- fromEither[LoadDescStack,java.lang.Throwable,String](loadContents(workflowId))
_ <- tell[LoadDescStack,String](s"[${Instant.now()}] Data is loaded from storage: $contents")
json <- fromEither[LoadDescStack,io.circe.ParsingFailure,io.circe.Json](parse(contents))
_ <- tell[LoadDescStack, String](s"[${Instant.now()}] Workflow descriptor parsed successfully")
desc <- fromEither[LoadDescStack, io.circe.DecodingFailure, WorkflowDescriptor](json.as[WorkflowDescriptor])
_ <- tell[LoadDescStack, String](s"[${Instant.now()}] Workflow descriptor hydrated into object.")
} yield desc
// Below is a test and you can choose either runEval or attemptEval
// attemptEval is a better option as it captures any errors met during the
// computation.
//println(loadDescriptor.runReader("1").runWriter.runEither.runEither.runEither.runPure)
lazy val result = {
val a = loadDescriptor.runReader("1").runWriter.runEither.runEither.runEither.runPure
val t = a.get
t.joinRight
}
// the logging version
lazy val result2 = {
val a = loadDescriptor.runReader("1").runWriterLog.runEither.runEither.runEither.runPure
val t = a.get
t.joinRight
}
}
Use-case #4
Eff-Monads allows us to stack computations