scalaz中的函数语法益智游戏

oxb*_*kes 51 functional-programming scala scala-2.8 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))
Run Code Online (Sandbox Code Playgroud)

我试图了解<|*|>方法正在做什么,这里是源代码:

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

好的,这是相当令人困惑的(!) - 但它引用了<**>声明的方法:

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)
Run Code Online (Sandbox Code Playgroud)

所以我有几个问题:

  1. 为什么这个方法看起来采用了更高通道类型的一个类型参数(M[B])但是可以通过a Validation(它有两个类型的参数)?
  2. 语法(_: A, _: B)定义了第二种(A, B) => Pair[A,B]方法所期望的功能:在故障情况下Tuple2/Pair发生了什么?看不到任何元组!

ret*_*nym 64

将构造函数键入类型参数

M是一个Scalaz的主皮条客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] = ...
}
Run Code Online (Sandbox Code Playgroud)

什么是类型构造函数?

来自Scala语言参考:

我们区分了一阶类型和类型构造函数,它们采用类型参数和产量类型.称为值类型的一阶类型的子集表示(第一类)值的集合.值类型是具体的或抽象的.每个具体的值类型都可以表示为类类型,即类型指示符(第3.2.3节)引用类1(第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] {}.类型参数定义描述了类型构造函数的形状,它被称为Kind.List据说有那种' * -> *.

类型的部分应用

但是如何MA包装类型的值Validation[X, Y]?该类型Validation有一种(* *) -> *,只能作为类型参数传递给声明为的类型参数M[_, _].

对象Scalaz中的隐式转换将type的值转换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)
}
Run Code Online (Sandbox Code Playgroud)

而在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)
Run Code Online (Sandbox Code Playgroud)

这允许你Validation[String, Int]与a 结合Validation[String, Boolean],因为它们共享类型构造函数[A] Validation[String, A].

应用函数

<**>要求类型构造函数M必须具有ApplyFunctor的关联实例.这构成了一个Applicative Functor,它像Monad一样,是一种通过某种效果构建计算的方法.在这种情况下,效果是子计算可能失败(当它们发生时,我们会累积失败).

容器Validation[NonEmptyList[String], A]可以A在此"效果"中包含纯值类型.该<**>操作需要两个effectful值,和一个纯函数,并与适用函子实例该容器将它们组合.

以下是它适用于Optionapplicative functor的方法.这里的"效果"是失败的可能性.

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)
Run Code Online (Sandbox Code Playgroud)

在这两种情况下,都有一个类型的纯函数(String, Int) => String,应用于有效的参数.请注意,结果包含在相同的效果(或容器,如果您愿意),作为参数.

您可以在具有关联的Applicative Functor的众多容器中使用相同的模式.所有Monads都是自动应用的Functors,但还有更多,如ZipStream.

Option并且[A]Validation[X, A]都是Monads,所以你也可以使用Bind(又名flatMap):

val result3 = oi flatMap { i => os map { s => s * i } }
val result4 = for {i <- oi; s <- os} yield s * i
Run Code Online (Sandbox Code Playgroud)

使用`<|**|>`进行组合

<|**|>是非常相似的<**>,但它为您提供了从结果中简单地构建Tuple2的纯函数.(_: A, _ B)是一个简写(a: A, b: B) => Tuple2(a, b)

超越

这是我们的应用验证的捆绑示例.我使用稍微不同的语法来使用Applicative Functor,(fa ? fb ? fc ? fd) {(a, b, c, d) => .... }

更新:但是在失败案例中会发生什么?

失败案例中Tuple2/Pair发生了什么

如果任何子计算失败,则永远不会运行提供的函数.仅当所有子计算(在这种情况下,传递给的两个参数<**>)成功时才运行它.如果是这样,它将这些组合成一个Success.这个逻辑在哪里?这定义了Apply实例[A] Validation[X, A].我们要求类型X必须具有Semigroup可用性,这是将各个类型的各个错误组合X成相同类型的聚合错误的策略.如果选择String作为错误类型,则Semigroup[String]连接字符串; 如果您选择NonEmptyList[String],每个步骤中NonEmptyList的错误将连接成更长的错误.当Failures使用?运算符组合两个时,这种连接发生在下面(例如,使用运算符扩展)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)
  }
}
Run Code Online (Sandbox Code Playgroud)

Monad或Applicative,我该如何选择?

还在看?(是的.Ed)

我已经证明了基于Option[A] Validation[E, A]可以与其中任何一个Apply或与之结合的子计算Bind.你何时会选择一个而不是另一个?

使用时Apply,计算结构是固定的.将执行所有子计算; 一个人的结果不能影响其他人.只有"纯"函数才能概述发生的情况.另一方面,Monadic计算允许第一个子计算影响后面的计算.

如果我们使用Monadic验证结构,则第一次失败会使整个验证短路,因为没有任何Success值可以用于后续验证.但是,我们很高兴子验证是独立的,因此我们可以通过Applicative将它们组合在一起,并收集我们遇到的所有失败.Applicative Functors的弱点已成为一种力量!

  • 真的很好,详细的答案. (5认同)