19 ans d’expérience professionnelle
dans le développement
ESN 😮
Plus récemment dans un organisme de paiement
Actuellement chez un éditeur SaaS
dans la data
Principalement autour des langages JVM
Quelle expérience avec des langages fonctionnels ?
Connaissez-vous les mots-clés ?
Mais rentrons dans le vif du sujet
FP…
E → F
x ↦ f(x)
Il s’agit d’une transformation
d’une valeur appartement à l’ensemble de départ
vers une valeur appartenant à l’ensemble d’arrivée.
enum TaskStatus:
case Created, Started, InReview, Done
enum TaskTransition:
case Start, Finish, RequestChange, Accept
def findTransitions: TaskStatus => Seq[TaskTransition] =
case Created => Seq(Start)
case Started => Seq(Finish)
case InReview => Seq(RequestChange, Accept)
case Done => Seq()
Toujours chercher à diminuer la taille des ensembles de départ et d'arrivée permet de
Mettre en place un environnement de développement pour écrire sa première fonction en Scala
def sayHello(niceMessage: String): Unit =
println(s"Hello $niceMessage")
sayHello("world!")
Hello world!
def dice(): Int =
Random.nextInt(6) + 1
dice()
val res0: Int = 5
dice()
val res1: Int = 3
def nowPlus(days: Long): Instant =
Instant.now().plus(days, ChronoUnit.DAYS)
nowPlus(3)
val res0: java.time.Instant = 2024-03-05T18:55:34.125784Z
nowPlus(5)
val res1: java.time.Instant = 2024-03-07T18:55:34.215660Z
def saveArticle(id: UUID, content: String, author: Author)(
database: Database
): Boolean =
database.executeQuery(
"""INSERT INTO Article(id, content, author_name, author_email)
|VALUES (:id, :content, :author_name, :author_email);
|""".stripMargin,
"id" -> id,
"content" -> content,
"author_name" -> author.name,
"author_email" -> author.email
)
saveArticle(
UUID.fromString("0d975fff-44dc-4110-b21a-1f31148969b8"),
"<p>Great content</p>",
Author("Sébastian", "seb@example.com")
)(postgresql)
val res0: Boolean = true
Toutes ces fonctions sont impures car :
Permet donc des optimisations massivement parallélisables
Écrire une fonction pure qui débarrasse les emails suivant du bruit:
"kai@example.com sacha@example.net" "cruz@example.org" noam@example.com
libraryDependencies +=
"org.scalameta" %% "munit" % "0.7.29" % Test
import munit.FunSuite
class EmailCleaner extends FunSuite:
private def clean(notCleanedEmail: String): String = ???
test("emailCleaner"):
assertEquals(
obtained = clean(""""kai@example.com"""),
expected = "kai@example.com"
)
import EmailCleaner.clean
import munit.FunSuite
class EmailCleanerSuite extends FunSuite:
test("remove starting quote"):
assertEquals(clean(""""kai@example.com"""),
"kai@example.com")
test("remove trailing quote"):
assertEquals(clean("""sacha@example.net""""),
"sacha@example.net")
test("remove starting and trailing quote"):
assertEquals(clean(""""cruz@example.org""""),
"cruz@example.org")
test("remove starting and trailing spaces"):
assertEquals(clean(""" noam@example.com """),
"noam@example.com")
object EmailCleaner:
private val startWithQuote = """^"(.+)""".r
private val endsWithQuote = """(.+)"$""".r
private val startAndEndWithQuote = """^"(.+)"$""".r
private val startAndEndWithSpaces = """^\s+(.*?)\s+$""".r
def clean(notCleanedEmail: String): String =
notCleanedEmail match
case startAndEndWithQuote(email) => email
case startWithQuote(email) => email
case endsWithQuote(email) => email
case startAndEndWithSpaces(email) => email
case _ => notCleanedEmail
Les fonctions pures sont importantes : ce sont les blocs de base de la programmation fonctionnelle.
Mais comment aller plus loin et finir par écrire de vrais programmes ?
f ∘ g
f(g(x))
Il s’agit d’un moyen de garantir
case class User(name: String, age: Int)
enum Category:
case Young, Adult, Old
val categoryByMaxAges: Map[Int, Category] =
Map(
24 -> Category.Young,
64 -> Category.Adult,
Integer.MAX_VALUE -> Category.Old
)
val discountByCategories: Map[Category, Int] =
Map(
Category.Young -> 25,
Category.Adult -> 0,
Category.Old -> 35
)
def computePrice(
user: User,
categories: Map[Int, Category],
price: Long,
discounts: Map[Category, Int]
): Long = price - price * discounts(
categories
.filter { case (maxAge, category) => maxAge >= user.age }
.minBy { case (maxAge, category) => maxAge }
._2
) / 100
computePrice( computePrice( computePrice(
User("Emma", 16), User("Nicolas", 41), User("Martine", 67),
categoryByMaxAges, categoryByMaxAges, categoryByMaxAges,
price = 1999, price = 5500, price = 4999,
discountByCategories discountByCategories discountByCategories
) ) )
val res0: Long = 1500 val res1: Long = 5500 val res2: Long = 3250
def computePrice(
user: User,
categories: Map[Int, Category],
price: Long,
discounts: Map[Category, Int]
): Long =
computePrice(
price,
computeDiscount(user, categories, discounts)
)
def computePrice(price: Long, discount: Long): Long =
price - price * discount / 100
def computeDiscount(
user: User,
categories: Map[Int, Category],
discounts: Map[Category, Int]
): Long = discounts(findCategory(user, categories))
def findCategory(
user: User,
categories: Map[Int, Category]
): Category =
val (_, category) = categories
.filter { case (maxAge, category) => maxAge >= user.age }
.minBy { case (maxAge, category) => maxAge }
category
times2
qui multiplie un Int
par 2addOne
qui ajoute 1 au Int
passé en paramètreInt
par 2 puis ajoute 1.object Composition:
def times2(value: Int): Int = value * 2
def addOne(value: Int): Int = value + 1
def times2PlusOne(value: Int): Int = addOne(times2(value))
@main def compositionMain(): Unit =
println(times2PlusOne(5)) // 11
Une fonction est considérée comme un type de données comme les autres.
def logBeforeAndAfter(message: String, task: () => Unit): Unit =
println(s"before $message")
task()
println(s" after $message")
logBeforeAndAfter("hello", () => { println("Hello World!") })
before hello
Hello World!
after hello
def logBeforeAndAfterResult(
message: String,
computeResult: String => Long
): Unit =
println(s"before $message")
println(s"result ${computeResult(message)}")
println(s" after $message")
logBeforeAndAfterResult("hello", message => message.length)
before hello
result 5
after hello
Une fonction est considérée comme un type de données comme les autres.
def operationToCompute(operation: String): (Long, Long) => Long =
operation match
case "add" => (left, right) => left + right
case "subtract" => (left, right) => left - right
case "multiply" => (left, right) => left * right
case _ => (left, right) => Long.MinValue
operationToCompute("add" )(3, 2)
val res0: Long = 5
operationToCompute("subtract")(3, 2)
val res1: Long = 1
operationToCompute("multiply")(3, 2)
val res2: Long = 6
operationToCompute("divide" )(3, 2)
val res3: Long = -9223372036854775808
Utiliser compose
et/ou andThen
pour composer times2
et addOne
object Composition:
def times2(value: Int): Int = value * 2
def addOne(value: Int): Int = value + 1
def times2PlusOne(value: Int): Int = addOne(times2(value))
def withCompose(value: Int): Int = addOne.compose(times2)(value)
def withAndThen(value: Int): Int = times2.andThen(addOne)(value)
@main def compositionMain(): Unit =
println(times2PlusOne(5)) // 11
println(withCompose(5)) // 11
println(withAndThen(5)) // 11
La possibilité pour une fonction de se rappeler elle-même. Permet de traiter :
L’appel récursif est la dernière évaluation dans l’implémentation de la fonction
Permet d’éviter des épuisements de la pile d’exécution (aka StackOverflow®)
val input = """34
|84
|12
|
|32
|28
|9
|7""".stripMargin
@tailrec
def sums(
input: List[String],
acc: Seq[Long] = Vector.empty,
sum: Long = 0
): Seq[Long] =
input match
case Nil => acc :+ sum
case "" :: tail => sums(tail, acc :+ sum)
case n :: tail => sums(tail, acc, sum + n.toLong)
sums(input.linesIterator.toList)
val res0: Seq[Long] = Vector(130, 76)
Calculer la somme
du premier et du dernier chiffre
de chacune des lignes
en n’utilisant que des fonctions récursives
two1nine eightwothree abcone2threexyz xtwone3four 4nineeightseven2 zoneight234 7pqrstsixteen
Calculer la somme
du premier et du dernier chiffre
de chacune des lignes
en n’utilisant que des fonctions récursives
two1nine -> 219 -> 29 eightwothree -> 8wo3 -> 83 abcone2threexyz -> abc123xyz -> 13 xtwone3four -> x2ne34 -> 24 4nineeightseven2 -> 49872 -> 42 zoneight234 -> z1ight234 -> 14 7pqrstsixteen -> 7pqrst6teen -> 76 281
import SumAll.toNumber
import munit.FunSuite
class SumAllSuite extends FunSuite:
test("toNumber"):
assertEquals(toNumber("two1nine"), "219")
assertEquals(toNumber("eightwothree"), "8wo3")
assertEquals(toNumber("abcone2threexyz"), "abc123xyz")
assertEquals(toNumber("xtwone3four"), "x2ne34")
assertEquals(toNumber("4nineeightseven2"), "49872")
assertEquals(toNumber("zoneight234"), "z1ight234")
assertEquals(toNumber("7pqrstsixteen"), "7pqrst6teen")
object SumAll:
def toNumber(value: String): String = value match
case "" => "" // terminating scenario
case s if s.startsWith("one") => "1" + toNumber(s.drop(3))
case s if s.startsWith("two") => "2" + toNumber(s.drop(3))
case s if s.startsWith("three") => "3" + toNumber(s.drop(5))
case s if s.startsWith("four") => "4" + toNumber(s.drop(4))
case s if s.startsWith("five") => "5" + toNumber(s.drop(4))
case s if s.startsWith("six") => "6" + toNumber(s.drop(3))
case s if s.startsWith("seven") => "7" + toNumber(s.drop(5))
case s if s.startsWith("eight") => "8" + toNumber(s.drop(5))
case s if s.startsWith("nine") => "9" + toNumber(s.drop(4))
case s => s.take(1) + toNumber(s.drop(1))
import SumAll.{firstAndLast, toNumber}
import munit.FunSuite
class SumAllSuite extends FunSuite:
test("firstAndLastNumber"):
assertEquals(firstAndLastNumber("219"), "29")
assertEquals(firstAndLastNumber("8wo3"), "83")
assertEquals(firstAndLastNumber("abc123xyz"), "13")
assertEquals(firstAndLastNumber("x2ne34"), "24")
assertEquals(firstAndLastNumber("49872"), "42")
assertEquals(firstAndLastNumber("z1ight234"), "14")
assertEquals(firstAndLastNumber("7pqrst6teen"), "76")
test("toNumber"):
() // …
import scala.util.Try
object SumAll:
def first(value: String): String = value match
case "" => ""
case s if Try(s.take(1).toInt).isSuccess => s.take(1)
case s => first(s.drop(1))
def last(value: String): String = first(value.reverse)
def firstAndLastNumber(value: String): String =
first(value) + last(value)
def toNumber(value: String): String = ??? // …
import SumAll.{firstAndLastNumber, sumAll, toNumber}
import munit.FunSuite
class SumAllSuite extends FunSuite:
test("sumAll"):
assertEquals(sumAll("""two1nine
|eightwothree
|abcone2threexyz
|xtwone3four
|4nineeightseven2
|zoneight234
|7pqrstsixteen""".stripMargin), 281)
test("firstAndLastNumber"):
() // …
test("toNumber"):
() // …
import scala.jdk.StreamConverters.*
import scala.util.Try
object SumAll:
def firstAndLastNumber(value: String): String = ??? // …
def toNumber(value: String): String = ??? // …
def sumAll(values: String): Int =
values.lines().toScala(Vector)
.map(toNumber)
.map(firstAndLastNumber)
.map(_.toInt)
.sum
ADT - Algebraic Data Types
enum TaskStatus:
case Created, Started,
InReview, Done
case class Task(
name: String,
status: TaskStatus
)
Écrire des fonctions
def findTransitions: TaskStatus => Seq[TaskTransition] =
case Created => Seq(Start)
case Started => Seq(Finish)
case InReview => Seq(RequestChange, Accept)
case Done => Seq()
def findTransitions(status: TaskStatus): Seq[TaskTransition] =
status match
case Created => Seq(Start)
case Started => Seq(Finish)
case InReview => Seq(RequestChange, Accept)
case Done => Seq()
Avec les ADT, on force les fonctions à être pures, car ces structures de données sont immuables.
Matrice de lumières : calcul de la consommation énergétique
turn on 0,0 through 999,999
toggle 0,0 through 999,0
turn off 499,499 through 500,500
turn on 887,9 through 959,629
turn on 454,398 through 844,448
turn off 539,243 through 559,965
turn off 370,819 through 676,868
turn off 145,40 through 370,997
turn off 301,3 through 808,453
turn on 351,678 through 951,908
toggle 720,196 through 897,994
toggle 831,394 through 904,860
Combien de lumières sont-elles allumées ?
import ChristmasLights.countLitLights
class ChristmasLightsSuite extends munit.FunSuite:
test("count lit lights after instructions"):
val program = """turn on 887,9 through 959,629
|turn on 454,398 through 844,448
|turn off 539,243 through 559,965
|turn off 370,819 through 676,868
|turn off 145,40 through 370,997
|turn off 301,3 through 808,453
|turn on 351,678 through 951,908
|toggle 720,196 through 897,994
|toggle 831,394 through 904,860""".stripMargin
val count = countLitLights(program)
assertEquals(count, 230_022)
object ChristmasLights:
case class Coordinates(x: Int, y: Int)
enum Instruction:
case ON(corner1: Coordinates, corner2: Coordinates)
case OFF(corner1: Coordinates, corner2: Coordinates)
case TOGGLE(corner1: Coordinates, corner2: Coordinates)
import scala.util.matching.Regex
object ChristmasLights:
object Instruction:
val onRegex: Regex =
"""turn on (\d+),(\d+) through (\d+),(\d+)""".r
val offRegex: Regex = """turn off (\d+),(\d+) through (\d+),(\d+)""".r
val toggleRegex: Regex = """toggle (\d+),(\d+) through (\d+),(\d+)""".r
def apply(line: String): Instruction = line match
case onRegex(x1, y1, x2, y2) =>
ON(Coordinates(x1.toInt, y1.toInt),
Coordinates(x2.toInt, y2.toInt))
case offRegex(x1, y1, x2, y2) => OFF(Coordinates(x1.toInt, y1.toInt), Coordinates(x2.toInt, y2.toInt))
case toggleRegex(x1, y1, x2, y2) => TOGGLE(Coordinates(x1.toInt, y1.toInt), Coordinates(x2.toInt, y2.toInt))
import ChristmasLights.Instruction.{OFF, ON, TOGGLE}
import scala.jdk.StreamConverters.*
object ChristmasLights:
def applyToRectangle[T](c1: Coordinates, c2: Coordinates, effect: (x: Int, y: Int) => T): Seq[T] =
Range(c1.y, c2.y + 1).flatMap(y => Range(c1.x, c2.x + 1)
.map(x => effect(x, y)))
def countLitLights(program: String): Int =
val lights = applyToRectangle(Coordinates(0, 0), Coordinates(999, 999), (_, _) => false).toBuffer
program.lines().toScala(Vector).map(Instruction.apply)
.foreach:
case ON(c1, c2) => applyToRectangle(c1, c2,
(x, y) => lights.update(y * 1000 + x, true))
case OFF(c1, c2) => applyToRectangle(c1, c2,
(x, y) => lights.update(y * 1000 + x, false))
case TOGGLE(c1, c2) => applyToRectangle(c1, c2,
(x, y) => lights.update(y * 1000 + x, !lights(y * 1000 + x)))
lights.count(lightIsOn => lightIsOn)
Un trait permet de définir une interface
C’est une liste de méthodes abstraites
trait AccountContract:
def deposit(amount: Int): Unit
def withdraw(amount: Int): Unit
def printStatement(): String
Il peut également définir des membres abstraits
def m(): T
val x: T
type T
given t: T
Ces traits peuvent ensuite être mixés dans
— Mixin Composition —
des implémentations concrètes
class Account(private val clock: Clock) extends AccountContract:
private val transactions = mutable.ListBuffer[Transaction]()
private var balance = 0
override def deposit(amount: Int): Unit =
balance += amount
val now = clock.instant().atZone(UTC).toLocalDate
transactions.append(Transaction(now, amount, balance))
override def withdraw(amount: Int): Unit =
balance -= amount
val now = clock.instant().atZone(UTC).toLocalDate
transactions.append(Transaction(now, -amount, balance))
override def printStatement(): String = transactions
.map(_.toString)
.mkString("Date Amount Balance\n", "\n", "")
Que se passe-t-il dans le cas où on mixin plusieurs traits avec des déclarations abstraites identiques ?