Трудный вопрос синтаксиса функций в scalaz

Следующая презентация Nick Partidge наблюдения получения 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)

Таким образом, у меня есть несколько вопросов:

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

51
задан oxbow_lakes 30 March 2010 в 05:00
поделиться

1 ответ

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

M - параметр типа одного из главных сутенеров Скалаза, MA , который представляет собой конструктор типа (он же высший тип) измененного значения. Этот конструктор типа используется для поиска соответствующих экземпляров 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), которое дополнительно ограничивает {{ 1}} типы его членов. Абстрактные типы значений вводятся параметрами типа (§4.4) и привязками абстрактного типа (§4.3). Круглые скобки в типах используются для группировки. Мы предполагаем, что объекты и пакеты также неявно определяют класс (с тем же именем, что и объект или пакет, но недоступный для пользовательских программ).

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

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

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

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

Но как 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 . Это составляет аппликативный функтор, который, как монада,это способ структурировать вычисления с помощью некоторого эффекта. В этом случае эффект состоит в том, что подвычисления могут дать сбой (и когда это произойдет, мы накапливаем сбои).

Контейнер Проверка [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 , применяемая к действующим аргументам. Обратите внимание, что результат заключен в тот же эффект (или контейнер, если хотите), что и аргументы.

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

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

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

Tupling with ` <| ** |> `

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

И не только

Вот наши объединенные примеры для Аппликатив и Проверка .Я использовал немного другой синтаксис для использования Applicative Functor, (fa ⊛ fb ⊛ fc ⊛ fd) {(a, b, c, d) => ....}

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

что происходит с кортежем2 / парой в случае сбоя ?

Если какое-либо из дополнительных вычислений не удается, предоставленная функция никогда не запускается. Он запускается только в том случае, если все подвычисления (в данном случае два аргумента, переданные в <**> ) успешны. Если это так, он объединяет их в Успех . Где эта логика? Это определяет экземпляр Применить для [A] Проверка [X, A] . Мы требуем, чтобы тип X имел доступную полугруппу , которая представляет собой стратегию объединения отдельных ошибок типа X в агрегированную ошибку того же типа. Если вы выберете String в качестве типа ошибки, Semigroup [String] объединит строки; если вы выберете NonEmptyList [String] , ошибки каждого шага будут объединены в более длинный NonEmptyList ошибок. Эта конкатенация происходит ниже, когда два сбоя объединяются с использованием оператора (который расширяется с имплицитами, например, до 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)
  }
}

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

Все еще читаете? ( Да.Ed )

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

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

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

64
ответ дан 7 November 2019 в 10:19
поделиться
Другие вопросы по тегам:

Похожие вопросы: