PRACTICAL CATS
sharing what i’ve learnt
ABOUT MYSELF
CATS….
To use cats effectively, understand what each
construct does:
Functor
Monads
Applicatives
Monoids
Semigroups
SO MANY ACRONYMS !!!
Validated
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.
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.
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. *
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. *
Monoids - what are they?
> import cats._, data._, implicits._

> Monoid[String].combine(“hi”, “there”)

// res0: String = “hithere”

> “hi” |+| “there”

// res1: String = “hithere”
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
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)
}
)
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 *
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” *
Monads
Monads are meant for 

sequencing computations
Monads
someList.flatmap(element => 

someOtherList.flatmap(element2 =>

(element, element2)))
*No tuples generated if either “someList”

Or “someOtherList” is empty*
Monads
someList.flatmap(element => 

someOtherList.flatmap(element2 =>

(element, element2)))
Monads allows for short-circuiting of

computations
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.
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”)
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.
Monads - examples
> Monad[List].pure(4)

> res2: List[Int] = List(4)
This is to lift values into a context, in this case
Monadic context.
Monads - flow
scala> def first = Reader( (x:Int) => Monad[List].ifM(List(true,true))(x::Nil, Nil) )

extractGroups: cats.data.Reader[Int,List[Int]]

scala> def second = Reader( (x:Int) => Monad[List].ifM(List(true,true))(x::Nil, Nil) )

loadGroup: cats.data.Reader[Int,List[Int]]

scala> for { g <- first(4); gg <- second(g) } yield gg

res21: List[Int] = List(4, 4, 4, 4)
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
Writer Monad
scala> def logNumber(x: Int): Writer[List[String], Int] =
         Writer(List("Got number: " + x.show), 3)
logNumber: (x: Int)cats.data.Writer[List[String],Int]
scala> def multWithLog: Writer[List[String], Int] =
         for {
           a <- logNumber(3)
           b <- logNumber(5)
         } yield a * b
multWithLog: cats.data.Writer[List[String],Int]
scala> multWithLog.run
res2: cats.Id[(List[String], Int)] = (List(Got number: 3, Got number: 5),9)
scala> multWithLog.reset
res6: cats.data.WriterT[cats.Id,List[String],Int] = WriterT((List(),9))
scala> multWithLog.swap
res8: cats.data.WriterT[cats.Id,Int,List[String]] = WriterT((9,List(Got number: 4, Got
number: 3)))
scala> multWithLog.value
res9: cats.Id[Int] = 9
scala> multWithLog.written
res10: cats.Id[List[String]] = List(Got number: 4, Got number: 3)
source: http://eed3si9n.com/herding-cats/Writer.html
Compose Writers
Reader Monad
“Readers” allows us to compose operations which depend
on some input.
source: http://eed3si9n.com/herding-cats/Reader.html
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.
State Monad
Allows us to pass state-information around in a computation.
http://eed3si9n.com/herding-cats/State.html
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
}
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
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.
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
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.
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.
Applicative - examples
A typical application is to leverage Applicatives in writing
Logic to validate configurations, forms etc
import cats.Cartesian
import cats.data.Validated
import cats.instances.list._ // Semigroup for List
type AllErrorsOr[A] = Validated[List[String], A]
Cartesian[AllErrorsOr].product(
Validated.invalid(List("Error 1")),
Validated.invalid(List("Error 2")) )
// res1: AllErrorsOr[(Nothing, Nothing)] = Invalid(List(Error 1,Error 2))
Applicative - examples
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)
}
}
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"
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.
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
How does anyone create a stack of Monads ?
Monad Transformers
How does anyone create a stack of Monads ?
Monad Transformers
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
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
Effectful Monads aka Eff-Monads
Effectful Monads

An alternative to Monad Transformers
http://atnos-org.github.io/eff/
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
// ...
}
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
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
Learning resources
https://www.haskell.org/tutorial/monads.html
http://eed3si9n.com/herding-cats/
http://typelevel.org/cats/
http://blog.higher-order.com/
https://gitter.im/typelevel/cats
That’s it from me :)
Questions ?

Practical cats

  • 1.
  • 2.
  • 4.
    CATS…. To use catseffectively, understand what each construct does: Functor Monads Applicatives Monoids Semigroups SO MANY ACRONYMS !!! Validated
  • 6.
    Building blocks … Understandthat 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 ofthe 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 - whatare 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 - whatare 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. *
  • 10.
    Monoids - whatare they? > import cats._, data._, implicits._ > Monoid[String].combine(“hi”, “there”) // res0: String = “hithere” > “hi” |+| “there” // res1: String = “hithere”
  • 11.
    Use case forMonoids/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 - whatare 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 - whatare 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” *
  • 15.
    Monads Monads are meantfor sequencing computations
  • 16.
    Monads someList.flatmap(element => someOtherList.flatmap(element2=> (element, element2))) *No tuples generated if either “someList” Or “someOtherList” is empty*
  • 17.
    Monads someList.flatmap(element => someOtherList.flatmap(element2=> (element, element2))) Monads allows for short-circuiting of computations
  • 18.
    Monads - aquick 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.
  • 22.
    Monads - flow scala>def first = Reader( (x:Int) => Monad[List].ifM(List(true,true))(x::Nil, Nil) ) extractGroups: cats.data.Reader[Int,List[Int]] scala> def second = Reader( (x:Int) => Monad[List].ifM(List(true,true))(x::Nil, Nil) ) loadGroup: cats.data.Reader[Int,List[Int]] scala> for { g <- first(4); gg <- second(g) } yield gg res21: List[Int] = List(4, 4, 4, 4)
  • 23.
    Writer Monad “Writers” aretypically 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
  • 24.
    Writer Monad scala> deflogNumber(x: Int): Writer[List[String], Int] =          Writer(List("Got number: " + x.show), 3) logNumber: (x: Int)cats.data.Writer[List[String],Int] scala> def multWithLog: Writer[List[String], Int] =          for {            a <- logNumber(3)            b <- logNumber(5)          } yield a * b multWithLog: cats.data.Writer[List[String],Int] scala> multWithLog.run res2: cats.Id[(List[String], Int)] = (List(Got number: 3, Got number: 5),9) scala> multWithLog.reset res6: cats.data.WriterT[cats.Id,List[String],Int] = WriterT((List(),9)) scala> multWithLog.swap res8: cats.data.WriterT[cats.Id,Int,List[String]] = WriterT((9,List(Got number: 4, Got number: 3))) scala> multWithLog.value res9: cats.Id[Int] = 9 scala> multWithLog.written res10: cats.Id[List[String]] = List(Got number: 4, Got number: 3) source: http://eed3si9n.com/herding-cats/Writer.html Compose Writers
  • 25.
    Reader Monad “Readers” allowsus to compose operations which depend on some input. source: http://eed3si9n.com/herding-cats/Reader.html
  • 26.
    Reader Monad case classConfig(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 usto 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 forfunctions 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 Atypical application is to leverage Applicatives in writing Logic to validate configurations, forms etc
  • 35.
    import cats.Cartesian import cats.data.Validated importcats.instances.list._ // Semigroup for List type AllErrorsOr[A] = Validated[List[String], A] Cartesian[AllErrorsOr].product( Validated.invalid(List("Error 1")), Validated.invalid(List("Error 2")) ) // res1: AllErrorsOr[(Nothing, Nothing)] = Invalid(List(Error 1,Error 2)) Applicative - examples
  • 36.
    package xxx.config import scala.concurrent.duration.{Duration,FiniteDuration} importcats._ 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} importcats._ 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} importcats._ 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} importcats._ 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 anyonecreate a stack of Monads ? Monad Transformers
  • 41.
    How does anyonecreate a stack of Monads ? Monad Transformers
  • 42.
    Let’s take acloser 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 acloser 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 akaEff-Monads Effectful Monads An alternative to Monad Transformers http://atnos-org.github.io/eff/
  • 45.
    Use-case #4 Putting inthe 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
  • 48.
  • 49.
    That’s it fromme :) Questions ?