Прагматичное ФП
1. Чем может помочь ФП в ежедневной практике?
2. Как прийти к ФП "прагматичным" путём
Agenda
Прагматичный путь?
Проблема
Решение
Боль
- Сложное API
- Код, подверженный ошибкам
- Код, который тяжело читать и понимать
- Нестабильное поведение
- Плохая тестируемость решения
- ...
- Нехватка функциональности
Прагматичный путь
≈
Pain-driven development
ФП
1. Immutability
2. Purity
Immutability
Боль от мутабельности
- Проблемы в многопоточной среде
- Кто угодно может поменять что угодно
- Затрудняет понимание программы в целом
- Легко допустить ошибку или не учесть что-то
- Race conditions даже в однопоточной среде
- Код плохо изолирован
- Плохой local reasoning
- Сложно тестировать и дебажить
Immutability
- Нет проблем в многопоточной среде
-
Бо́льшие возможности для композиции
- Код меньше подвержен ошибкам
- Лучшая изоляция кода
- Лучший local reasoning
-
Легче тестировать и дебажить
Immutability
Боль от иммутабельности
-
Иногда иммутабельный код читать сложнее, чем мутабельный
- Некоторые задачи решаются сложнее или требуют дополнительных приседаний
- Больше аллокаций
Immutability
Рецепт использования
-
В редких случаях допустимо использовать мутабельность локально для более читабельного кода
- Мутабельность — оптимизация производительности
- По-умолчанию всё иммутабельно (там, где это возможно)
Immutability
- Нет проблем в многопоточной среде
-
Бо́льшие возможности для композиции
- Код меньше подвержен ошибкам
- Лучшая изоляция кода
- Лучший local reasoning
-
Легче тестировать и дебажить
}
Может быть неочевидно без практики
Purity
Чистые функции
1. Принимают аргументы
2. Возвращают результат, полученный на основе переданных аргументов
3. Для одних и тех же аргументов результат всегда будет одинаков
4. Не выбрасывают исключений
1. Не читают состояние за пределами функции
2. Не пишут в состояние за пределами функции
5. Не работают с IO (БД, файлы, сеть, ...)
3. Не мутируют входные аргументы
Purity
Чистые функции
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)
}
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)
}
Purity
Referential transparency
def future() = Future { println(1); 1 }
def future() = Future { println(1); 1 }
def res1 = future().flatMap(_ => future())
def future() = Future { println(1); 1 }
def res1 = future().flatMap(_ => future())
def res2 = {
val f = future()
f.flatMap(_ => f)
}
def future() = Future { println(1); 1 }
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
1
res2
1
1
Purity
Referential transparency
def future1() = Future { println(1); 1 }
def future2() = Future { println(2); 2 }
val future1val = future1()
val future2val = future2()
for {
res1 <- future1val
res2 <- future2val
} yield {
val res = res1 + res2
println(res)
res
}
def future1() = Future { println(1); 1 }
def future2() = Future { println(2); 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))
}
def fromFuture[A](future: => Future[A]): LazyFuture[A] = {
new LazyFuture[A](_ => future)
}
}
LazyFuture
val lazyFuture: LazyFuture[Int] =
LazyFuture.delay { println(1); 1 }
val lazyFuture: LazyFuture[Int] =
LazyFuture.delay { println(1); 1 }
val future: Future[Int] = lazyFuture.run(ec)
val lazyFuture: LazyFuture[Int] =
LazyFuture.delay { println(1); 1 }
val future: Future[Int] = lazyFuture.run(ec)
val res: Int = Await.result(future, 1.second)
val lazyFuture: LazyFuture[Int] =
LazyFuture.delay { println(1); 1 }
val future: Future[Int] = lazyFuture.run(ec)
val res: Int = Await.result(future, 1.second)
println(res)
1
1
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); 1 }
def lazyFuture2() = LazyFuture.delay { println(2); 2 }
def lazyFuture1() = LazyFuture.delay { println(1); 1 }
def lazyFuture2() = LazyFuture.delay { println(2); 2 }
val lazyFuture = for {
res1: Int <- lazyFuture1()
res2: Int <- lazyFuture2()
} yield {
val res = res1 + res2
println(res)
res
}
def lazyFuture1() = LazyFuture.delay { println(1); 1 }
def lazyFuture2() = LazyFuture.delay { println(2); 2 }
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); 1 }
def lazyFuture2() = LazyFuture.delay { println(2); 2 }
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
2
3
3
LazyFuture
def lfuture() = LazyFuture.delay { println(1); 1 }
res1
1
1
1
res2
1
1
1
Referential transparency
def lfuture() = LazyFuture.delay { println(1); 1 }
def res1 = lfuture().flatMap(_ => lfuture())
def lfuture() = LazyFuture.delay { println(1); 1 }
def res1 = lfuture().flatMap(_ => lfuture())
def res2 = {
val lf = lfuture()
lf.flatMap(_ => lf)
}
def lfuture() = LazyFuture.delay { println(1); 1 }
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); 1 }
def lazyFuture2() = LazyFuture.delay { println(2); 2 }
Referential transparency
def lazyFuture1() = LazyFuture.delay { println(1); 1 }
def lazyFuture2() = LazyFuture.delay { println(2); 2 }
val future1started = lazyFuture1()
val future2started = lazyFuture2()
def lazyFuture1() = LazyFuture.delay { println(1); 1 }
def lazyFuture2() = LazyFuture.delay { println(2); 2 }
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); 1 }
def lazyFuture2() = LazyFuture.delay { println(2); 2 }
val future1started = lazyFuture1()
val future2started = lazyFuture2()
val lazyFuture = for {
res1 <- future1started
res2 <- future2started
} yield {
val res = res1 + res2
println(res)
res
}
lazyFuture.run(ec)
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
val lazyFuture = {
(lazyFuture1() par lazyFuture2())
}
val lazyFuture = {
(lazyFuture1() par lazyFuture2()).map {
case (res1, res2) =>
val res = res1 + res2
println(res)
res
}
}
val lazyFuture = {
(lazyFuture1() par lazyFuture2()).map {
case (res1, res2) =>
val res = res1 + res2
println(res)
res
}
}
lazyFuture.run(ec)
Purity
Боль от функций с эффектами
- Сложно тестировать и дебажить
- Не всегда легко компизировать
- Кто угодно может сделать что угодно
- Затрудняет понимание программы в целом
- Легко допустить ошибку или не учесть что-то
- Race conditions даже в однопоточной среде
- Код плохо изолирован
- Плохой local reasoning
Purity
Чистые функции
- Легко тестировать и дебажить
- Хорошо композируются
- Меньше подвержены ошибкам
- Более предсказуемы в многопоточной среде
- Код полностью изолирован
- Отличный local reasoning
Purity
Боль от чистых функций
- Сигнатуры чистых функций часто сложнее
Прагматичное ФП
By Yury Badalyants
Прагматичное ФП
- 426