Trampoline[_]

Эффекты, трамплины и зачем нам это надо?!

  1. `Effect[_]` -- что это, откуда и зачем?
  2. Что такое передача продолжений?
  3. Что такое "трамплины" и чем они полезны?

Agenda

Effect[F]

Теорема Пифагора

a^2 + b^2 = c^2 => c = sqrt(a^2 + b^2)

"Императивно"

def pyth(a: Double, b: Double): Double =
  require(a > 0)
  require(b > 0)

  var result = 0.0
  result += math.pow(a, 2.0)
  result += math.pow(b, 2.0)

  math.sqrt(result)
pyth(3.0, 4.0) = 5.0

"Функционально"

def squared(a: Double): Option[Double] =
  Option.when(a > 0)(math.pow(a, 2.0))

def pyth(a: Double, b: Double): Option[Double] =
  squared(a) match
    case None => None
    case Some(a) =>
      squared(b) match
        case None    => None
        case Some(b) => Some(math.sqrt(a + b))
end pyth
pyth(3.0, 4.0) = Some(5.0)

"Функционально"

def squared(a: Double): Option[Double] =
  Option.when(a > 0)(math.pow(a, 2.0))

def pyth(a: Double, b: Double): Option[Double] =
  squared(a) match
    case None => None
    case Some(a) =>
      squared(b) match
        case None    => None
        case Some(b) => Some(math.sqrt(a + b))
end pyth
trait Option[A] { self =>
  def flatMap[B](f: A => Option[B]): Option[B] = 
    self match {
      case None => None
      case Some(a) => f(a)
    }
}

"Функционально"

def squared(a: Double): Option[Double] =
  Option.when(a > 0)(math.pow(a, 2.0))

def pyth(a: Double, b: Double): Option[Double] =
  squared(a).flatMap { a =>
    squared(b).flatMap { b =>
      Some(math.sqrt(a + b))
    }
  }
pyth(3.0, 4.0) = Some(5.0)

`for`-comprehensions

val list = List(1 to 100: _*)

val foo(i: Int): List[Int] = 
  List(i to 0 by -1: _*)
val l2 = for (i <- list) yield i + 1
val l3 = list.map(i => i + 1)

assert(l2 == l3)
val l4 = for
  i <- list
  j <- foo(i)
yield i * j

val l5 = list.flatMap { i =>
  foo(i).map(j => i + j)
}

assert(l4 == l5)

"Функционально"

def squared(a: Double): Option[Double] =
  Option.when(a > 0)(math.pow(a, 2.0))

def pyth(a: Double, b: Double): Option[Double] =
  squared(a).flatMap { a =>
    squared(b).flatMap { b =>
      Some(math.sqrt(a + b))
    }
  }

"Функционально"

def squared(a: Double): Option[Double] =
  Option.when(a > 0)(math.pow(a, 2.0))

def pyth(a: Double, b: Double): Option[Double] = 
  for
    a <- squared(a)
    b <- squared(b)
  yield math.sqrt(a + b)
pyth(3.0, 4.0) = 5.0

Снова "императивно"

def squared(a: Double): Option[Double] =
  require(a > 0)
  math.pow(a, 2.0)

def pyth(a: Double, b: Double): Double = 
  val a2 = squared(a)
  val b2 = squared(b)
  math.sqrt(a2 + b2)
pyth(3.0, 4.0) = 5.0

Попробуем обобщить?

def squared(a: Double): Option[Double] =
  Option.when(a > 0)(math.pow(a, 2.0))

def pyth(a: Double, b: Double): Option[Double] = 
  for
    a <- squared(a)
    b <- squared(b)
  yield math.sqrt(a + b)

Было бы здорово иметь интерфейс, который определял бы "универсальные"
`.map`, `.flatMap`, `.foreach`...

Cats, Scalaz и все-все-все

trait Functor[F[_]]:
  extension [A](fa: F[A])
    def map[B](f: A => B): F[B]
trait Applicative[F[_]] extends Functor[F]:
  def pure[A](a: A): F[A]
trait Monad[F[_]] extends Applicative[F]:
  extension [A](fa: F[A])
    def flatMap[B](f: A => F[B]): F[B]

    def map[B](f: A => B): F[B] = 
      flatMap(a => pure(f(a)))

Попробуем обобщить?

def squared(a: Double): Option[Double] =
  Option.when(a > 0)(math.pow(a, 2.0))

def pyth(a: Double, b: Double): Option[Double] = 
  for
    a <- squared(a)
    b <- squared(b)
  yield math.sqrt(a + b)

Попробуем обобщить?

def squared[F[_]: Applicative](a: Double): F[Double] =
  math.pow(a, 2.0).pure[F]

def pyth[F[_]: Monad](a: Double, b: Double): F[Double] = 
  for
    a2 <- squared[F](a)
    b2 <- squared[F](b)
  yield math.sqrt(a2 + b2)
pyth[Option](3.0, 4.0) = Some(5.0)

А где проверка?!

def positive[F[_]: ApplicativeThrow](
  a: Double
): F[Unit] = ApplicativeThrow[F]
  .raiseWhen(a <= 0) {
    java.lang.IllegalArgumentException(
      s"Non positive value: $a"
    )
  }

def pythChecked[F[_]: MonadThrow](
  a: Double, 
  b: Double
): F[Double] = for
  _ <- positive[F](a)
  _ <- positive[F](b)
  c <- pyth[F](a, b)
yield c

А где проверка?!

pythChecked[Either[Throwable, _]](3.0, 4.0) =
  Right(5.0)

pythChecked[Either[Throwable, _]](-3.0, 4.0) =
  Left(java.lang.IllegalArgumentException(
    Non positive value: -3.0
  ))

А зачем нам тут `Monad`?

def squared[F[_]: Applicative](a: Double): F[Double] =
  math.pow(a, 2.0).pure[F]

def pyth[F[_]: Monad](a: Double, b: Double): F[Double] = 
  for
    a2 <- squared[F](a)
    b2 <- squared[F](b)
  yield math.sqrt(a2 + b2)

А зачем нам тут `Monad`?

def squared(a: Double): Option[Double] =
  Option.when(a > 0)(math.pow(a, 2.0))

def pyth(a: Double, b: Double): Option[Double] = 
 (squared(a), squared(b)) match {
   (Some(a2), Some(b2)) => Some(math.sqrt(a2 + b2))
   (None, None)         => None
 }

А незачем!

def squared[F[_]: Applicative](a: Double): F[Double] =
  math.pow(a, 2.0).pure[F]

def pyth[F[_]: Applicative](
  a: Double, b: Double
): F[Double] = 
  (squared[F](a), squared[F](b)).mapN { (a2, b2) =>
    math.sqrt(a2 + b2)
  }
pyth[Option](3.0, 4.0) = Some(5.0)

Ограничим `F[_]`

def pythChecked(
    a: Double, b: Double
): Validated[NonEmptyChain[Throwable], Double] = 
  (
    positive[Either[Throwable, _]](a).toValidatedNec,
    positive[Either[Throwable, _]](b).toValidatedNec,
  ).tupled *> 
  pyth(a, b)
pythChecked(3.0, 4.0) = Valid(5.0)
pythChecked(-3.0, -4.0) = Invalid(
  Chain(
   java.lang.IllegalArgumentException(..),
   java.lang.IllegalArgumentException(..)
  )
)

Effect[F]

Часть логики программы, обладающая средствами композиции с другими такими частями

def andThen[A, B, C](f: A => B, g: B => C): A => C =
  f.andThen(g)
  
def flatMap[F[_]: Monad, A, B](fa: F[A])(f: A => F[B]): F[B] =
  fa.flatMap(f)

def ap[F[_]: Applicative, A, B](fa: F[A])(ff: F[A => B]): F[B] = 
  fa.ap(ff)

Continuations

Поиск первого четного числа

def hasEven(is: Iterable[Int]): Boolean =
  val iter = is.iterator
  while iter.hasNext do 
    if iter.next % 2 == 0 
    then return true
  false
hasEven(Some(1)) = false

hasEven(List(1, 3, 5, 20)) = true

hasEven(LazyList.continually(Random.nextInt(1024))) = true

"Функционально", take 1

def hasEven(is: Iterable[Int]): Boolean =
  is.foldLeft(false) { (acc, i) => 
    acc || (i % 2 == 0)
  }
hasEven(Some(1)) = false

hasEven(List(1, 3, 5, 20)) = true

hasEven(LazyList.continually(Random.nextInt(1024))) = ...
// OutOfMemoryError

"Функционально", take 2

def hasEven(is: Iterable[Int]): Boolean =
  is.foldRight(false) { (i, acc) => 
    (i % 2 == 0) || acc
  }
hasEven(Some(1)) = false

hasEven(List(1, 3, 5, 20)) = true

hasEven(LazyList.continually(Random.nextInt(1024))) = ...
// StackOverflowError

"Функционально", finally

def hasEven(is: Iterable[Int]): Boolean =
  @tailrec 
  def loop(rest: Iterable[Int]): Boolean =
    if rest.isEmpty then false
    else if rest.head % 2 == 0 then true
    else loop(rest.tail)

  loop(is)
end hasEven
hasEven(Some(1)) = false

hasEven(List(1, 3, 5, 20)) = true

hasEven(LazyList.continually(Random.nextInt(1024))) = true

Что же делать? Эврика!

def pow2[R](a: Double): (Double => R) => R = 
  next => next(math.pow(a, 2.0))
def sqrt[R](a: Double): (Double => R) => R = 
  next => next(math.sqrt(a))
def add[R](a: Double, b: Double): (Double => R) => R =
  next => next(a + b)

def pyth[R](a: Double, b: Double): (Double => R) => R =
  next => pow2(a) { a2 =>
    pow2(b) { b2 =>
      add(a2, b2) { anb =>
        sqrt(anb)(next)
      }
    }
  }
pyth(3.0, 4.0)(identity) = 5.0

CPS: continuation passing style

type Cont[R, A] = (A => R) => R
object Cont:
  def apply[R, A](f: (A => R) => R): Cont[R, A] =
    f
  
  def pure[R, A](a: A): Cont[R, A] =
    apply(cb => cb(a))

extension [R, A](cont: Cont[R, A])
  def map[B](f: A => B): Cont[R, B] = Cont { cb =>
    cont(f.andThen(cb))
  }

  def flatMap[B](f: A => Cont[R, B]): Cont[R, B] = Cont { cb =>
    val run: Cont[R, B] => R = c => c(cb)
    cont(f.andThen(run))
  }
end extension

Перепишем старые функции

def pow2[R](a: Double): (Double => R) => R = 
  next => next(math.pow(a, 2.0))
def sqrt[R](a: Double): (Double => R) => R = 
  next => next(math.sqrt(a))
def add[R](a: Double, b: Double): (Double => R) => R =
  next => next(a + b)

def pyth[R](a: Double, b: Double): (Double => R) => R =
  next => pow2(a) { a2 =>
    pow2(b) { b2 =>
      add(a2, b2) { anb =>
        sqrt(anb)(next)
      }
    }
  }
pyth(3.0, 4.0)(identity) = 5.0

Перепишем старые функции

def pow2[R](a: Double): Cont[R, Double] = 
  cb => cb(math.pow(a, 2.0))
def sqrt[R](a: Double): Cont[R, Double] = 
  cb => cb(math.sqrt(a))
def add[R](a: Double, b: Double): Cont[R, Double] = 
  cb => cb(a + b)

def pyth[R](a: Double, b: Double): Cont[R, Double] = for
  a2  <- pow2(a)
  b2  <- pow2(b)
  anb <- add(a2, b2)
  c   <- sqrt(anb)
yield c
pyth(3.0, 4.0)(identity) = 5.0

А куда снова пропали проверки?!

type Cont[R, A] = (A => R) => R
object Cont:
  def callCC[R, A, B](
    f: (A => Cont[R, B]) => Cont[R, A]
  ): Cont[R, A] = apply { cb =>
    val cont = f(a => apply(_ => cb(a)))
    cont(cb)
  }
def pythChecked[R](a: Double, b: Double): Cont[R, Option[Double]] =
  Cont.callCC((exit: Option[Double] => Cont[R, Option[Double]]) =>
    if a <= 0 || b <= 0 then exit(None)
    else pyth(a, b).map(Some(_))
  )
pyth(3.0, 4.0)(identity) = Some(5.0)
pyth(-3.0, 4.0)(identity) = None

И теперь вернемся в поиск четного!

def hasEven[R](is: Iterable[Int]): Cont[R, Boolean] =
  Cont.callCC { (exit: Boolean => Cont[R, Boolean]) =>
    if is.isEmpty then exit(false)
    else if is.head % 2 == 0 then exit(true)
    else hasEven(is.tail)
  }
hasEven(Some(1))(identity) = false

hasEven(List(1, 3, 5, 20))(identity) = true

hasEven(
  LazyList.continually(Random.nextInt(1024))
)(identity) = true

Cont[R, A]

Тип данных, позволяющий представить композицию функций как композицию монад.
Плюс отложенное выполнение.

Trampolining

Итак, `Eval[A]`

sealed trait Eval[A]:
  def result: Either[Throwable, A]
  def value: A = result match
    case Right(value) => value
    case Left(err)    => throw err

  def map[B](f: A => B): Eval[B]
  def flatMap[B](f: A => Eval[B]): Eval[B]

object Eval:
  def now[A](a: A): Eval[A]            = ???
  def later[A](thunk: => A): Eval[A]   = ???
  def fail[A](err: Throwable): Eval[A] = ???

Итак, `Eval[A]`

object Eval:
  def now[A](a: A): Eval[A]            = Now(a)
  def fail[A](err: Throwable): Eval[A] = Failure(err)
  def later[A](thunk: => A): Eval[A]   =
    Later(() => thunk)

  class Now[A](override val value: A) extends Eval[A]:
    def result = Right(value)

  class Failure[A](val err: Throwable) extends Eval[A]:
    def result = Left(err)

  class Later[A](thunk: () => A) extends Eval[A]:
    def result: Either[Throwable, A] =
      try Right(thunk())
      catch case NonFatal(err) => Left(err)
  end Later
end Eval

Как быть с `.flatMap`?

sealed trait Eval[A]:
  def map[B](f: A => B): Eval[B] = 
    flatMap(a => later(f(a)))

  def flatMap[B](f: A => Eval[B]): Eval[B]
object Eval:
  abstract class FlatMap[A] extends Eval[A]:
    type Head

    val head: () => Eval[Head]
    val tail: Either[Throwable, Head] => Eval[A]

    def result: Either[Throwable, A] = ???
  end FlatMap

Как быть с `.flatMap`?

sealed trait Eval[A]:
  self =>

  def map[B](f: A => B): Eval[B] = 
    flatMap(a => later(f(a)))

  def flatMap[B](f: A => Eval[B]): Eval[B] = self match
    case fm: FlatMap[A] =>
      new FlatMap[B]:
        type Head = fm.Head

        val head = fm.head
        val tail = hd =>
          new FlatMap[B]:
            type Head = A
            val head = () => fm.tail(hd)
            val tail = _.fold(fail, f)

    case _ =>
      new FlatMap[B]:
        type Head = A
        val head = () => self
        val tail = _.fold(fail, f)
  end flatMap

Как быть с `FlatMap.result`?

object Eval:
  abstract class FlatMap[A] extends Eval[A]:
    def result: Either[Throwable, A] = evaluate(this)

  private[Eval] sealed trait Stack[A, B]
  private[Eval] final class One[A, B](val f: A => B)
    extends Stack[A, B]
  private[Eval] final class Many[A, B, C](
      val head: Either[Throwable, A] => Eval[B],
      val tail: Stack[B, C]
  ) extends Stack[A, C]

  private[Eval] def evaluate[A](eval: Eval[A]): Either[Throwable, A] =
    @tailrec def loop[A1](
      eval: Eval[A1], 
      stack: Stack[A1, A]
    ): Either[Throwable, A] = ???

    loop(eval, One(identity))
  end evaluate
end Eval

Как быть с `FlatMap.result`?

object Eval:
  def evaluate[A](eval: Eval[A]): Either[Throwable, A] =
    @tailrec def loop[A1](
      eval: Eval[A1], stack: Stack[A1, A]
    ): Either[Throwable, A] = eval match
      case f: Failure[A1] => 
        Left(f.err)
      case fm: FlatMap[A1] => fm.head() match
        case f: Failure[A1]        => 
          Left(f.err)
        case fm1: FlatMap[fm.Head] => 
          loop(fm1.head(), Many(fm1.tail, Many(fm.tail, stack)))
        case inner                 => 
          loop(fm.tail(inner.result), stack)
      // leafs: Now, Later, Failure
      case _ => stack match
        case o: One[A1, A]     => 
          eval.result.map(o.f)
        case m: Many[A1, b, A] => 
          loop(m.head(eval.result), m.tail)

    loop(eval, One(identity))
  end evaluate

Зачем это все?!

Зачем это все?!

def hasEven(is: Iterable[Int]): Eval[Boolean] =
  Eval.later(is.isEmpty).flatMap { empty =>
    if empty then Eval.now(false)
    else if is.head % 2 == 0 then Eval.now(true)
    else hasEven(is.tail)
  }
val eval = hasEven(
  LazyList.continually(Random.nextInt(1024))
)

eval.result = Right(true)

Зачем это все?!

def foldRight[A, B](ia: Iterable[A], zero: Eval[B])(
    f: (A, Eval[B]) => Eval[B]
): Eval[B] =
  def loop(ia: Iterable[A]): Eval[B] =
    Eval.later(ia.isEmpty).flatMap {
      case true  => zero
      case false => f(ia.head, loop(ia.tail))
    }

  loop(ia)
end foldRight
def hasEven(is: Iterable[Int]): Eval[Boolean] =
  foldRight(is, Eval.now(false)) { (i, acc) =>
    if i % 2 == 0 then Eval.now(true)
    else acc
  }

Зачем это все?!

def hasEven(is: Iterable[Int]): Eval[Boolean] =
  foldRight(is, Eval.now(false)) { (i, acc) =>
    if i % 2 == 0 then Eval.now(true)
    else acc
  }
val eval = hasEven(
  LazyList.continually(Random.nextInt(1024))
)

eval.result = Right(true)

Ну допустим. А еще?

Взаимная рекурсия!

def isEven(i: Int): Eval[Boolean] =
  Eval.later(i == 0).flatMap {
    case true  => Eval.now(true)
    case false => isOdd(i - 1)
  }

def isOdd(i: Int): Eval[Boolean] =
  Eval.later(i == 0).flatMap {
    case true  => Eval.now(false)
    case false => isEven(i - 1)
  }
isEven(100501).value = false

isOdd(100501).value = true

Взаимная рекурсия!

expression
  + terms
  - terms
  terms
terms
  term + terms
  term - terms
  term
term
  factor
  factor * term
  factor / term
primary
  ( expression )
  number
factor
  primary
  primary ^ factor

Взаимная рекурсия!

object Calc:
  case class Parsed(result: Int, leftovers: List[String])

  def expression(tokens: List[String]): Eval[Parsed] = 
    tokens match
      case "-" :: tail => terms(tail).map(p => p.copy(result = -p.result))
      case "+" :: tail => terms(tail)
      case _           => terms(tokens)

  def terms(tokens: List[String]): Eval[Parsed] = ???
  def term(tokens: List[String]): Eval[Parsed] = ???
  def factor(tokens: List[String]): Eval[Parsed] = ???

  def primary(tokens: List[String]): Eval[Parsed] = tokens match
    case "(" :: tail => expression(tail).flatMap {
      case Parsed(r, ")" :: tail) => Eval.now(Parsed(r, tail))
      case _ => Eval.fail(...)
    }
    case head :: tail => Eval.later(Parsed(head.toInt, tail))
    case _ => Eval.fail(...)

Взаимная рекурсия!

Calc("1").result = Right(1)

Calc("1 + 1").result = Right(2)

Calc("2 * 2").result = Right(4)

Calc("( 1 + 3 ) * 4 - ( 6 / ( 3 ^ 0 + 1 ) )").result =
  Right(13)

Calc("( 1 + 3 ) * 4 - ( 6 / ( 3 ^ 0 + 1 )").result =
  Left(java.lang.IllegalArgumentException(..))

Trampolining

Методика, позволяющая представить потенциально бесконечную (в т.ч. рекурсивную) последовательность вызова функций в виде ленивой и стеко-безопасной монадической композиции этих самых функций.

Полезные ссылки

Код примеров

Эффект трамплина

By Alexey Shuksto

Эффект трамплина

Что такое эффекты, трамплины и зачем они нам нужны?

  • 220