Functions, Types, Programs and Effects
-
Upload
raymond-roestenburg -
Category
Software
-
view
131 -
download
5
Transcript of Functions, Types, Programs and Effects
What's this all about?type E = Task |: (String \/ ?) |: Option |: Base
type Stack = ReaderInt |: WriterString |: Eval |: NoEffect
type PRG[A] = (Log.DSL :@: DB.DSL :@: FXNil)#Cop[A]Free[PRG, Xor[DBError, Entity]]
FUNCTIONS, TYPES, PROGRAMS AND EFFECTS
Strict separation of safe and unsafe
Safe: Functions, Types, creating ProgramsUnsafe: Effects, running Programs
WRITING FUNCTIONAL SCALA
1. Write functions, which return values2. Choose effects (or write your own)
3. Construct a program that composes values into effects4. Run the program (in an 'App' in the main method)
(Not necessarily in that order)
WRITING FUNCTIONAL SCALA
▸ You can possibly have programs of programs▸ A program is very often defined in a for comprehension
A hypothetical example. val program = for { client <- server.connect(details) Exchange(src, snk) = client.exchange _ <- snk.sendRequest(request) in = src.pipe(text.utf8Decode) .to(io.stdOutLines) } yield ()
program.run
Define first, run later.
Anything 'wrong' with these methods / functions?def getUser(username: Username): Future[User]
def createUser(details: UserDetails): Future[User]
def getPrimaryAccount(user: User): Future[Account]
Anything 'wrong' with this?def getUser(username: Username): Future[Option[User]]
def getAccounts(user: User): Future[List[Account]]
def approved(user: User, accounts: List[Account]): Future[Boolean]
DATA LOSS
Boolean blindnessFunctions return values, discarding them constrains the Program 1
1 Think of the information you need later on in a for comprehension
What is 'wrong' with this?sealed trait Errorcase object UserNotFound extends Errorcase object UserNameNotFound extends Errorcase object AccountNotFound extends Error
def getUser(username: Username): Future[Either[Error, User]]def getAccounts(user: User): Future[Either[Error, Account]]
scalaz Disjunction (left or right)val res: String \/ Int = \/-(5)val res1: String \/ Int = 5.rightval moreVerbose: String \/ Int = 5.right[String]val res2: String \/ Int = "Some error".left
val friendlier = res2.leftMap(error => s"$error has occured, we apologize.")val res3 = res1.map(five => five * 2) // \/-(10)
Map over the right
From Throwable to \/\/.fromTryCatchThrowable[String, Exception] { getDangerousString()} // returns a Exception \/ String
\/.fromTryCatchThrowable[String, Exception] { getDangerousString()}.leftMap(e=> MyError(e)) // returns a MyError \/ String
From A \/ B to Xval res: String \/ Int = "Error!".leftval res1 = res.fold(left=> 0, r => r) val res2 = res.fold(left=> 0, identity) // this is the same
Why should you not use this?
Combining \/case class Error(in: Int, reason: String)
def even(in: Int): Error \/ Int = if(in % 2 == 0) in.right else Error(in, "not even").left
def divByThree(in: Int): Error \/ Int = if(in % 3 == 0) in.right else Error(in, "not div 3").left
def evenAndByThree(in: Int) = for { evenNr <- even(in) byThree <- divByThree(evenNr) } yield byThree
println(evenAndByThree(12)) // \/-(12)println(evenAndByThree(3)) // -\/-(3, "not even")println(evenAndByThree(4)) // -\/-(4, "not div 3")
def evenAndByThree(in: Int) = for { evenNr <- even(in) byThree <- divByThree(evenNr) } yield byThree
No loss of information (why the first error occurred).
Givendef getUser(username: Username): Future[Error \/ User]]def getAccounts(user: User): Future[Error \/ List[Account]]
Does this compile?val result = for { user <- getUser(name) accounts <- getAccounts(user)} yield accounts
Combining values
Monoid - collecting / combining values into one value//simplified / pseudoval append: (F,F) => Fval zero: F
//actualdef append(f1: F, f2: => F): Fdef zero: F
COMBINING EFFECTSMonad - collecting / combining effects 2
def point[A](a: => A): F[A] // Creates an 'effects collector'def bind[A, B](fa: F[A])(f: A => F[B]): F[B] // adds an effectdef map[A,B](fa: F[A])(f: A => B):F[B] // transforms input for next effect
Can only combine for same type of F.2
2 The Monad is a monoid in the category of endofunctors joke
Monads are great, but in general NOT composable.
Monad Transformers
Effect systems (i.e. Eff from eff-cats)
In common, a Type that combines N Monad types
Monad Transformer EitherTval res = for { user <- EitherT(getUser("bla")) accounts <- EitherT(getAccounts(user))} yield accounts // returns EitherT[Future, Error, List[Account]]
res.run // Future[Error \/ List[Account]]
Scalaz provides Future Monad instance in scalaz.std.scalaFuture
Construct the Programval res = for { user <- EitherT(getUser("bla")) accounts <- EitherT(getAccounts(user))} yield accounts
Run the Program
res.run
WRITE YOUR OWN MONAD WRAPPER
Topics (Kafka library)
Combine Future and \/
Covariant +A\/+B
Know why a Kafka operation failed in a Program
Covariant Error typesealed trait TopicsError
sealed trait FindTopicError extends TopicsError
sealed trait SelectTopicError extends TopicsError
sealed trait DeleteTopicError extends TopicsError
Example programval program: Topics[TopicsError, Chunk[String, String]] = for { partition ← Topics.partitioned[String, String](topicDef, partitionId) info ← partition.write(record) chunk ← partition.read(info.offset, info.offset)} yield chunk// ... somewhere laterprogram.run(broker, executionContext)
Minor detour (type lambda)Monad[{ type λ[α] = Topics[E, α] })#λ]
~=
type MyMonad[T] = Monad[Topics[E, T]]
only inline.
Roll your own Monad instance (Scalaz)object Topics { implicit def topicsMonad[E] = new Monad[({ type λ[α] = Topics[E, α] })#λ] { def point[T](value: ⇒ T): Topics[E, T] = Topics.point(value) def bind[T, U](topics: Topics[E, T])(f: T ⇒ Topics[E, U]): Topics[E, U] = topics.flatMap(f) }}
delegate to point and flatMap
Roll your own Monadcase class Topics[+E, +T](run: (TopicBroker, ExecutionContext) ⇒ Future[E \/ T]) { // more to follow
Roll your own Monadcase class Topics[+E, +T](run: (TopicBroker, ExecutionContext) ⇒ Future[E \/ T]) { def map[C](f: T ⇒ C): Topics[E, C] = Topics((broker, ec) ⇒ run(broker, ec).map(_.map(f))(ec))
def flatMap[E1 >: E, C](f: T ⇒ Topics[E1, C]): Topics[E1, C] = Topics { (broker, ec) ⇒ run(broker, ec).flatMap( _.fold(err ⇒ Future.successful(-\/(err)), res ⇒ f(res).run(broker, ec)) )(ec) }}
case class Topics[+E, +T](run: (TopicBroker, ExecutionContext) ⇒ Future[E \/ T]) { def map[C](f: T ⇒ C): Topics[E, C] = Topics((broker, ec) ⇒ run(broker, ec).map(_.map(f))(ec))
zoom inTopics((broker, ec) ⇒ run(broker, ec).map(_.map(f))(ec))
zoom in run(broker, ec).map(_.map(f))
Topics { (broker, ec) ⇒ run(broker, ec).flatMap( _.fold(err ⇒ Future.successful(-\/(err)), res ⇒ f(res).run(broker, ec)) )(ec) }
zoom in run(broker, ec).flatMap( _.fold(err ⇒ Future.successful(-\/(err)), res ⇒ f(res).run(broker, ec)))(ec)
zoom in fold(err ⇒ Future.successful(-\/(err)), res ⇒ f(res).run(broker, ec))
Convenience methodsobject Topics { def run[E, T](broker: TopicBroker, action: Topics[E, T])(implicit ec: ExecutionContext): Future[E \/ T] = action.run(broker, ec) def point[T](value: ⇒ T): Topics[Nothing, T] = Topics[Nothing, T]((broker, ec) ⇒ Future.successful(value.right))
def future[T](action: Future[T]): Topics[Nothing, T] = Topics((broker, ec) ⇒ action.map(_.right)(ec)) def futureF[T](action: ExecutionContext ⇒ Future[T]): Topics[Nothing, T] = Topics((broker, ec) ⇒ action(ec).map(_.right)(ec))
def either[E, T](either: E \/ T): Topics[E, T] = Topics((_, _) ⇒ Future.successful(either))
def futureEither[E, T](action: Future[E \/ T]): Topics[E, T] = Topics((_, _) ⇒ action) def futureEitherF[E, T](action: ExecutionContext ⇒ Future[E \/ T]): Topics[E, T] = Topics((_, ec) ⇒ action(ec)) // ... more code }
import scalaz.scalacheck.ScalazProperties.monad
class MonadLawsSpec extends Spec { def is = s2"""obey the monad laws $laws"""
def laws = { implicit val b = broker monad.laws[({ type l[a] = Topics[Int, a] })#l] }}
case class Config(inputFile: Option[File], outputFile: Option[File])
def main(args: Array[String]): Unit = { val config = parseArgs(args) // ... }
val inputFile = optional(opt[File]( ensure[File](readStr.map(str ⇒ new File(str)), "INPUT_FILE must be an existing file", _.isFile), long("input-file"), metavar("INPUT_FILE"), short('f'), help("input file to read from") ))
val outputFile = optional(opt[File]( readStr.map(str ⇒ new File(str)), long("output-file"), metavar("OUTPUT_FILE"), short('o'), help("output file to write to") ))
def parseArgs(args: Array[String]): Config = { val inputFile = optional(opt[File]( // ... omitted val outputFile = optional(opt[File]( // ... omitted
val parser = (input |@| inputFile)(Config.apply(_, _)) execParser(args, "copy", info(parser <*> helper, header("The awesome copy utility.") ))}
Define the program val parser = (input |@| inputFile)(Config.apply(_, _))
Execute the program execParser(args, "copy", info(parser <*> helper, header("The awesome copy utility.") ))
On a lazy Sunday..trait MonadPlus[F[_]] extends Monad[F] with ApplicativePlus[F] { self =>//... /** Generalized version of Haskell's `partitionEithers` */ def separate[G[_, _], A, B](value: F[G[A, B]])(implicit G: Bifoldable[G]): (F[A], F[B]) = { val lefts = bind(value)((aa) => G.leftFoldable.foldMap(aa)(a => point(a))(monoid[A])) val rights = bind(value)((bb) => G.rightFoldable.foldMap(bb)(b => point(b))(monoid[B])) (lefts, rights) }//...
final class MonadPlusOps[F[_],A] private[syntax](val self: F[A])(implicit val F: MonadPlus[F]) extends Ops[F[A]] { final def separate[G[_, _], B, C](implicit ev: A === G[B, C], G: Bifoldable[G]): (F[B], F[C]) = F.separate(ev.subst(self))//...
You read some code..
But, separate is very useful.def separate[G[_, _], A, B](value: F[G[A, B]])(implicit G: Bifoldable[G]): (F[A], F[B])
Remove everything, keep args and return value(value: F[G[A, B]]): (F[A], F[B])
Lets substitute F, G, A and B to something we knowF[G[A, B]] => (F[A], F[B])
F = ListG[A,B] = Error \/ Result4
4 \/ uses infix type notation, same as \/[Error,Result]
List[Error \/ Result] => (List[Error], List[Result])
It's a function to separate the errors from the results!
DIG
Remove all syntax until you are left with a functionThen find which implicits / type classes are needed for the function.
Scalaz requires you to know how the typeclasses are organized
cats project has more docs
An introduction to cats
http://typelevel.org/cats/Advanced Scala with Cats book 5
http://underscore.io/blog
5 http://underscore.io/books/advanced-scala/
Functional Programming in Scala (the red book) 6
6 https://www.manning.com/books/functional-programming-in-scala
WRITING FUNCTIONAL SCALA
▸ Write functions▸ Choose effects (or write your own)
▸ Construct a program that composes the functions and effects▸ Run the program (in an 'App' in the main method)
RETURN VALUESUse data types like A \/ B that do not lose information about what
happened
Boolean blindness
'Option flatMap' blindness?
PROGRAMSChoose a reasonable architecture to construct your Programs
Monad Wrappers
Monad Transformers
Effect Systems