Чистое ФП:

Зачем и как?

О себе

  • Программирую с 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 митапа

  • 373