Чистое ФП:
Зачем и как?
О себе
- Программирую с 2011 года
- PHP
- JavaScript
- Java
- Scala
- 2GIS
1. Чем может помочь ФП в ежедневной практике?
2. Как начать писать программы с использованием приемуществ чистого ФП?
Agenda
ФП
1. Immutability
2. Purity
Immutability
+ Нет проблем в многопоточной среде
+ Бо́льшие возможности для композиции
+ Код меньше подвержен ошибкам
+ Лучшая изоляция кода
+ Лучший local reasoning
+ Легче тестировать и дебажить
- Иногда иммутабельный код читать сложнее, чем мутабельный
- Некоторые задачи решаются сложнее или требуют дополнительных приседаний
- Больше аллокаций
}
Может быть неочевидно без практики
Immutability
Рецепт использования
3. В редких случаях допустимо использовать мутабельность локально для более читабельного кода
2. Мутабельность — оптимизация производительности
1. По-умолчанию всё иммутабельно
Immutability
Итоги
2. Все приемущества могут быть неочевидны без практики.
1. Иммутабельность делает программирование в целом чуть лучше.
Purity
Чистые функции
1. Принимают аргументы
2. Возвращают результат, полученный на основе переданных аргументов
3. Для одних и тех же аргументов результат всегда будет одинаков
4. На любой input возвращают какой-то output заданного типа
4. Не выбрасывают исключений
1. Не читают состояние за пределами функции
2. Не пишут в состояние за пределами функции
5. Не работают с IO (БД, файлы, сеть, ...)
3. Не мутируют входные аргументы
Purity
Чистые функции
6. Не вызывают функции с эффектами
Чистые функции
+ Легко тестировать и дебажить
+ Хорошо композируются
+ Меньше подвержены ошибкам
+ Более предсказуемы в многопоточной среде
+ Код полностью изолирован
+ Отличный local reasoning
- Сигнатуры чистых функций могут быть сложнее
- Не все функции можно сделать чистыми
Purity
Referential transparency
Если функция referentially transparent, то вызов функции можно заменить на результат её выполнения (и наоборот) и это не повлияет на поведение программы.
Purity
Referential transparency
def sum(a: Int, b: Int): Int = a + b
def mul(a: Int, b: Int): Int = a * b
def sum(a: Int, b: Int): Int = a + b
def mul(a: Int, b: Int): Int = a * b
val res1 = mul(sum(1, 2), sum(2, 3))
def sum(a: Int, b: Int): Int = a + b
def mul(a: Int, b: Int): Int = a * b
val res1 = mul(sum(1, 2), sum(2, 3))
val res2 = {
val s1 = sum(1, 2)
val s2 = sum(2, 3)
mul(s1, s2)
}
res1
15
res2
15
Purity
Referential transparency
def getList() = List(1)
def getList() = List(1)
val res1 = getList().flatMap(_ => getList())
def getList() = List(1)
val res1 = getList().flatMap(_ => getList())
val res2 = {
val list = getList()
list.flatMap(_ => list)
}
res1
List(1)
res2
List(1)
Purity
Referential transparency
def future() = Future { println(1); 2 }
def future() = Future { println(1); 2 }
def res1 = future().flatMap(_ => future())
def future() = Future { println(1); 2 }
def res1 = future().flatMap(_ => future())
def res2 = {
val f = future()
f.flatMap(_ => f)
}
def future() = Future { println(1); 2 }
def res1 = future().flatMap(_ => future())
def res2 = {
val f = future()
f.flatMap(_ => f)
}
println("res1")
println(Await.result(res1, 1.second))
println("res2")
println(Await.result(res2, 1.second))
res1
1
1
2
res2
1
2
Purity
Referential transparency
def future1() = Future { println(1); 2 }
def future2() = Future { println(3); 4 }
val future1val = future1()
val future2val = future2()
def future1() = Future { println(1); 2 }
def future2() = Future { println(3); 4 }
3
1
6
1
3
6
def future1() = Future { println(1); 2 }
def future2() = Future { println(3); 4 }
val future1val = future1()
val future2val = future2()
for {
res1 <- future1val
res2 <- future2val
} yield {
val res = res1 + res2
println(res)
res
}
Referential transparency
≈
Purity
Чистые функции
Рецепт использования
2. Функции с эффектами делают только эффекты, а потом отдают управление чистым функциям.
1. Все функции, которые можно сделать чистыми — делать чистыми.
Чистые функции
Реальность
2. Иногда возникают "островки" чистого кода.
1. Подавляющее большинство функций делают какое-то IO, или вызывают функции, которые делают IO.
Purity
Итоги
1. Чистота делает программирование в целом чуть лучше.
2. Не весь код можно сделать чистым
И что, всё?
Future
def future() = Future { println(1); 2 }
def res1 = future().flatMap(_ => future())
def res2 = {
val f = future()
f.flatMap(_ => f)
}
println("res1")
println(Await.result(res1, 1.second))
println("res2")
println(Await.result(res2, 1.second))
res1
1
1
2
res2
1
2
LazyFuture
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
)
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
)
object LazyFuture {
def delay[A](body: => A): LazyFuture[A] = {
new LazyFuture[A](ec => Future(body)(ec))
}
}
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
)
object LazyFuture {
def delay[A](body: => A): LazyFuture[A] = {
new LazyFuture[A](ec => Future(body)(ec))
}
def fromFuture[A](future: => Future[A]): LazyFuture[A] = {
new LazyFuture[A](_ => future)
}
}
LazyFuture
val lazyFuture: LazyFuture[Int] =
LazyFuture.delay { println(1); 2 }
val lazyFuture: LazyFuture[Int] =
LazyFuture.delay { println(1); 2 }
val future: Future[Int] = lazyFuture.run(ec)
val lazyFuture: LazyFuture[Int] =
LazyFuture.delay { println(1); 2 }
val future: Future[Int] = lazyFuture.run(ec)
val res: Int = Await.result(future, 1.second)
val lazyFuture: LazyFuture[Int] =
LazyFuture.delay { println(1); 2 }
val future: Future[Int] = lazyFuture.run(ec)
val res: Int = Await.result(future, 1.second)
println(res)
1
2
LazyFuture
def futureFunc(): Future[Int] = Future { println(1); 2 }
def futureFunc(): Future[Int] = Future { println(1); 2 }
val lazyFuture: LazyFuture[Int] =
LazyFuture.fromFuture(futureFunc())
def futureFunc(): Future[Int] = Future { println(1); 2 }
val lazyFuture: LazyFuture[Int] =
LazyFuture.fromFuture(futureFunc())
val future: Future[Int] = lazyFuture.run(ec)
def futureFunc(): Future[Int] = Future { println(1); 2 }
val lazyFuture: LazyFuture[Int] =
LazyFuture.fromFuture(futureFunc())
val future: Future[Int] = lazyFuture.run(ec)
val res: Int = Await.result(future, 1.second)
1
2
def futureFunc(): Future[Int] = Future { println(1); 2 }
val lazyFuture: LazyFuture[Int] =
LazyFuture.fromFuture(futureFunc())
val future: Future[Int] = lazyFuture.run(ec)
val res: Int = Await.result(future, 1.second)
println(res)
LazyFuture
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
) { self =>
def map[B](f: A => B): LazyFuture[B] = {
new LazyFuture[B]((ec: ExecutionContext) => {
self.run(ec).map(f)(ec)
})
}
}
LazyFuture
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
) { self =>
def flatMap[B](f: A => LazyFuture[B]): LazyFuture[B] = {
new LazyFuture[B]((ec: ExecutionContext) => {
self.run(ec).flatMap { a =>
f(a).run(ec)
}(ec)
})
}
}
LazyFuture
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val lazyFuture = for {
res1: Int <- lazyFuture1()
res2: Int <- lazyFuture2()
} yield {
val res = res1 + res2
println(res)
res
}
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val lazyFuture = for {
res1: Int <- lazyFuture1()
res2: Int <- lazyFuture2()
} yield {
val res = res1 + res2
println(res)
res
}
val future: Future[Int] = lazyFuture.run(ec)
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val lazyFuture = for {
res1: Int <- lazyFuture1()
res2: Int <- lazyFuture2()
} yield {
val res = res1 + res2
println(res)
res
}
val future: Future[Int] = lazyFuture.run(ec)
println(Await.result(future, 1.second))
1
3
6
6
LazyFuture
def lfuture() = LazyFuture.delay { println(1); 2 }
res1
1
1
2
res2
1
1
2
Referential transparency
def lfuture() = LazyFuture.delay { println(1); 2 }
def res1 = lfuture().flatMap(_ => lfuture())
def lfuture() = LazyFuture.delay { println(1); 2 }
def res1 = lfuture().flatMap(_ => lfuture())
def res2 = {
val lf = lfuture()
lf.flatMap(_ => lf)
}
def lfuture() = LazyFuture.delay { println(1); 2 }
def res1 = lfuture().flatMap(_ => lfuture())
def res2 = {
val lf = lfuture()
lf.flatMap(_ => lf)
}
println("res1")
println(Await.result(res1.run(ec), 1.second))
println("res2")
println(Await.result(res2.run(ec), 1.second))
LazyFuture
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
Referential transparency
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val future1started = lazyFuture1()
val future2started = lazyFuture2()
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val future1started = lazyFuture1()
val future2started = lazyFuture2()
val lazyFuture = for {
res1 <- future1started
res2 <- future2started
} yield {
val res = res1 + res2
println(res)
res
}
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val future1started = lazyFuture1()
val future2started = lazyFuture2()
val lazyFuture = for {
res1 <- future1started
res2 <- future2started
} yield {
val res = res1 + res2
println(res)
res
}
lazyFuture.run(ec)
1
3
6
LazyFuture
Referential transparency
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
) { self =>
def par[B](that: LazyFuture[B]): LazyFuture[(A, B)]
}
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
) { self =>
def par[B](that: LazyFuture[B]): LazyFuture[(A, B)] = {
new LazyFuture[(A, B)]((ec: ExecutionContext) => {
})
}
}
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
) { self =>
def par[B](that: LazyFuture[B]): LazyFuture[(A, B)] = {
new LazyFuture[(A, B)]((ec: ExecutionContext) => {
val selfStarted = self.run(ec)
val thatStarted = that.run(ec)
})
}
}
class LazyFuture[A] private (
val run: ExecutionContext => Future[A]
) { self =>
def par[B](that: LazyFuture[B]): LazyFuture[(A, B)] = {
new LazyFuture[(A, B)]((ec: ExecutionContext) => {
val selfStarted = self.run(ec)
val thatStarted = that.run(ec)
selfStarted.flatMap { selfRes =>
thatStarted.map { thatRes =>
(selfRes, thatRes)
}(ec)
}(ec)
})
}
}
LazyFuture
Referential transparency
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val lazyFuture = {
(lazyFuture1() par lazyFuture2())
}
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val lazyFuture = {
(lazyFuture1() par lazyFuture2()).map {
case (res1, res2) =>
val res = res1 + res2
println(res)
res
}
}
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
val lazyFuture = {
(lazyFuture1() par lazyFuture2()).map {
case (res1, res2) =>
val res = res1 + res2
println(res)
res
}
}
lazyFuture.run(ec)
1
3
6
3
1
6
def lazyFuture1() = LazyFuture.delay { println(1); 2 }
def lazyFuture2() = LazyFuture.delay { println(3); 4 }
LazyFuture
Итоги
1. Просто сделав эффект ленивым мы вернули referential transparency, а значит чистоту.
2. Developer experience либо не хуже, либо лучше.
3. Кардинальных отличий от привычного кода с фьючами нет.
4. Уже есть несколько готовых решений
LazyFuture
Готовые решения
1. IO из cats-effect
2. Task из monix
3. zio из scalaz-zio
Эти готовые решения работают быстрее и умеют больше, чем стандартная Future.
IO/Task
Рецепт
1. Все синхронные эффекты заворачивайте функцией delay
2. Все асинхронные эффекты заворачивайте функцией async
3. Все Future преобразуйте в IO/Task
4. Стройте программы на композиции IO/Task: map, flatMap, ...
5. Запускайте IO/Task как можно позже
IO/Task
Запуск
1. На границах взаимодействия с библиотеками
get {
path("foo") {
val serviceResult: IO[String] = service.method()
complete(serviceResult.unsafeToFuture())
}
}
IO/Task
Запуск
2. В main (или используя IOApp/TaskApp)
object Main extends IOApp {
def run(args: List[String]): IO[ExitCode] = {
val component1 = Component1()
val component2 = Component2(component1)
val appIO: IO[Unit] = component2.doSomeStuff
appIO.map { _ =>
ExitCode.Success
}
}
}
Минусы подхода
1. Boilerplate код для заворачивания кода с эффектами
5. Внутренности библиотек, построенных на таком подходе, могут иметь очень высокий порог входа
4. Усложняется дебаг
2. Компилятор никак не форсирует подход и даёт ошибиться
3. Иногда чистый код может потребовать дополнительных приседаний для сохранения чистоты
Итоги
1. Иммутабельность и чистота делают программирование в целом лучше, в том числе и в ежедневных задачах.
2. Чистое ФП не так значительно отличается от привычных практик, как может казаться.
3. Перейдя от Future к IO/Task можно относительно легко превратить свои программы в чистые.
4. Для понимания всех плюсов может потребоваться практика.
lmnet89@gmail.com
Бадальянц Юрий, 2019
Спасибо!
Чистое ФП: Зачем и как?
By Yury Badalyants
Чистое ФП: Зачем и как?
Вариант для Tinkoff митапа
- 400