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

为什么Scala没有为每个monad定义返回/单元函数(与Haskell相反)?

  •  28
  • jhegedus  · 技术社区  · 11 年前

    与Haskell不同,Scala中的设计决策是因为monad没有返回/单元函数,而Haskell中每个monad都有一个返回函数,将一个值放入给定monad的标准monadic上下文中?

    例如,为什么List、Option、Set等没有在标准库中定义的返回/单位函数,如下面的幻灯片所示?

    我之所以问这个问题,是因为在Coursera的反应式课程中,Martin Odersky明确提到了这一事实,正如下面的幻灯片所示,但没有解释为什么Scala没有它们,尽管单位/返回是monad的一个基本属性。

    enter image description here enter image description here enter image description here

    4 回复  |  直到 11 年前
        1
  •  36
  •   Vladimir Matveev    11 年前

    正如rjan Johansen所说,Scala不支持返回类型的方法调度。Scala对象系统构建在JVM 1和JVM之上 invokevirtual 指令是动态多态性的主要工具,它根据 this 对象

    顺便说一句,调度是选择要调用的具体方法的过程。在Scala/Java中,所有方法都是虚拟的,也就是说,调用的实际方法取决于对象的实际类型。

    class A { def hello() = println("hello method in A") }
    
    class B extends A { override def hello() = println("hello method in B") }
    
    val x: A = new A
    x.hello()  // prints "hello method in A"
    
    val y: A = new B
    y.hello()  // prints "hello method in B"
    

    在这里,即使 y 变量的类型为 A , hello 方法来自 B 因为JVM“看到” y B 并调用适当的方法。

    然而,JVM只考虑调用方法的变量的类型。例如,在没有显式检查的情况下,不可能基于运行时类型的参数调用不同的方法。例如:

    class A {
      def hello(x: Number) = println(s"Number: $x")
      def hello(y: Int) = println(s"Integer: $y")
    }
    
    val a = new A
    val n: Number = 10: Int
    a.hello(n)  // prints "Number: 10"
    

    这里有两个同名但参数类型不同的方法。即使 n s实际类型为 Int , hello(Number) 版本被称为-它是基于 n 静态变量类型(这个特性,基于参数类型的静态解析,称为重载)。因此,对方法参数没有动态分派。一些语言也支持方法参数的分派,例如,CommonLisp的CLOS或Clojure的多方法就是这样工作的。

    Haskell拥有高级类型系统(它与Scala相当,事实上它们都起源于 System F ,但Scala类型系统支持子类型化,这使得类型推断更加困难),这至少允许在不启用某些扩展的情况下进行全局类型推断。Haskell还具有类型类的概念,这是它用于动态多态性的工具。类型类可以松散地认为是没有继承但带有参数分派的接口 和返回值 类型。例如,这是一个有效的类型类:

    class Read a where
        read :: String -> a
    
    instance Read Integer where
        read s = -- parse a string into an integer
    
    instance Read Double where
        read s = -- parse a string into a double
    

    然后根据调用方法的上下文, read 的函数 Integer Double 可以称为:

    x :: Integer
    x = read "12345"  // read for Integer is called
    
    y :: Double
    y = read "12345.0"  // read for Double is called
    

    这是一种非常强大的技术,在裸JVM对象系统中没有对应关系,因此Scala对象系统也不支持它。此外,缺少全尺寸类型推断会使此功能使用起来有些麻烦。因此,Scala标准库没有 return / unit 方法无处不在-不可能使用常规对象系统来表达它,根本没有地方可以定义这样的方法。因此,Scala中的monad概念是隐式的和常规的——所有的东西都是适当的 flatMap 方法可以被认为是一个monad,所有具有正确方法的东西都可以在 for 建设这很像鸭子打字。

    然而,Scala类型系统及其隐式机制足够强大,可以以形式化的方式表达功能齐全的类型类,并且可以扩展为泛型monad,尽管由于在完全类型推理方面存在困难,它可能需要添加比Haskell更多的类型注释。

    这是Scala中monad类型类的定义:

    trait Monad[M[_]] {
      def unit[A](a: A): M[A]
      def bind[A, B](ma: M[A])(f: A => M[B]): M[B]
    }
    

    这是它的实现 Option :

    implicit object OptionMonad extends Monad[Option] {
      def unit[A](a: A) = Some(a)
      def bind[A, B](ma: Option[A])(f: A => Option[B]): Option[B] =
        ma.flatMap(f)
    }
    

    然后可以以如下通用方式使用:

    // note M[_]: Monad context bound
    // this is a port of Haskell's filterM found here:
    // http://hackage.haskell.org/package/base-4.7.0.1/docs/src/Control-Monad.html#filterM
    def filterM[M[_]: Monad, A](as: Seq[A])(f: A => M[Boolean]): M[Seq[A]] = {
      val m = implicitly[Monad[M]]
      as match {
        case x +: xs =>
          m.bind(f(x)) { flg =>
            m.bind(filterM(xs)(f)) { ys =>
              m.unit(if (flg) x +: ys else ys)
            }
          }
        case _ => m.unit(Seq.empty[A])
      }
    }
    
    // using it
    
    def run(s: Seq[Int]) = {
      import whatever.OptionMonad  // bring type class instance into scope
    
      // leave all even numbers in the list, but fail if the list contains 13
      filterM[Option, Int](s) { a =>
        if (a == 13) None
        else if (a % 2 == 0) Some(true)
        else Some(false)
      }
    }
    
    run(1 to 16)  // returns None
    run(16 to 32)  // returns Some(List(16, 18, 20, 22, 24, 26, 28, 30, 32))
    

    在这里 filterM 对于 Monad 类型类。因为 OptionMonad 隐式对象存在于 过滤器M 呼叫站点,它将传递给 过滤器M 隐式地,它将能够利用它的方法。

    从上面可以看到,即使在Scala中,类型类也允许模拟返回类型的调度。事实上,这正是Haskell在幕后所做的——Scala和Haskell都在传递一个实现某个类型类的方法字典,尽管在Scala中,它更显式,因为这些“字典”是一级对象,可以按需导入,甚至可以显式传递,所以它并不是一个真正恰当的分派,因为它没有那么嵌入。

    如果您需要此数量的泛型,可以使用 Scalaz 库,其中包含许多类型类(包括monad)及其一些常见类型的实例,包括 选项 .

        2
  •  9
  •   Cyäegha    8 年前

    我不认为你真的是在说Scala的monad没有单位函数,而是 名称 单位函数的值可以变化。第二张幻灯片的示例中似乎显示了这一点。

    至于为什么会这样,我认为这仅仅是因为Scala在JVM上运行,而这些函数必须作为JVM方法来实现——它们唯一的标识是:

    • 他们所属的阶级;
    • 他们的名字;
    • 它们的参数类型。 但它们的返回类型无法识别。由于参数类型通常不会区分不同的单元函数(它通常只是一种泛型类型),因此需要为它们取不同的名称。

    在实践中,它们通常作为 apply(x) 方法。例如,对于类 List ,单位函数为 应用(x) 对象上的方法 列表 按照惯例, List.apply(x) 可以称为 List(x) 这也是比较常见/惯用的。

    所以我想Scala至少对单元函数有一个命名约定,尽管它没有唯一的名称:

    // Some monad :
    class M[T] {
      def flatMap[U](f: T => M[U]): M[U] = ???
    }
    // Companion object :
    object M {
      def apply(x: T): M[T] = ??? // Unit function
    }
    
    // Usage of the unit function :
    val x = ???
    val m = M(x)
    
        3
  •  5
  •   Chris Martin    11 年前

    Caveat:我还在学习Haskell,我在不断地编这个答案。


    首先,你已经知道的-Haskell的 do 符号减至 绑定 :

    Borrowing this example from Wikipedia:

    add mx my = do
      x <- mx
      y <- my
      return (x + y)
    
    add mx my =
      mx >>= (\x ->
        my >>= (\y ->
          return (x + y)))
    

    Scala与 对于产量 表示类似地,每个步骤都会降低 flatMap (相当于bind)。

    但有一点不同: <- 在a中,用于降低产量 map ,不到 平面地图 .

    def add(mx: Option[Int], my: Option[Int]) =
      for {
        x <- mx
        y <- my
      } yield x + y
    
    def add(mx: Option[Int], my: Option[Int]) =
      mx.flatMap(x =>
        my.map(y =>
          x + y))
    

    因此,由于没有最后一步的“扁平化”,表达式值已经具有monad类型,因此不需要使用类似于 return .

        4
  •  1
  •   ayvango    11 年前

    实际上,scala中有一个返回函数。很难找到。

    Scala在许多方面与Haskell略有不同。大多数差异都是JVM限制的直接后果。JVM无法根据其返回类型分派方法。因此Scala基于隐式证据引入了类型类多态性来解决这一问题。

    它甚至在scala标准集合中使用。您可能会注意到 CanBuildFrom CanBuild scala集合api中使用的隐式。看见 scala.collection.immutable.List 例如

    每次您想要构建自定义集合时,都应该为这个隐式编写实现。不过,写一本指南的指南并不多。我推荐你 this guide 。这说明了原因 CanBuildFrom 对于收藏以及如何使用它非常重要。事实上,这只是 return 函数,任何熟悉Haskell monads的人都会清楚地理解它的重要性。

    所以您可以使用自定义集合作为示例monad,并根据提供的教程编写其他monad。