代码之家  ›  专栏  ›  技术社区  ›  oxbow_lakes

scalaz中的函数语法拼图

  •  50
  • oxbow_lakes  · 技术社区  · 15 年前

    跟踪观察 Nick Partidge's presentation 关于推导 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. 为什么这种方法似乎需要 高科型 一种类型参数( M[B] )但是可以通过 Validation (哪个有两种类型的参数表)?
    2. 句法 (_: A, _: B) 定义函数 (A, B) => Pair[A,B] 第二种方法期望: 在失败的情况下,tuple2/对发生了什么情况?看不到元组!
    1 回复  |  直到 14 年前
        1
  •  63
  •   oxbow_lakes    14 年前

    类型构造函数作为类型参数

    M 是Scalaz的一个主要皮条客的类型参数, MA ,它表示pimped值的类型构造函数(也称为更高的同类类型)。此类型构造函数用于查找 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语言参考:

    我们区分一阶 类型和类型构造函数,其中 采用类型参数和屈服类型。 一阶类型的子集称为 值类型表示 (一等)价值观。值类型是 具体的或抽象的。每个 具体值类型可以表示 作为类类型,即类型 指代 Class1(5.3),或作为复合类型 (3.2.7)表示交叉口 类型,可能是经过改进的 (§3.2.7)进一步限制 其成员的类型。抽象价值 类型按类型介绍 参数(§4.4)和抽象类型 绑定(第4.3节)。类型中的括号 用于分组。我们假设 对象和包也隐式地 定义一个类(与 对象或包,但是 用户程序无法访问)。

    非值类型捕获的属性 不是值的标识符 (3.3)。例如,类型 构造器(3.3.3)不直接 指定值的类型。然而, 当类型构造函数应用于 正确的类型参数会产生 一阶类型,可以是 值类型。非值类型是 用scala间接表达。例如,A 方法类型是通过编写 下一个方法签名,其中 它本身不是真正的类型,尽管它 产生相应的功能 类型(§3.3.1)。类型构造函数是 另一个例子,可以写类型 交换[m[,],a,b]=m[b,a],但是 没有语法来编写 对应的匿名类型函数 直接。

    List 是类型构造函数。您可以应用类型 Int 要获取值类型, List[Int] ,它可以对值进行分类。其他类型构造函数接受多个参数。

    特质 scalaz.MA 要求它的第一个类型参数必须是一个类型构造函数,该构造函数接受单个类型以返回值类型,语法为 trait MA[M[_], A] {} . 类型参数定义描述类型构造函数的形状,该类型构造函数称为其类型。 据说有那种 * -> * .

    类型的部分应用

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

    这种隐式转换 object Scalaz 转换类型的值 验证[X,Y] 妈妈 :

    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 部分应用类型构造函数 验证 ,修复错误类型,但不应用成功类型。

    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] .

    应用函子

    <**; 要求类型构造函数 必须具有的关联实例 Apply Functor . 这就构成了一个应用函子,它和单子一样,是通过某种效应来构造计算的一种方法。在这种情况下,结果是子计算可能会失败(当它们失败时,我们会累积失败)。

    集装箱 Validation[NonEmptyList[String], A] 可以包装类型为的纯值 A 在这个“效果”。这个 <**; 运算符获取两个有效值和一个纯函数,并将它们与该容器的应用函子实例组合。

    以下是它对 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 .

    选择权 [A]Validation[X, A] 都是单子,所以你也可以用 Bind (又名平面图):

    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函子, (fa ⊛ fb ⊛ fc ⊛ fd) {(a, b, c, d) => .... }

    更新:但是在失败的情况下会发生什么呢?

    故障情况下tuple2/对发生了什么 ?

    如果任何子计算失败,则永远不会运行提供的函数。只有在所有子计算(在本例中,传递给 <**; )都是成功的。如果是的话,它将这些组合成 Success . 这个逻辑在哪里?这就定义了 应用 实例为 [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)
      }
    }
    

    单子还是应用程序,我该怎么选择?

    还在看书吗?( 对。预计起飞时间 )

    我已经证明了子计算基于 选择权 [A] Validation[E, A] 可以与 应用 或与 束缚 . 你什么时候会选择一个而不是另一个?

    当你使用 应用 ,计算结构是固定的。所有的子计算都将执行;其中一个的结果不能影响其他的。只有“pure”函数对发生的事情有一个概述。另一方面,一元计算允许第一个子计算影响后面的子计算。

    如果我们使用一元验证结构,第一个失败将使整个验证短路,因为没有 成功 要输入到后续验证中的值。但是,我们很高兴子验证是独立的,因此我们可以通过应用程序将它们组合起来,并收集我们遇到的所有失败。应用函子的弱点已经成为一种优势!