Подтвердить что ты не робот

Будущее в Scala монаде?

Почему и как конкретно это Scala Будущее, а не Монада; и кто-нибудь, пожалуйста, сравните его с чем-то, что является Монадой, как вариант?

Причина, по которой я спрашиваю, - это Даниэль Вестхайд Руководство Неофита по Scala Часть 8: Добро пожаловать в будущее, где я спросил, есть ли Scala Будущее было Монадой, и автор ответил, что это не так, что отбросило базу. Я пришел сюда, чтобы попросить разъяснения.

4b9b3361

Ответ 1

Сводка первая

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

К счастью, библиотека под названием Scalaz предоставляет абстракцию под названием Task, которая не имеет проблем с эффектами или без них.

Определение монады

Давайте кратко рассмотрим, что такое монада. Монада должна уметь определять по крайней мере эти две функции:

def unit[A](block: => A)
    : Future[A]

def bind[A, B](fa: Future[A])(f: A => Future[B])
    : Future[B]

И эти функции должны учитывать три закона:

  • Левая идентичность: bind(unit(a))(f) ≡ f(a)
  • Идентичность: bind(m) { unit(_) } ≡ m
  • Ассоциативность: bind(bind(m)(f))(g) ≡ bind(m) { x => bind(f(x))(g) }

Эти законы должны соблюдаться для всех возможных значений по определению монады. Если они этого не сделают, у нас просто нет монады.

Существуют другие способы определения монады, которые более или менее одинаковы. Это популярно.

Эффекты приводят к значениям

Почти каждое использование Future, которое я видел, использует его для асинхронных эффектов, ввода/вывода с внешней системой, такой как веб-служба или база данных. Когда мы это делаем, будущее не является даже значением, а математические термины, такие как монады, описывают только значения.

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

Вот иллюстрация проблемы. Если мы имеем два эффекта:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits._


def twoEffects =
  ( Future { println("hello") },
    Future { println("hello") } )

у нас будут две печати "привет" при вызове twoEffects:

scala> twoEffects
hello
hello

scala> twoEffects
hello
hello

Но если бы фьючерсы были значениями, мы должны были бы учитывать общее выражение:

lazy val anEffect = Future { println("hello") }

def twoEffects = (anEffect, anEffect)

Но это не дает нам такого же эффекта:

scala> twoEffects
hello

scala> twoEffects

Первый вызов twoEffects запускает эффект и кэширует результат, поэтому эффект не запускается во второй раз, когда мы вызываем twoEffects.

С Futures мы в конечном итоге должны думать о политике оценки языка. Например, в приведенном выше примере факт, что я использую ленивую ценность, а не строгую, делает разницу в оперативной семантике. Это именно то, что извращенное функциональное программирование рассуждений призвано избежать - и оно делает это путем программирования значений.

Без замены законы нарушают

В присутствии эффектов нарушаются законы монады. Поверхностно законы, как представляется, сохраняются для простых случаев, но в тот момент, когда мы начинаем подставлять выражения с их оцененными значениями, мы заканчиваем теми же проблемами, которые были проиллюстрированы выше. Мы просто не можем говорить о математических концепциях, таких как монады, когда у нас нет значений в первую очередь.

Если говорить откровенно, если вы используете эффекты со своими фьючерсами, говоря, что они являются монадами, не ошибается, потому что они даже не значения.

Чтобы узнать, как нарушаются законы монады, просто отбросьте свое эффективное будущее.

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits._


def unit[A]
    (block: => A)
    : Future[A] =
  Future(block)

def bind[A, B]
    (fa: Future[A])
    (f: A => Future[B])
    : Future[B] =
  fa flatMap f

lazy val effect = Future { println("hello") }

Опять же, он будет запускаться только один раз, но вам нужно, чтобы он выполнялся дважды - один раз для правой части закона, а другой для левой. Я проиллюстрирую проблему для правильного закона идентичности:

scala> effect  // RHS has effect
hello

scala> bind(effect) { unit(_) }  // LHS doesn't

Неявный ExecutionContext

Не помещая ExecutionContext в неявную область, мы не можем определить unit или bind в нашей монаде. Это связано с тем, что API Scala для фьючерсов имеет такую ​​подпись:

object Future {
  // what we need to define unit
  def apply[T]
      (body: ⇒ T)
      (implicit executor: ExecutionContext)
      : Future[T]
}

trait Future {
   // what we need to define bind
   flatMap[S]
       (f: T ⇒ Future[S])
       (implicit executor: ExecutionContext)
       : Future[S]
}

Как "удобство" для пользователя, стандартная библиотека поощряет пользователей определять контекст выполнения в неявной области, но я думаю, что это огромная дыра в API, которая просто приводит к дефектам. Одна область вычислений может иметь один контекст выполнения, тогда как другая область может иметь другой контекст.

Возможно, вы можете проигнорировать проблему, если вы определяете экземпляр unit и bind, который связывает обе операции с одним контекстом и последовательно использует этот экземпляр. Но это не то, что люди делают большую часть времени. Большую часть времени люди используют фьючерсы с понятием "доходность", которые становятся вызовами map и flatMap. Чтобы сделать работу с пониманием эффективности, контекст выполнения должен быть определен в некоторой неглобальной неявной области (поскольку for-yield не предоставляет способ указать дополнительные параметры для вызовов map и flatMap).

Чтобы быть понятным, Scala позволяет использовать множество вещей с помощью понятий for-yield, которые на самом деле не являются монадами, поэтому не верьте, что у вас есть монада только потому, что она работает с синтаксисом for-yield.

Лучший способ

Там есть хорошая библиотека для Scala, называемая Scalaz, которая имеет абстракцию, называемую scalaz.concurrent.Task. Эта абстракция не влияет на построение данных, как это делает стандартная библиотека Future. Кроме того, задача на самом деле является монадой. Мы составляем задачу монадически (мы можем использовать понимание по-урожайным, если хотите), и никакие эффекты не выполняются во время составления. У нас есть наша окончательная программа, когда мы составили одно выражение, оценивающее значение Task[Unit]. Это в конечном итоге является нашим эквивалентом "основной" функции, и мы можем, наконец, запустить ее.

Вот пример, иллюстрирующий, как мы можем подменить выражения Task с их соответствующими оцененными значениями:

import scalaz.concurrent.Task
import scalaz.IList
import scalaz.syntax.traverse._


def twoEffects =
  IList(
    Task delay { println("hello") },
    Task delay { println("hello") }).sequence_

У нас будет две печати "привет" при вызове twoEffects:

scala> twoEffects.run
hello
hello

И если мы отбросим общий эффект,

lazy val anEffect = Task delay { println("hello") }

def twoEffects =
  IList(anEffect, anEffect).sequence_

получаем то, что мы ожидаем:

scala> twoEffects.run
hello
hello

На самом деле, не имеет большого значения, будем ли мы использовать ленивое значение или строгое значение с Task; мы дважды получаем приветствие в любом случае.

Если вы хотите функционально программировать, рассмотрите возможность использования Task везде, где вы можете использовать Futures. Если API заставляет Futures на вас, вы можете преобразовать будущее в задачу:

import concurrent.
  { ExecutionContext, Future, Promise }
import util.Try
import scalaz.\/
import scalaz.concurrent.Task


def fromScalaDeferred[A]
    (future: => Future[A])
    (ec: ExecutionContext)
    : Task[A] =
  Task
    .delay { unsafeFromScala(future)(ec) }
    .flatMap(identity)

def unsafeToScala[A]
    (task: Task[A])
    : Future[A] = {
  val p = Promise[A]
  task.runAsync { res =>
    res.fold(p failure _, p success _)
  }
  p.future
}

private def unsafeFromScala[A]
    (future: Future[A])
    (ec: ExecutionContext)
    : Task[A] =
  Task.async(
    handlerConversion
      .andThen { future.onComplete(_)(ec) })

private def handlerConversion[A]
    : ((Throwable \/ A) => Unit)
      => Try[A]
      => Unit =
  callback =>
    { t: Try[A] => \/ fromTryCatch t.get }
      .andThen(callback)

"Небезопасные" функции запускают задачу, подвергая любые внутренние эффекты побочным эффектам. Поэтому постарайтесь не называть какие-либо из этих "небезопасных" функций, пока вы не составите одну гигантскую задачу для всей вашей программы.

Ответ 2

Как и другие комментаторы, вы ошибаетесь. Тип Scala Future имеет монадические свойства:

import scala.concurrent.Future
import scala.concurrent.ExecutionContext.Implicits._

def unit[A](block: => A): Future[A] = Future(block)
def bind[A, B](fut: Future[A])(fun: A => Future[B]): Future[B] = fut.flatMap(fun)

Вот почему вы можете использовать синтаксис for -comprehension с фьючерсами в Scala.