class: center, middle
🤔
Jules Ivanic • @guizmaii
???
- Bonjour
- Merci à vous
- Merci à Scaleway
- Mathieu
-
30 years old
-
Professional developer for 7 years.
-
Mostly Scala/FP developer today
-
Ex-CTO @ Colisweb .small[(Ils recrutent: https://www.welcometothejungle.co/fr/companies/colisweb)]
-
Always learning
-
Passionate surfer since I'm 11. Leaving to Australia in June 🌊🏄
-
Motivation
-
Disclaimer
-
Functional programming ? 🤔
-
What is a function in (pure) FP ?
-
Why do we restrict ourself to pure functions ?
-
Imperative OO
-
Impure FP
-
Pure FP
-
Conclusion
-
Questions
class: center, middle
.center[
Explain my transition .bold[from Impure FP to Pure FP] and its benefits
but
.center[
Explain my transition .bold[from Impure FP to Pure FP] and its benefits
but
I ended up writing a kind of (bad) Rosetta Stone. 😅
] --- class: center, middle.center[
]
??? Apprendre est un processus personnel. Je ne peux rien vous apprendre. Je peux vous montrer, vous expliquer mais la compréhension de ce que je vous montre/explique se passe dans votre tête.
Apprendre demande du travail personnel. Vous n'apprenez pas juste en regardant un talk.
Sur des sujets non triviaux, il faut que vous trouviez votre propre "intuition". C'est cette intuition qui vous permet de comprendre les choses complexes.
Je peux vous partager mon intuition, plus quelques autres si j'en connais d'autres. Si une de ces intuitions vous parle, cool. Cela va vous permettre de comprendre. Sinon, il va falloir que vous travailliez pour réussir à trouver votre propre intuition. Ce processus peut-être long. Cela dépend de plein de paramêtres: du sujet, de votre proximité avec celui-ci, etc.
Pour moi, cette notion d'"intuition" est essentiel, c'est pourquoi je tenais à prendre ces deux minutes pour vous en parler.
class: center, middle
.center[ ## Functional Programing is programming with functions ] --- # 3. Functional programming ? 🤔
.center[ ## Functional Programing is programming with functions ]
.center[
]
.bold[What does this mean ?]
It means that functions are .bold[first-class values]:
val f: `String => Int` = (s: String) => s.size // a function is a Type, just like String, Int, Person, etc.
def map[F[_], A, B](a: F[A], `f: A => B`): F[B] // A function can take a function in parameter
def parse[A, B](value: String): `(A, String) => B` // A function can return a function
.center[
]
.bold[What does this mean ?]
.center[
]
.bold[What does this mean ?]
.bold[What is programming ?]
.center[
]
.bold[What does this mean ?]
.bold[What is programming ?]
.center[
]
.bold[What does this mean ?]
.bold[What is programming ?]
.bold[What is a function ?]
class: center, middle
.center[
]
A pure function is a function that respects:
-
.bold[Totality] (defined for every possible values of its input types)
-
.bold[Determinism] (same inputs will always produce same outputs)
-
.bold[Referencial transparency] (produce its output and nothing else)
A pure function is a function that respects:
- .bold[Totality] (defined for every possible values of its input types)
.underlined[Counter examples:]
def divide0(a: Int, b: Int): Int = if (b == 0) null else a / b
def divide1(a: Int, b: Int): Int = if (b == 0) throw new RuntimeException("...") else a / b
-
.bold[Determinism] (same inputs will always produce same outputs)
-
.bold[Referencial transparency] (produce its output and nothing else)
A pure function is a function that respects:
-
.bold[Totality] (defined for every possible values of its input types)
-
.bold[Determinism] (same inputs will always produce same outputs)
.underlined[Counter examples:]
def randMult(a: Int): Int = Random.nextInt * a
def isToday(d: Date): Boolean = Date.today == d
- .bold[Referencial transparency] (produce its output and nothing else)
A pure function is a function that respects:
-
.bold[Totality] (defined for every possible values of its input types)
-
.bold[Determinism] (same inputs will always produce same outputs)
-
.bold[Referencial transparency] (produce its output and nothing else)
.underlined[Counter examples:]
def mult(a: Int, b: Int): Int = {
val res = a * b
launchMissile()
res
}
A pure function is a function that respects:
-
.bold[Totality] (defined for every possible values of its input types)
-
.bold[Determinism] (same inputs will always produce same outputs)
-
.bold[Referencial transparency] (produce its output and nothing else)
.underlined[Counter examples:]
def mult(a: Int, b: Int): Int = {
val res = a * b
launchMissile()
res
}
// This simplistic program:
val r0 = mult(1, 2) + mult(2, 3)
A pure function is a function that respects:
-
.bold[Totality] (defined for every possible values of its input types)
-
.bold[Determinism] (same inputs will always produce same outputs)
-
.bold[Referencial transparency] (produce its output and nothing else)
.underlined[Counter examples:]
def mult(a: Int, b: Int): Int = {
val res = a * b
launchMissile()
res
}
// This simplistic program:
val r0 = mult(1, 2) + mult(2, 3)
// Is not equivalent to this one:
val r1 = 2 + 6
A pure function is a function that respects:
-
.bold[Totality] (defined for every possible values of its input types)
-
.bold[Determinism] (same inputs will always produce same outputs)
-
.bold[Referencial transparency] (produce its output and nothing else)
.underlined[Counter examples:]
def mult(a: Int, b: Int): Int = {
val res = a * b
launchMissile()
res
}
// This simplistic program:
val r0 = mult(1, 2) + mult(2, 3)
// Is not equivalent to this one:
val r1 = 2 + 6
// So the 'mult' function is not referencially transparent
class: center, middle
Because this constraint enables a simple and natural mode of .bold[reasoning] about program evaluation called the .bold[substitution model].
Because this constraint enables a simple and natural mode of .bold[reasoning] about program evaluation called the .bold[substitution model].
Because this constraint enables a simple and natural mode of .bold[reasoning] about program evaluation called the .bold[substitution model].
if we know that .blue[f(x)
] returns .blue[y
] and know that f
is pure
then we can replace any .blue[f(x)
] call by its result, .blue[y
]
Because this constraint enables a simple and natural mode of .bold[reasoning] about program evaluation called the .bold[substitution model].
if we know that .blue[f(x)
] returns .blue[y
] and know that f
is pure
then we can replace any .blue[f(x)
] call by its result, .blue[y
]
.bold[without the fear of changing] the behavior of the program
Because this constraint enables a simple and natural mode of .bold[reasoning] about program evaluation called the .bold[substitution model].
if we know that .blue[f(x)
] returns .blue[y
] and know that f
is pure
then we can replace any .blue[f(x)
] call by its result, .blue[y
]
.bold[without the fear of changing] the behavior of the program
In other words, pure functions enable .bold[equational reasoning] about programs.
What Referential Transparency can do for you - Luka Jacobowitz ??? raisonnement équationnel
It's a HUGE benefit because, now your program are really simple to read and understand.
You don't anymore have to track variables mutations (immutability ❤️), nor any external state, nor find your way in multiples ways.
There's one way of evaluation: from the top of the tree to the bottom.
class: center, middle
// I used to write things like that...
class Person {
private String name;
private Int age;
private String address;
public Person(String name, Int age, String address) {
if (age < 0 || age > 120) {
throw new RuntimeException("age is invalid");
}
this.name = name; this.age = age; this.address = address;
}
public String getName {
return this.name;
}
public void setName(String name) {
this.name = name;
}
...
}
// I used to write things like that...
class PersonRepo {
private DB dbInstance;
public PersonRepo(DB dbInstance) {
this.dbInstance = dbInstance;
}
public void savePerson(Person p, GpsPoint point) {
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
}
// I used to write things like that...
class GoogleService {
private final HttpClient httpClient;
public GoogleService(HttpClient httpClient) {
this.httpClient = httpClient;
}
public GpsPoint geocode(String address) {
String res =
httpClient
.get("https://api.google.com/geocode/address")
.body("{ \"address\": \"" + address + "\" }")
.execute();
return parse(res);
}
}
class Server extends FrameworkMagic {
private final GoogleService geocoder;
private final PersonRepo repo;
/**
* Accepted JSON requests:
*
* { "name": "Jules", "age": 30, "address": "Sydney, Australia" }
*/
@Put("/person")
public HttpResponse createPerson(@Body Person person) {
* // TODO: Your mission, if you accept, is to code this method.
}
}
@Put("/person")
public HttpResponse createPerson(@Body Person person) {
GpsPoint point = geocoder.geocode(person.address);
repo.savePerson(person, point);
return HttpResponse.Created("Done 👍");
}
@Put("/person")
public HttpResponse createPerson(@Body Person person) {
GpsPoint point = geocoder.geocode(person.address);
repo.savePerson(person, point);
return HttpResponse.Created("Done 👍");
}
.bold[Your nice ignorant boss 🤴 :] Nice work. Here's your 💵
@Put("/person")
public HttpResponse createPerson(@Body Person person) {
GpsPoint point = geocoder.geocode(person.address);
repo.savePerson(person, point);
return HttpResponse.Created("Done 👍");
}
Problems in this code:
- No error handling
- Synchronous
- Uncontrolled side effects
- mutable
@Put("/person")
public HttpResponse createPerson(@Body Person person) {
GpsPoint point = geocoder.geocode(person.address);
repo.savePerson(person, point);
return HttpResponse.Created("Done 👍");
}
.bold[ Let's improve that piece of code! ]
We'll make it asynchronous and we'll handle errors in the same time
exclude: true
.center[
Synchronous | Asynchronous | |
---|---|---|
.bold[blocking] | Waste LOTS OF CPU resources | Unsafe, waste resources |
.bold[non-blocking] | Nonsensical | Holy Grail 🏆 |
] |
class: center, middle
First, let's rewrite the Person
class ...
class Person {
private String name;
private Int age;
private String address;
public Person(String name, Int age, String address) {
if (age < 0 || age > 120) {
throw new RuntimeException("age is invalid");
}
this.name = name; this.age = age; this.address = address;
}
public String getName {
return this.name;
}
public void setName(String name) {
this.name = name;
}
...
}
... in Scala
final case class Person(name: String, age: Int, address: String)
... in .bold[Scala]
final case class Person(name: String, age: Int, address: String)
... in .bold[Scala]
final case class Person(name: String, age: Int, address: String)
.bold[BOOOMM] 🤩
But why choose Scala ? Why not stay with Java ?
final case class Person(name: String, age: Int, address: String)
object Person {
def apply(name: String, age: Int, address: String): Person =
if (age < 0 || age > 120) throw new RuntimeException("age is invalid")
else new Person(name, age, address)
}
// Usage example:
val jules = Person("Jules", 30, "Sydney, Australia")
final case class Person(name: String, age: Int, address: String)
object Person {
def apply(name: String, age: Int, address: String): Person =
if (age < 0 || age > 120) throw new RuntimeException("age is invalid")
else new Person(name, age, address)
}
// Usage example:
val jules = Person("Jules", 30, "Sydney, Australia")
Now, it's equivalent to the Java code.
final case class Person(name: String, age: Int, address: String)
object Person {
def apply(name: String, age: Int, address: String): `Person` =
if (age < 0 || age > 120) `throw new RuntimeException("age is invalid")`
else new Person(name, age, address)
}
// Usage example:
val jules = Person("Jules", 30, "Sydney, Australia")
Now, it's equivalent to the Java code.
.bold[Except that it's immutable] 🤩
exclude: true class: center, middle
By default immutability is the absolute minimum you should have in your programming language and its stdlib collections.
.right.small[Jules Ivanic]
.left.small[https://twitter.com/guizmaii/status/966370888898744320] .left.small[https://twitter.com/guizmaii/status/1036580744360062976]
final case class Person(name: String, age: Int, address: String)
object Person {
// bad
def apply(name: String, ...): Person =
if (age < 0 || age > 120) throw new RuntimeException("age is invalid")
else new Person(name, age, address)
}
final case class Person(name: String, age: Int, address: String)
object Person {
// bad
def apply(name: String, ...): Person =
if (age < 0 || age > 120) throw new RuntimeException("age is invalid")
else new Person(name, age, address)
// better
def apply2(name: String, ...): Option[Person] =
if (age < 0 || age > 120) None
else Some(new Person(name, age, address))
}
final case class Person(name: String, age: Int, address: String)
object Person {
// bad
def apply(name: String, ...): Person =
if (age < 0 || age > 120) throw new RuntimeException("age is invalid")
else new Person(name, age, address)
// better
def apply2(name: String, ...): Option[Person] =
if (age < 0 || age > 120) None
else Some(new Person(name, age, address))
// Why not
def apply3(name: String, ...): Either[RuntimeException, Person] =
if (age < 0 || age > 120) Left(new RuntimeException("age is invalid"))
else Right(new Person(name, age, address))
}
final case class Person(name: String, age: Int, address: String)
object Person {
// bad
def apply(name: String, ...): Person =
if (age < 0 || age > 120) throw new RuntimeException("age is invalid")
else new Person(name, age, address)
// better
def apply2(name: String, ...): Option[Person] =
if (age < 0 || age > 120) None
else Some(new Person(name, age, address))
// Why not
def apply3(name: String, ...): Either[RuntimeException, Person] =
if (age < 0 || age > 120) Left(new RuntimeException("age is invalid"))
else Right(new Person(name, age, address))
// Even better
final case class InvalidPersonAge(invalidAge: Int) extends RuntimeException(s"age is invalid: $invalidAge")
def apply4(name: String, ...): Either[InvalidPersonAge, Person] =
if (age < 0 || age > 120) Left(InvalidPersonAge(age))
else Right(new Person(name, age, address))
}
final case class Person(name: String, age: Int, address: String)
object Person {
// Even better
final case class InvalidPersonAge(invalidAge: Int) extends RuntimeException(s"age is invalid: $invalidAge")
def apply4(name: String, ...): Either[InvalidPersonAge, Person] =
if (age < 0 || age > 120) Left(InvalidPersonAge(age))
else Right(new Person(name, age, address))
}
final case class Person(name: String, age: Int, address: String)
object Person {
// Even better
final case class InvalidPersonAge(invalidAge: Int) extends RuntimeException(s"age is invalid: $invalidAge")
def apply4(name: String, ...): Either[InvalidPersonAge, Person] =
if (age < 0 || age > 120) Left(InvalidPersonAge(age))
else Right(new Person(name, age, address))
// Holy Grail 🏆 (PCE == PersonCreationException)
sealed abstract class `PCE`(message: String) extends RuntimeException(message)
final case class InvalidPersonAge(invalidAge: Int) extends PCE(s"age is invalid: $invalidAge")
final case class TooDumbNameException(name: String) extends PCE(s"choosed name is too dumb: $name")
...
def apply🏆(name: String, ...): Either[`PCE`, Person] =
if (age < 0 || age > 120) Left(InvalidPersonAge(age))
else if (name == "Harry Potter") Left(TooDumbNameException(name))
...
else Right(new Person(name, age, address))
}
final case class Person(name: String, age: Int, address: String)
object Person {
// Holy Grail 🏆 (PCE == PersonCreationException)
sealed abstract class `PCE`(message: String) extends RuntimeException(message)
final case class InvalidPersonAge(invalidAge: Int) extends PCE(s"age is invalid: $invalidAge")
final case class TooDumbNameException(name: String) extends PCE(s"choosed name is too dumb: $name")
...
def apply🏆(name: String, ...): Either[`PCE`, Person] =
if (age < 0 || age > 120) Left(InvalidPersonAge(age))
else if (name == "Harry Potter") Left(TooDumbNameException(name))
...
else Right(new Person(name, age, address))
}
final case class Person(name: String, age: Int, address: String)
object Person {
// Holy Grail 🏆 (PCE == PersonCreationException)
sealed abstract class `PCE`(message: String) extends RuntimeException(message)
final case class InvalidPersonAge(invalidAge: Int) extends PCE(s"age is invalid: $invalidAge")
final case class TooDumbNameException(name: String) extends PCE(s"choosed name is too dumb: $name")
...
def apply🏆(name: String, ...): Either[`PCE`, Person] =
if (age < 0 || age > 120) Left(InvalidPersonAge(age))
else if (name == "Harry Potter") Left(TooDumbNameException(name))
...
else Right(new Person(name, age, address))
}
What's the problem with throwing exceptions ?
- Breaks linear execution path
- Makes methods' signature lie
- Impossible to know if a method is throwing without reading its code and all the code it calls! .bold[(Takes infinite time)]
- it's an uncontrolled side effect
exclude: true class: center, middle
With imperative programming, it's impossible to produce a safe and deterministic program in a finite time.
It's impossible to know if a method is throwing without reading its code and all the code it calls which takes an infinite time.
final case class Person(name: String, age: Int, address: String)
object Person {
// Holy Grail 🏆 (PCE == PersonCreationException)
sealed abstract class `PCE`(message: String) extends RuntimeException(message)
final case class InvalidPersonAge(invalidAge: Int) extends PCE(s"age is invalid: $invalidAge")
final case class TooDumbNameException(name: String) extends PCE(s"choosed name is too dumb: $name")
...
def apply🏆(name: String, ...): Either[`PCE`, Person] =
if (age < 0 || age > 120) Left(InvalidPersonAge(age))
else if (name == "Harry Potter") Left(TooDumbNameException(name))
...
else Right(new Person(name, age, address))
}
Now that we have our Person
constructor correctly implemented,
let's continue our refactoring by fixing the PersonRepo
code.
Reminder: Here's the Java code we'll rewrite ...
class PersonRepo {
private DB dbInstance;
public PersonRepo(DB dbInstance) {
this.dbInstance = dbInstance;
}
public void savePerson(Person p, GpsPoint point) {
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
}
... in ("Java++") Scala
class PersonRepo(dbInstance: DB) {
def savePerson(p: Person, point: GpsPoint): Unit =
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
exclude: true
... in .bold[(blocking synchronous impure FP)] Scala
import scala.util.Try
class PersonRepo(dbInstance: DB) {
def savePerson(p: Person, point: GpsPoint): `Try[Unit]` =
`Try {`
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
`}`
}
... in impure FP Scala
import scala.concurrent.{ExecutionContext, Future, blocking}
class PersonRepo(dbInstance: DB) {
def savePerson(p: Person, point: GpsPoint)(implicit ex: ExecutionContext): `Future[Unit]` =
`Future {`
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
`}`
}
exclude: true
... in .bold[(.red[non-]blocking .red[a]synchronous impure FP)] Scala
import scala.concurrent.{ExecutionContext, Future}
class PersonRepo(dbInstance: `AsyncNIODB`) {
def savePerson(p: Person, point: GpsPoint)(implicit ex: ExecutionContext): `Future[Unit]` =
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
Now, let's do quickly the same thing for the GoogleService
.
Reminder: Here's the Java code we'll rewrite ...
class GoogleService {
private final HttpClient httpClient;
public GoogleService(HttpClient httpClient) {
this.httpClient = httpClient;
}
public GpsPoint geocode(String address) {
String res =
httpClient
.get("https://api.google.com/geocode/address")
.body("{ \"address\": \"" + address + "\" }")
.execute();
return parse(res);
}
}
... in impure FP Scala
import scala.concurrent.{ExecutionContext, Future}
class GoogleService(httpClient: HttpClient) {
// 'map' signature: Future[A].map[B](f: A => B): Future[B]
def geocode(address: String)(implicit ex: ExecutionContext): `Future[GpsPoint]` =
`Future {`
httpClient
.get("https://api.google.com/geocode/address")
.body("{ \"address\": \"" + address + "\" }")
.execute()
`}`
`.map(res: String => parse(res))`
}
exclude: true
... in impure FP Scala
import scala.concurrent.{ExecutionContext, Future}
class GoogleService(httpClient: HttpClient) {
// 'map' signature: Future[A].map[B](f: A => B): Future[B]
def geocode(address: String)(implicit ex: ExecutionContext): Future[GpsPoint] =
Future {
httpClient
.get("https://api.google.com/geocode/address")
.body("{ \"address\": \"" + address + "\" }")
.execute()
}
`.map(parse)` // 'parse' signature: String => GpsPoint (I'm assuming that parsing will never fail)
// Side note: A simple and interesting exercice for beginners could be to rewrite this code
// with a 'parse' function which have the following signature: String => Either[ParsingError, GpsPoint]
// 😉
}
Finally, let's rewrite our Server
'createPerson' method.
Here's the Java version we will rewrite ...
@Put("/person")
public HttpResponse createPerson(@Body Person person) {
GpsPoint point = geocoder.geocode(person.address);
repo.savePerson(person, point);
return HttpResponse.Created("Done 👍");
}
... in impure FP Scala
@Put("/person")
def createPerson(`json: Json`)(implicit ec: ExecutionContext): `Future[HttpResponse]` = {
...
}
... in impure FP Scala
@Put("/person")
def createPerson(json: Json)(implicit ec: ExecutionContext): Future[HttpResponse] = {
// 'flatMap' signature: Future[A].flatMap[B](f: A => Future[B]): Future[B]
// 'map' signature: Future[A].map[B](f: A => B): Future[B]
def save(person: Person): Future[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
...
}
... in impure FP Scala
@Put("/person")
def createPerson(json: Json)(implicit ec: ExecutionContext): Future[HttpResponse] = {
// 'flatMap' signature: Future[A].flatMap[B](f: A => Future[B]): Future[B]
def save(person: Person): Future[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
def internalError: HttpResponse = HttpResponse.InternalServerError()
def created: HttpResponse = HttpResponse.Created("Done 👍")
...
}
... in impure FP Scala
@Put("/person")
def createPerson(json: Json)(implicit ec: ExecutionContext): Future[HttpResponse] = {
// 'flatMap' signature: Future[A].flatMap[B](f: A => Future[B]): Future[B]
def save(person: Person): Future[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
def internalError: HttpResponse = HttpResponse.InternalServerError()
def created: HttpResponse = HttpResponse.Created("Done 👍")
val parsed: Either[PCE, Person] = JSON.parse[Person](json)
...
}
... in impure FP Scala
@Put("/person")
def createPerson(json: Json)(implicit ec: ExecutionContext): Future[HttpResponse] = {
// 'flatMap' signature: Future[A].flatMap[B](f: A => Future[B]): Future[B]
def save(person: Person): Future[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
def internalError: HttpResponse = HttpResponse.InternalServerError()
def created: HttpResponse = HttpResponse.Created("Done 👍")
val parsed: Either[PCE, Person] = JSON.parse[Person](json)
parsed match {
case Left(InvalidPersonAge(age)) => Future.successful(HttpResponse.BadRequest(s"Invalid age: $age"))
case Left(TooDumbNameException(name)) => Future.successful(HttpResponse.BadRequest(s"Invalid name: $name"))
...
case Right(person) => save(person).fold(_ => internalError, _ => created)
}
}
TOTO
.bold[Now, let's get back to our primary example:]
impure FP Scala
import scala.concurrent.{ExecutionContext, Future}
class PersonRepo(dbInstance: DB) {
def savePerson(p: Person, point: GpsPoint)(implicit ex: ExecutionContext): Future[Unit] =
Future {
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
}
.bold[Now, let's get back to our primary example:]
impure FP Scala
import scala.concurrent.{ExecutionContext, Future}
class PersonRepo(dbInstance: DB) {
def savePerson(p: Person, point: GpsPoint)(implicit ex: ExecutionContext): Future[Unit] =
Future {
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
}
.bold[Now, let's get back to our primary example:]
impure FP Scala
import scala.concurrent.{ExecutionContext, Future}
class PersonRepo(dbInstance: DB) {
def savePerson(p: Person, point: GpsPoint)(implicit ex: ExecutionContext): `Future[Unit]` =
Future {
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
}
.bold[Why is this code not pure ?]
.bold[Why is the previous code not pure ?] => The problem comes from the Future
.
// This:
def twoMissiles: (Future[Unit], Future[Unit]) = ( Future { launchMissile() }, Future { launchMissile() } )
.bold[Why is the previous code not pure ?] => The problem comes from the Future
.
// This:
def twoMissiles: (Future[Unit], Future[Unit]) = ( Future { launchMissile() }, Future { launchMissile() } )
scala> twoMissiles
🚀
🚀
.bold[Why is the previous code not pure ?] => The problem comes from the Future
.
// This:
def twoMissiles: (Future[Unit], Future[Unit]) = ( Future { launchMissile() }, Future { launchMissile() } )
scala> twoMissiles
🚀
🚀
scala> twoMissiles
🚀
🚀
.bold[Why is the previous code not pure ?] => The problem comes from the Future
.
// This:
def twoMissiles: (Future[Unit], Future[Unit]) = ( Future { launchMissile() }, Future { launchMissile() } )
scala> twoMissiles
🚀
🚀
scala> twoMissiles
🚀
🚀
// is not equivalent to this while it should be if pure:
lazy val oneMissile: Future[Unit] = Future { launchMissile() }
def twoMissiles: (Future[Unit], Future[Unit]) = (oneMissile, oneMissile)
.bold[Why is the previous code not pure ?] => The problem comes from the Future
.
// This:
def twoMissiles: (Future[Unit], Future[Unit]) = ( Future { launchMissile() }, Future { launchMissile() } )
scala> twoMissiles
🚀
🚀
scala> twoMissiles
🚀
🚀
// is not equivalent to this while it should be if pure:
lazy val oneMissile: Future[Unit] = Future { launchMissile() }
def twoMissiles: (Future[Unit], Future[Unit]) = (oneMissile, oneMissile)
scala> twoMissiles
🚀
.bold[Why is the previous code not pure ?] => The problem comes from the Future
.
// This:
def twoMissiles: (Future[Unit], Future[Unit]) = ( Future { launchMissile() }, Future { launchMissile() } )
scala> twoMissiles
🚀
🚀
scala> twoMissiles
🚀
🚀
// is not equivalent to this while it should be if pure:
lazy val oneMissile: Future[Unit] = Future { launchMissile() }
def twoMissiles: (Future[Unit], Future[Unit]) = (oneMissile, oneMissile)
scala> twoMissiles
🚀
scala> twoMissiles
∅
.bold[Why is the previous code not pure ?] => The problem comes from the Future
.
Complete explanation of the problem:
https://stackoverflow.com/questions/27454798/is-future-in-scala-a-monad/27467037#27467037
class: center, middle
Now that we know what a pure function is and we know that Future
s doesn't respect pure function laws.
Now that we know what a pure function is and we know that Future
s doesn't respect pure function laws.
Let's purify our code !
Now that we know what a pure function is and we know that Future
s doesn't respect pure function laws.
Let's purify our code !
.bold[But how ?]
Here's the trick FP devs found to purify everything:
Now that we know what a pure function is and we know that Future
s doesn't respect pure function laws.
Let's purify our code !
.bold[But how ?]
Here's the trick FP devs found to purify everything:
.bold[They don't execute anything!]
final case class Task[A](a: () => A)
val t: Task[GpsPoint] = Task(syncGeocoder.geocode(person.address))
Now that we know what a pure function is and we know that Future
s doesn't respect pure function laws.
Let's purify our code !
.bold[But how ?]
Here's the trick FP devs found to purify everything:
.bold[They don't execute anything!]
final case class Task[A](a: () => A)
val t: Task[GpsPoint] = Task(syncGeocoder.geocode(person.address))
// is very different than:
val f: Future[GpsPoint] = Future(syncGeocoder.geocode(person.address))
Now that we know what a pure function is and we know that Future
s doesn't respect pure function laws.
Let's purify our code !
.bold[But how ?]
Here's the trick FP devs found to purify everything:
.bold[They don't execute anything!]
import scala.concurrent.{Future, Await}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
case class GpsPoint(lat: Double, long: Double)
case class Person(address: String)
class SyncGeocoder {
def geocode(address: String): GpsPoint = GpsPoint(12.23, 32.21)
}
val syncGeocoder = new SyncGeocoder
val person = Person("osef")
final case class Task[A](a: () => A)
object Task {
def apply[A](a: => A)(implicit dummy: DummyImplicit): Task[A] = new Task(() => a)
}
val t: Task[GpsPoint] = Task(syncGeocoder.geocode(person.address)) // lazy
// is very different than:
val f: Future[GpsPoint] = Future(syncGeocoder.geocode(person.address)) // eager
Now that we know what a pure function is and we know that Future
s doesn't respect pure function laws.
Let's purify our code !
.bold[But how ?]
Here's the trick FP devs found to purify everything:
.bold[They don't execute anything!]
import scala.concurrent.{Future, Await}
import scala.concurrent.ExecutionContext.Implicits.global
import scala.concurrent.duration._
final case class GpsPoint(lat: Double, long: Double)
final case class Person(address: String)
class SyncGeocoder {
def geocode(address: String): GpsPoint = GpsPoint(12.23, 32.21)
}
val syncGeocoder = new SyncGeocoder
val person = Person("osef")
final case class Task[A](a: () => A)
object Task {
def apply[A](a: => A)(implicit dummy: DummyImplicit): Task[A] = new Task(() => a)
}
val t: Task[GpsPoint] = Task(syncGeocoder.geocode(person.address)) // lazy
// is very different than:
val f: Future[GpsPoint] = Future(syncGeocoder.geocode(person.address)) // eager
Await.result(f, 10.seconds)
So, let's rewrite our impure FP Scala code.
final case class Person(name: String, age: Int, address: String)
object Person {
// Holy Grail 🏆 (PCE == PersonCreationException)
sealed abstract class `PCE`(message: String) extends RuntimeException(message)
final case class InvalidPersonAge(invalidAge: Int) extends PCE(s"age is invalid: $invalidAge")
final case class TooDumbNameException(name: String) extends PCE(s"choosed name is too dumb: $name")
...
def apply🏆(name: String, ...): Either[`PCE`, Person] =
if (age < 0 || age > 120) Left(InvalidPersonAge(age))
else if (name == "Harry Potter") Left(TooDumbNameException(name))
...
else Right(new Person(name, age, address))
}
Here's our PersonRepo
impure FP code ...
import scala.concurrent.{ExecutionContext, Future}
class PersonRepo(dbInstance: DB) {
def savePerson(p: Person, point: GpsPoint)(implicit ex: ExecutionContext): Future[Unit] =
Future {
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
}
... now pure.
import monix.eval.Task
class PersonRepo(dbInstance: DB) {
def savePerson(p: Person, point: GpsPoint): `Task[Unit]` =
`Task` {
dbInstance.query(
"INSERT INTO PERSONS (name, age, address, gpsPoint) VALUES (?, ?, ?, ?)",
p.name, p.age, p.address, point
)
}
}
Here's our GoogleService
impure FP code ...
import scala.concurrent.{ExecutionContext, Future}
class GoogleService(httpClient: HttpClient) {
def geocode(address: String)(implicit ex: ExecutionContext): Future[GpsPoint] =
Future {
httpClient
.get("https://api.google.com/geocode/address")
.body("{ \"address\": \"" + address + "\" }")
.execute()
}
.map(res => parse(res))
}
... now pure.
import monix.eval.Task
class GoogleService(httpClient: HttpClient) {
def geocode(address: String): `Task[GpsPoint]` =
`Task` {
httpClient
.get("https://api.google.com/geocode/address")
.body("{ \"address\": \"" + address + "\" }")
.execute()
}
.map(res => parse(res))
}
And finally, let's purify our Server
impure code ...
@Put("/person")
def createPerson(json: Json)(implicit ec: ExecutionContext): Future[HttpResponse] = {
def save(person: Person): Future[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
def internalError: HttpResponse = HttpResponse.InternalServerError()
def created: HttpResponse = HttpResponse.Created("Done 👍")
val parsed: Either[PCE, Person] = JSON.parse[Person](json)
parsed match {
case Left(InvalidPersonAge(age)) => Future.successful(HttpResponse.BadRequest(s"Invalid age: $age"))
case Left(TooDumbNameException(name)) => Future.successful(HttpResponse.BadRequest(s"Invalid name: $name"))
...
case Right(person) => save(person).fold(_ => internalError, _ => created)
}
}
... now pure.
@Put("/person")
def createPerson(json: Json): `Future[HttpResponse]` = {
...
}
... now pure.
@Put("/person")
def createPerson(json: Json): Future[HttpResponse] = {
def save(person: Person): `Task[Unit]` =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
...
}
... now pure.
@Put("/person")
def createPerson(json: Json): Future[HttpResponse] = {
def save(person: Person): Task[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
def internalError: HttpResponse = HttpResponse.InternalServerError()
def created: HttpResponse = HttpResponse.Created("Done 👍")
val parsed: `Task[Either[PCE, Person]]` = `Task.now(JSON.parse[Person](json))`
...
}
... now pure.
@Put("/person")
def createPerson(json: Json): Future[HttpResponse] = {
def save(person: Person): Task[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
def internalError: HttpResponse = HttpResponse.InternalServerError()
def created: HttpResponse = HttpResponse.Created("Done 👍")
val parsed: Task[Either[PCE, Person]] = Task.now(JSON.parse[Person](json))
parsed`.flatMap(`_ match {
case Left(InvalidPersonAge(age)) => `Task.now`(HttpResponse.BadRequest(s"Invalid age: $age"))
case Left(TooDumbNameException(name)) => `Task.now`(HttpResponse.BadRequest(s"Invalid name: $name"))
...
case Right(person) => save(person).fold(_ => internalError, _ => created)
}`)`
}
... now pure.
@Put("/person")
def createPerson(json: Json): Future[HttpResponse] = {
def save(person: Person): Task[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
def internalError: HttpResponse = HttpResponse.InternalServerError()
def created: HttpResponse = HttpResponse.Created("Done 👍")
val parsed: Task[Either[PCE, Person]] = Task.now(JSON.parse[Person](json))
parsed`.flatMap(`_ match {
case Left(InvalidPersonAge(age)) => `Task.now`(HttpResponse.BadRequest(s"Invalid age: $age"))
case Left(TooDumbNameException(name)) => `Task.now`(HttpResponse.BadRequest(s"Invalid name: $name"))
...
case Right(person) => save(person).fold(_ => internalError, _ => created)
}`)`
}
... now pure.
@Put("/person")
def createPerson(json: Json): Future[HttpResponse] = {
def save(person: Person): Task[Unit] =
geocoder
.geocode(person.address)
.flatMap(gpsPoint => repo.savePerson(person, gpsPoint))
def internalError: HttpResponse = HttpResponse.InternalServerError()
def created: HttpResponse = HttpResponse.Created("Done 👍")
val parsed: Task[Either[PCE, Person]] = Task.now(JSON.parse[Person](json))
`val program: Task[HttpResponse] =`
parsed`.flatMap(`_ match {
case Left(InvalidPersonAge(age)) => `Task.now`(HttpResponse.BadRequest(s"Invalid age: $age"))
case Left(TooDumbNameException(name)) => `Task.now`(HttpResponse.BadRequest(s"Invalid name: $name"))
...
case Right(person) => save(person).fold(_ => internalError, _ => created)
}`)`
`TaskRuntime.runToFuture(program)`
}
The previous Task
data structure is an implementation of .bold[the IO Monad].
The IO Monad is sometimes named .blue[IO
], sometimes .blue[Task
], etc.
It'll depend on your programming language and/or the implementation you choose.
In Scala, there're 3 different implementations:
- .underlined[Monix], which named it .blue[
Task
]: https://monix.io/ - .underlined[Cats-Effects], which named it .blue[
IO
]: https://typelevel.org/cats-effect/ - .underlined[ZIO], which implemented a bifunctor IO Monad named .blue[
ZIO
]: https://zio.dev/
Now, you're able to understand the signature of the Haskell main
function:
main :: IO () -- '()' == 'Unit' in Scala. So, the equivalent signature in Scala is `IO[Unit]`
Now, you're able to understand the signature of the Haskell main
function:
main :: IO () -- '()' == 'Unit' in Scala. So, the equivalent signature in Scala is `IO[Unit]`
And also see why Scala is not a pure programming language:
def main(args: Array[String]): Unit
class: center, middle
.center[
.center[
.center[
class: center, middle
Slides at https://guizmaii.github.io/LilleFP
Code of the slides at https://github.com/guizmaii/LilleFP