Upload
hang-zhao
View
446
Download
0
Embed Size (px)
DESCRIPTION
Function Programming in Scala. A lot of my examples here comes from the book Functional programming in Scala By Paul Chiusano and Rúnar Bjarnason, It is a good book, buy it.
Citation preview
FP in Scala with adtsWonderful Machines code
ArrowArrow are generalisation of function.It is inherent from Category and can be used for general computation.Arrow heavily use tuple (x,y) in its implementation for multiple input functions. It can also encapsulate in side-effect, and often compare with Applicative and Monad.
Arrowtrait Category[~>[_, _]] {
self =>
// minimum set
def id[A]: A ~> A
def compose[A, B, C](f: B ~> C, g: A ~> B): (A ~> C)
def andThen[A, B, C](f: A ~> B, g: B ~> C): (A ~> C) =
compose(g, f)
}
trait Arrow[~>[_, _]] extends Category[~>] {
self =>
//minimum set
def arr[A, B](f: A => B): A ~> B
def first[A,B,C](f: A ~> B): ((A, C) ~> (B, C))
def second[A,B,C](f: A ~> B): ((C, A) ~> (C, B)) = {
def swap[X, Y] = arr[(X, Y), (Y, X)] {
case (x, y) => (y, x)
}
compose(swap, compose(first[A, B, C](f), swap))
}
def split[A,B,C,D](f: A ~> B, g: C ~> D)
: ((A, C) ~> (B, D)) = //parallelComposition
compose(second[C, D, B](g), first[A, B, C](f))
def combine[A,B,C](f: A ~> B, g: A ~> C)
: (A ~> (B,C)) = //fanoutComposition
compose(split(f, g), arr((b: A) => (b, b)))
}
Arrow
Arrow as generation of function, contains our familiar combinator: compose, andThen It also has various other combinators:Like first/second/split/combine
Understand Arrow combinators
arr andThen first
Understand Arrow combinators
second split combine
Simple exampleval FuncArrow = new Arrow[Function1] {
def arr[A, B](f: A => B): Function1[A, B] = f
def first[A,B,C](f: A => B): (((A, C)) => (B, C)) =
(ac: (A, C)) => (f(ac._1), ac._2)
def id[A]: A => A = (a: A) => a
def compose[A, B, C](f: B => C, g: A => B): (A => C)
=
(a: A) => f(g(a))
}
def twoVarFunc(x: (Int,Int)): Int = {
x._1 + x._2
}
def eleven (x: Any): Int = {11}
def twelve (x: Any): Int = {12}
val test = FuncArrow.andThen(
FuncArrow.combine(
FuncArrow.arr(eleven),
FuncArrow.arr(twelve)
),
FuncArrow.arr[(Int, Int), Int](twoVarFunc)
)
import scala.util._
val res = test.apply(new Random(System.currentTimeMillis).nextDouble)
Stream example def twoVarFunc: ((Stream[Int], Stream[Int])) =>
Stream[Int] = n => {
(n._1).zip(n._2).map { n => {
( n._1 < n._2 ) match {
case true => n._1
case false => n._2
}
}}
}
implicit def Stream2Applicative[A](o: Stream[A]):
Applicative[Stream, A] = new Applicative[Stream, A]
(o) {
def unit[C](a: C): Stream[C] = Stream.continually(a)
def ap_: [B](f: Stream[A => B]): Stream[B] = {
o.zip(f).map { x => (x._2(x._1)) }
}
}
val x1: Stream[Int] = {
def loop(v: Int): Stream[Int] = v #:: loop(v + 1000)
loop(0)
}
val x2: Stream[Int] = 0 #:: 1 #:: x2.zip(x2.tail).map {
n => n._1 + n._2 }
val x3: Stream[Int] = {
def loop(v: Int): Stream[Int] = v #:: loop(v * 2)
loop(1)
}
Stream example (cont.)val test1 = ((threeVarFunc.curried map_: x1) ap_: x2) ap_: x3
val test2 = FuncArrow.andThen(
FuncArrow.combine[Stream[Int], Stream[Int], Stream[Int]](
FuncArrow.arr( (x: Any) => { x1 } ),
FuncArrow.arr( (x: Any) => { x3 } )
),
FuncArrow.arr[(Stream[Int], Stream[Int]), Stream[Int]](twoVarFunc)
)(Stream(0))
As you can see, applicative and arrow can both acting as the framework and building block for Stream processing (unlimited Stream is not a monad, so no monad here), we can try monad and see what happens val test3 = for {
r1 <- x1
r2 <- x2
r3 <- x3
} yield { (r1,r2,r3) }
Operator .*: and implicit conversionLast slide, you can see operate map_: and ap_: this is because I want to rearrange the sequence and used : , every method end with : is right associative.Also I create a implicit function here that convert Stream[A] automatically to Applicative[Stream, A] , as long as this function in scope. It is powerful, but can be dangerous and hard to read.
Applicative/Arrow/Monad comparison val x4 = Option(1)
val x5 = Option(2)
val x6 = Option(3)
def twoVarFunc2: ((Option[Int], Option[Int])) => Option[Int] = n => {
n._1 match {
case Some(a) => {
n._2 match {
case Some(b) => {
( a < b ) match {
case true => Some(a)
case false => Some(b)
}}
case None => None
}}
case None => None
}}
def threeVarFunc: (Int, Int, Int) => Int = {
_ + _ + _
}
Applicative/Arrow/Monad comparison (Cont)
val test4 = ((threeVarFunc.curried map_: x4) ap_: x5) ap_: x6
val test6 = {
for {
r1 <- x4
r2 <- x5
} yield {
if(r1 == 1) {
r1 * r2
} else {
r1 + r2
}}}
val test5 = FuncArrow.andThen(
FuncArrow.combine[Option[Int], Option[Int], Option[Int]](
FuncArrow.arr( (x: Any) => { x5 } ),
FuncArrow.arr( (x: Any) => { x6 } )
),
FuncArrow.arr[(Option[Int], Option[Int]), Option[Int]](twoVarFunc2)
)(Option(0))
Applicative/Arrow/Monad comparison (Cont)
They looks similar and can do similar things…What distinguish them?Build system normally have 2 phases:
1. build building blocks2. assemble build blocks
These 2 phases are normally done at different time, and possibly by different people.
Building BlocksApplicatives
x4x5x6
Automatically implicitly converted to Applicative
Arrow
FuncArrow.arr( (x: Any) =>
{ x5 } )
FuncArrow.arr( (x: Any) =>
{ x6 } )
FuncArrow.arr[(Option[Int], Option[Int]), Option[Int]](twoVarFunc2)
Monad
x4x5x6
Which are Monads
Building BlocksApplicative: Isolated, Static, Sequential appliedArrow: Connected, Static, Sequential appliedMonad: Connected, Dynamic, Sequential applied
Even you can do anything at assembling time, they are different when providing the building blocks for a system.
Usefulness of Arrow
Arrow was proposed as the tools for Functional Reactive Programming, but because of the clumsy Tuple and difficult syntax, a lot people use Applicative and Monad more for FRP.It can be good when you need it for certain scenarios. There is no support in Scala to simplify the Arrow syntax.
Arrow Familytrait ArrowChoice[~>[_, _]] extends Arrow[~>] {
def left[A,B,C](f: A ~> B): (Either[A, C] ~> Either[B,C])
def right[A,B,C](f: A ~> B): (Either[C, A] ~> Either[C,B]) = {
def swap[X,Y] = arr[Either[X, Y], Either[Y, X]] {
case Left(x) => Right(x)
case Right(y) => Left(y)
}
compose(swap, compose(left[A, B, C](f), swap))
}
def multiplex[A,B,C,D](f: A ~> B, g: C ~> D)
: (Either[A,C] ~> Either[B,D]) =
compose(right[C, D, B](g), left[A, B, C](f))
def merge[A,B,C](f: A ~> C, g: B ~> C)
: (Either[A,B] ~> C) = {
def utag[D](v: Either[D, D]): D = v match {
case Left(x) => x
case Right(y) => y}
compose(arr[Either[C,C],C](utag[C]), multiplex(f,g))
}}
Arrow with choice on the left, a bit more flexible.
trait ArrowApply[~>[_, _]] extends Arrow[~>] {
def app[B, C]: (B ~> C, B) ~> C
}
Same power as Monad, so it is not popular...
ComonadMonadic
A => M[B]
abstract impure computation
abstract producer
Comonadic
M[A] => B
abstract context related computation
abstract consumer
Comonadtrait Functor[F[_]] {
def map[A,B](fa: F[A])(f: A => B): F[B]
}
trait Monad[F[_]] extends Functor[F] {
def unit[A](a: => A): F[A]
def flatMap[A,B](ma: F[A])(f: A => F[B]): F[B]
def join[A](mma: F[F[A]]): F[A] = flatMap(mma)(la => la)
override def map[A,B](ma: F[A])(f: A => B): F[B] =
flatMap(ma)(a => unit(f(a)))
}
trait Comonad[F[_]] extends Functor[F] {
def extract[A](a: F[A]): A
def extend[A,B](la: F[A])(f: F[A] => B): F[B]
// = map(duplicate(la))(f)
def duplicate[A](ma: F[A]): F[F[A]] =
extend(ma)(mma => mma)
override def map[A,B](fa: F[A])(f: A => B): F[B] =
extend(fa)((w: F[A]) => f(extract(w)))
}
Unlimited stream continually calculationimport scala.language.higherKinds
val StreamComonad = new Comonad[Stream] {
def extract[A](a: Stream[A]): A = a.head
override def duplicate[A](ma: Stream[A]): Stream[Stream[A]] = {
def rec(remain: Stream[A], last: Stream[A]):Stream[Stream[A]]
= {
val cur = last :+ remain.head
cur #:: rec(remain.tail, cur)
}
Stream(ma.head) #:: rec(ma.tail, Stream(ma.head))
}
def extend[A,B](la: Stream[A])(f: Stream[A] => B): Stream[B] =
{
duplicate(la).map(f)
}
}
object test {
def average(i: Stream[Int]): Double = {
val res = i.foldLeft((0,0d))( (b,a) => (b._1 + a, b._2 + 1) )
res._1.toDouble / res._2.toDouble
}
val fibs: Stream[Int] = 0 #:: 1 #:: fibs.zip(fibs.tail).map { n => n._1
+ n._2 }
val test1 = StreamComonad.extend[Int, Double](fibs)(average)
}
Comonad
Someone like it, as it capture the model: generate next element based on all historywhich is common in multi media area (audio/video), but it also means you carry a whole history around, if not careful, someone hate it...
ArrowLoopabstract class LazyTuple[A,B]{
def _1: A
def _2: B
}
object LazyTuple {
def apply[A, B](a: => A, b: => B): LazyTuple[A, B] = new LazyTuple[A, B] {
def _1 = a
def _2 = b
}
}
def lazyTuple[A, B](a: => A, b: => B): LazyTuple[A, B] = new LazyTuple[A, B] {
def _1 = a
def _2 = b
}
trait ArrowLoop[~>[_, _]] extends Arrow[~>] {
def loop[A, B, C](f: LazyTuple[A,C] ~> LazyTuple[B,C] ): A ~> B
}
Require language’s lazy support, kind of recursion value.Think about a feedback circuit.
ArrowLoopArrowLoop can have positive feedback (diverge, never stop), can have negative feedback (converge, stop at some point, which we care more)Let us do a square root calculation based on
ArrowLoop (square root)val FuncArrowLoop = new ArrowLoop[Function1] {
def arr[A, B](f: A => B): Function1[A, B] = f
def first[A,B,C](f: A => B): (((A, C)) => (B, C)) =
(ac: (A, C)) => (f(ac._1), ac._2)
def id[A]: A => A = (a: A) => a
def compose[A, B, C](f: B => C, g: A => B): (A => C) =
(a: A) => f(g(a))
def loop[A, B, C](f: LazyTuple[A,C] => LazyTuple[B,C] ): A
=> B =
(a: A) => new { val bc: LazyTuple[B,C] = f(lazyTuple(a, bc.
_2)) }.bc._1
}
val squareRoot = FuncArrowLoop.loop[(Double, Double),
Double, Double => Double] {
ac => lazyTuple[Double, Double => Double](
ac._2(ac._1._1),
{ x:Double => {
val ret: Double = x - (((x * x) - ac._1._2) / (2 * x))
(abs(ret - x) < 0.0001) match {
case true => ret
case false => ac._2(ret)
}
}}
)
}
ArrowLoop (explanation)This is not recursive function call, it is using lazy evaluation, in fact rewrite code in some sense.Happens in heap space, and not in stack, internally in fact use Java Reflection.It is Heavy, not Fast, so use it with care, and better only use it when really needed.But it does provide many things youdo not see often.
Fix/MonadFixdef fix[A](f: Lazy[A => A]): A // result is where f(x) = x
trait MonadFix[F[_]] extends Monad[F] {
def mfix[A](f: Lazy[A => F[A]]): F[A]
}
Fix comes from mathematical notion of least fixpoint of function , for engineer, it is more of a abstraction of recursive function call, similar to ArrowLoop with lazy, can happen in heap space, Fix also related to Free Monad provide a way to do general stackless recursion (without heavy lifting in reflection).
MonadFix similar to Fix , it means apply the monadic effect (side effect) only once, but also recursion to converge to get final result.