Прагматичное ФП

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

Прагматичное ФП

  • 394