1 of 35

zeenea.com.

Pure FP with IO

And without headache

2 of 35

3 of 35

What is FP

Functional programming is programming with functions

Pure functions

No side effects (the less possible)

4 of 35

Side effects

  • reassigning a variable
  • modifying a data structure
  • setting a field on an object
  • throwing an exception
  • printing to console
  • reading user input
  • reading / writing to a file / network / db
  • drawing on the screen

Avoidable

Deferrable

5 of 35

The monad detour

“All methaphor are wrong, but some are useful”

6 of 35

Put a type in a box

pure(a: A): M[A]

7 of 35

Transform the type in the box

map(m: M[A], f: A => B): M[B]

8 of 35

Transform the type in the box

map(m: M[A], f: A => M[B]): M[M[B]]

9 of 35

Stacking the boxes

flatMap(m: M[A], f: A => M[B]): M[B]

10 of 35

A monad represent a concept

11 of 35

Common operations: plural

val users = Seq(User("1", "John"))

val users = Seq()

for (user <- users) {

doStuff(user)

}

val users = Seq(User("1", "John"))

val users = Seq()

users.map(doStuff)

12 of 35

Common operations: optional

val userOpt= User("1", "John")

val userOpt = null

if (userOpt != null) {

doStuff(userOpt)

}

val userOpt = Option(User("1", "John"))

val userOpt = None

users.map(doStuff)

13 of 35

Common operations: error

def fetchUser(id: String) = User(id, "John")

def fetchUser(id: String) = throw new Exception

try {

doStuff(fetchUser("1"))

} catch {

case _: Throwable =>

}

val userErr = Try(User("1", "Loïc"))

val userErr = Failure(new Exception)

userErr.map(doStuff)

14 of 35

Common operations: async

var result: User = null

val r = new Runnable {

override def run(): Unit = {

result = User("1", "John")

}

}

val t = new Thread(r)

t.start()

t.join()

doStuff(result)

val userFut = Future(User("1", "Loïc"))

val userFut = Future(throw new Exception)

Await.result(userFut.map(doStuff), Duration.Inf)

15 of 35

Map operation

16 of 35

Pure function ~ Referentially transparent

An expression is called referentially transparent if it can be replaced with its corresponding value without changing the program's behavior.

17 of 35

Pure function ~ Referentially transparent

sum(Seq(1, 2, 3)) + sum(Seq(1, 2, 3))

val x = sum(Seq(1, 2, 3))

x + x

def sum(in: Seq[Int]): Int = {

println("aaa")

in.sum

}

// aaa

// aaa

// aaa

18 of 35

Pure function ~ Referentially transparent

def trySum(in: Seq[Int]): Try[Int] = Try {

println("aaa")

in.sum

}

for {

r1 <- trySum(Seq(1, 2, 3))

r2 <- trySum(Seq(1, 2, 3))

} yield r1 + r2

val x = trySum(Seq(1, 2, 3))

for {

r1 <- x

r2 <- x

} yield r1 + r2

// aaa

// aaa

// aaa

19 of 35

Who to make code pure?

20 of 35

21 of 35

Creating an IO

class IO[A](private val effect: () => A)

22 of 35

Creating an IO

class IO[A](private val effect: () => A) {

def unsafeRunSync(): A = effect()

}

23 of 35

Creating an IO

class IO[A](private val effect: () => A) {

def map[B](f: A => B): IO[B] = ???

def flatMap[B](f: A => IO[B]): IO[B] = ???

def unsafeRunSync(): A = effect()

}

24 of 35

Creating an IO

class IO[A](private val effect: () => A) {

def map[B](f: A => B): IO[B] =

new IO(() => f(effect()))

def flatMap[B](f: A => IO[B]): IO[B] = ???

def unsafeRunSync(): A = effect()

}

25 of 35

Creating an IO

class IO[A](private val effect: () => A) {

def map[B](f: A => B): IO[B] =

new IO(() => f(effect()))

def flatMap[B](f: A => IO[B]): IO[B] =

new IO(() => f(effect()).effect())

def unsafeRunSync(): A = effect()

}

26 of 35

Creating an IO

class IO[A](private val effect: () => A) {

def map[B](f: A => B): IO[B] =

new IO(() => f(effect()))

def flatMap[B](f: A => IO[B]): IO[B] =

new IO(() => f(effect()).effect())

def unsafeRunSync(): A = effect()

}

object IO {

def apply[A](a: => A): IO[A] = new IO(() => a)

}

27 of 35

Creating an IO

28 of 35

Pure function ~ Referentially transparent

def ioSum(in: Seq[Int]): IO[Int] = IO {

println("aaa")

in.sum

}

val res = for {

r1 <- ioSum(Seq(1, 2, 3))

r2 <- ioSum(Seq(1, 2, 3))

} yield r1 + r2

val x = ioSum(Seq(1, 2, 3))

val res = for {

r1 <- x

r2 <- x

} yield r1 + r2

29 of 35

Pure function ~ Referentially transparent

def ioSum(in: Seq[Int]): IO[Int] = IO {

println("aaa")

in.sum

}

val res = for {

r1 <- ioSum(Seq(1, 2, 3))

r2 <- ioSum(Seq(1, 2, 3))

} yield r1 + r2

res.unsafeRunSync()

val x = ioSum(Seq(1, 2, 3))

val res = for {

r1 <- x

r2 <- x

} yield r1 + r2

res.unsafeRunSync()

// aaa

// aaa

// aaa

// aaa

30 of 35

Pure function ~ Referentially transparent

def ioSum(in: Seq[Int]): IO[Int] = IO {

println("aaa")

in.sum

}

val res = for {

r1 <- ioSum(Seq(1, 2, 3))

r2 <- ioSum(Seq(1, 2, 3))

} yield r1 + r2

res.unsafeRunSync()

val x = ioSum(Seq(1, 2, 3))

val res = for {

r1 <- x

r2 <- x

} yield r1 + r2

res.unsafeRunSync()

// aaa

// aaa

// aaa

// aaa

31 of 35

Pure function ~ Referentially transparent

32 of 35

Pure vs impure

def trySum(in: Seq[Int]): Try[Int] = Try {

println("aaa")

in.sum

}

val res = for {

r1 <- trySum(Seq(1, 2, 3))

r2 <- trySum(Seq(1, 2, 3))

} yield r1 + r2

res.get

def ioSum(in: Seq[Int]): IO[Int] = IO {

println("aaa")

in.sum

}

val res = for {

r1 <- ioSum(Seq(1, 2, 3))

r2 <- ioSum(Seq(1, 2, 3))

} yield r1 + r2

res.unsafeRunSync()

Execute a program

Build a program

Execute it

33 of 35

34 of 35

Conclusion

35 of 35

Conclusion