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

Функциональный синтаксис puzzler в scalaz

После просмотра анонса Ника Партиджа о выводе scalaz, Мне пришлось посмотреть на этот пример, который просто потрясающий:

import scalaz._
import Scalaz._
def even(x: Int) : Validation[NonEmptyList[String], Int] 
    = if (x % 2 ==0) x.success else "not even: %d".format(x).wrapNel.fail

println( even(3) <|*|> even(5) ) //prints: Failure(NonEmptyList(not even: 3, not even: 5))

Я пытался понять, что делает метод <|*|>, вот исходный код:

def <|*|>[B](b: M[B])(implicit t: Functor[M], a: Apply[M]): M[(A, B)] 
    = <**>(b, (_: A, _: B))

ОК, это довольно запутанно (!), но оно ссылается на метод <**>, который объявляется следующим образом:

def <**>[B, C](b: M[B], z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C] 
    = a(t.fmap(value, z.curried), b)

Итак, у меня есть несколько вопросов:

  • Как получилось, что метод принимает более высокий тип одного параметра типа (M[B]), но может быть передан a Validation (который имеет два типа пареметров)?
  • Синтаксис (_: A, _: B) определяет функцию (A, B) => Pair[A,B], которую ожидает второй метод: что происходит с Tuple2/Pair в случае отказа? Там нет кортежа!
4b9b3361

Ответ 1

Конструкторы типов как параметры типа

M является параметром типа одному из главных сутенерств Scalaz, MA, который представляет собой Type Constructor (aka Higher Kinded Type ) от фиксированного значения. Этот конструктор типов используется для поиска соответствующих экземпляров Functor и Apply, которые являются неявными требованиями к методу <**>.

trait MA[M[_], A] {
   val value: M[A]
   def <**>[B, C](b: M[B], z: (A, B) => C)(implicit t: Functor[M], a: Apply[M]): M[C] = ...
}

Что такое конструктор типов?

Из справочника языка Scala:

Мы различаем порядок первого порядка типы и типы конструкторов, которые принимать параметры типа и типы урожая. Подмножество типов первого порядка, называемых типы значений представляют собой (первоклассных) значений. Типы значений либо конкретными, либо абстрактными. каждый конкретный тип значения может быть представлен как тип класса, то есть тип (§3.2.3), который относится к class1 (§5.3) или как составной тип (§3.2.7), представляющий пересечение типов, возможно с уточнением (§3.2.7), что дополнительно сдерживает типы его членов. Абстрактное значение типы вводятся по типу параметры (§4.4) и абстрактный тип привязки (§ 4.3). Скобки в типах используются для группировки. Предположим, что объекты и пакеты также неявно определите класс (с тем же именем, что и объект или пакет, но недоступны для пользовательских программ).

Нецензурные типы фиксируют свойства идентификаторы, которые не являются значениями (§3.3). Например, тип конструктор (п .3.3.3) напрямую не укажите тип значений. Однако, когда конструктор типа применяется к правильные аргументы типа, это дает тип первого порядка, который может быть тип значения. Нецензурные типы выраженное косвенно в Scala. Например, тип метода описывается путем записи вниз подписи метода, которая в сам по себе не является реальным типом, хотя он приводит к соответствующей функции типа (§3.3.1). Конструкторы типов другой пример, так как можно написать тип Swap [m [_, _], a, b] = m [b, a], но нет синтаксиса для написания соответствующая функция анонимного типа непосредственно.

List - это конструктор типов. Вы можете применить тип Int, чтобы получить Тип значения, List[Int], который может классифицировать значение. Другие конструкторы типов принимают более одного параметра.

Признак scalaz.MA требует, чтобы параметр первого типа должен быть конструктором типа, который принимает один тип для возврата типа значения с синтаксисом trait MA[M[_], A] {}. Определение параметра типа описывает форму конструктора типа, который называется его видом. Говорят, что List имеет вид * -> *.

Частичное применение типов

Но как MA обернуть значения типа Validation[X, Y]? Тип Validation имеет вид (* *) -> * и может быть передан только как аргумент типа параметру типа, объявленному как M[_, _].

Это неявное преобразование в объект Scalaz преобразует значение типа Validation[X, Y] в MA:

object Scalaz {
    implicit def ValidationMA[A, E](v: Validation[E, A]): MA[PartialApply1Of2[Validation, E]#Apply, A] = ma[PartialApply1Of2[Validation, E]#Apply, A](v)
}

Что в свою очередь использует трюк с псевдонимом типа PartialApply1Of2, чтобы частично применить конструктор типа Validation, установив тип ошибки, но оставляя тип успеха непримененным.

PartialApply1Of2[Validation, E]#Apply лучше писать как [X] => Validation[E, X]. Недавно я предложил добавить такой синтаксис в Scala, это может произойти в 2.9.

Подумайте об этом как эквиваленте уровня типа:

def validation[A, B](a: A, b: B) = ...
def partialApply1Of2[A, B C](f: (A, B) => C, a: A): (B => C) = (b: B) => f(a, b)

Это позволяет объединить Validation[String, Int] с Validation[String, Boolean], потому что оба совместно используют конструктор типа [A] Validation[String, A].

Аппликативные функторы

<**> требует, чтобы конструктор типа M должен иметь связанные экземпляры Apply и Functor. Это представляет собой аппликативный функтор, который, подобно Монаде, является способом структурирования вычисления посредством некоторого эффекта. В этом случае эффект заключается в том, что суб-вычисления могут терпеть неудачу (и когда они это делают, мы накапливаем отказы).

Контейнер Validation[NonEmptyList[String], A] может обернуть чистое значение типа A в этом "эффекте". Оператор <**> принимает два эффективных значения и чистую функцию и объединяет их с экземпляром Applicative Functor для этого контейнера.

Здесь, как это работает для прикладного функтора Option. "Эффект" здесь - возможность отказа.

val os: Option[String] = Some("a")
val oi: Option[Int] = Some(2)

val result1 = (os <**> oi) { (s: String, i: Int) => s * i }
assert(result1 == Some("aa"))

val result2 = (os <**> (None: Option[Int])) { (s: String, i: Int) => s * i }
assert(result2 == None)

В обоих случаях существует чистая функция типа (String, Int) => String, применяемая к эффективным аргументам. Обратите внимание, что результат обернут в тот же эффект (или контейнер, если хотите) в качестве аргументов.

Вы можете использовать один и тот же шаблон для множества контейнеров со связанным с ним аппликативным функтором. Все монады автоматически аппликативные функторы, но есть еще больше, например ZipStream.

Option и [A]Validation[X, A] являются Monads, поэтому вы также можете использовать Bind (aka flatMap):

val result3 = oi flatMap { i => os map { s => s * i } }
val result4 = for {i <- oi; s <- os} yield s * i

Кортеж с `< | ** | > `

<|**|> действительно похож на <**>, но он обеспечивает чистую функцию для простого создания Tuple2 из результатов. (_: A, _ B) является сокращением для (a: A, b: B) => Tuple2(a, b)

И дальше

Здесь приведенные примеры Applicative и Validation. Я использовал несколько иной синтаксис для использования Applicative Functor, (fa ⊛ fb ⊛ fc ⊛ fd) {(a, b, c, d) => .... }

ОБНОВЛЕНИЕ: Но что происходит в случае сбоя?

что происходит с Tuple2/Pair в случае сбоя?

Если какой-либо из подкомплексов не выполняется, предоставленная функция никогда не запускается. Он запускается только в том случае, если все подвычисления (в данном случае два аргумента, переданные на <**>) успешны. Если это так, он объединяет их в Success. Где эта логика? Это определяет экземпляр Apply для [A] Validation[X, A]. Мы требуем, чтобы тип X должен иметь Semigroup avaiable, что является стратегией объединения отдельных ошибок, каждый из типов X, в агрегированную ошибку того же типа. Если вы выбрали String как тип ошибки, Semigroup[String] объединяет строки; если вы выберете NonEmptyList[String], ошибки (ошибки) с каждого шага будут объединены в более длинные ошибки NonEmptyList. Эта конкатенация происходит ниже, когда два Failures объединяются, используя оператор (который расширяется с implicits, например, Scalaz.IdentityTo(e1).⊹(e2)(Semigroup.NonEmptyListSemigroup(Semigroup.StringSemigroup)).

implicit def ValidationApply[X: Semigroup]: Apply[PartialApply1Of2[Validation, X]#Apply] = new Apply[PartialApply1Of2[Validation, X]#Apply] {
  def apply[A, B](f: Validation[X, A => B], a: Validation[X, A]) = (f, a) match {
    case (Success(f), Success(a)) => success(f(a))
    case (Success(_), Failure(e)) => failure(e)
    case (Failure(e), Success(_)) => failure(e)
    case (Failure(e1), Failure(e2)) => failure(e1 ⊹ e2)
  }
}

Монад или Аппликативный, как выбрать?

По-прежнему читаешь? (Да, Эд)

Я показал, что суб-вычисления на основе Option или [A] Validation[E, A] могут быть объединены либо с Apply, либо с Bind. Когда вы выберете один за другим?

Когда вы используете Apply, структура вычисления фиксирована. Все суб-вычисления будут выполнены; результаты одного не могут влиять на других. Только "чистая" функция имеет обзор того, что произошло. С другой стороны, монадические вычисления позволяют первыми подсчислить влияние на более поздние.

Если бы мы использовали структуру монодальной проверки, первый сбой был бы кратковременным завершением всей проверки, так как не было бы значения Success для последующей последующей проверки. Тем не менее, мы рады, что суб-валидации являются независимыми, поэтому мы можем объединить их с помощью Applicative и собрать все неудачи, с которыми мы сталкиваемся. Слабость аппликативных функторов стала силой!