40

Scala 和 F# 统一 OO 和 FP 范式的方法之间的主要区别是什么?

编辑

每种方法的相对优缺点是什么?如果尽管支持子类型化,F# 可以推断函数参数的类型,那么为什么 Scala 不能?

4

7 回答 7

43

我看过F#,做低级教程,所以我对它的了解非常有限。然而,对我来说很明显,它的风格本质上是功能性的,OO 更像是一个附加组件——比真正的 OO 更像是一个 ADT + 模块系统。我得到的感觉可以最好地描述为好像其中的所有方法都是静态的(如 Java 中的静态)。

例如,参见任何使用管道运算符 ( |>) 的代码。从F# 上的维基百科条目中获取此片段:

[1 .. 10]
|> List.map     fib

(* equivalent without the pipe operator *)
List.map fib [1 .. 10]

该函数map不是列表实例的方法。相反,它的工作方式类似于List模块上的静态方法,将列表实例作为其参数之一。

另一方面,Scala 是完全面向对象的。首先,让我们从与该代码等效的 Scala 开始:

List(1 to 10) map fib

// Without operator notation or implicits:
List.apply(Predef.intWrapper(1).to(10)).map(fib)

这里,map是 的实例上的一个方法List。类似静态的方法,例如intWrapperonPredefapplyon List,要少得多。然后是函数,比如fib上面。在这里,fib不是 on 的方法int,但也不是静态方法。相反,它是一个对象——我看到的 F# 和 Scala 之间的第二个主要区别。

让我们考虑一下 Wikipedia 中的 F# 实现和等效的 Scala 实现:

// F#, from the wiki
let rec fib n =
    match n with
    | 0 | 1 -> n
    | _ -> fib (n - 1) + fib (n - 2)

// Scala equivalent
def fib(n: Int): Int = n match {
  case 0 | 1 => n
  case _ => fib(n - 1) + fib(n - 2)
}

上面的 Scala 实现是一个方法,但是 Scala 将它转换为一个函数,以便能够将它传递给map. 我将在下面对其进行修改,使其成为一个返回函数的方法,以展示函数在 Scala 中的工作方式。

// F#, returning a lambda, as suggested in the comments
let rec fib = function 
    | 0 | 1 as n -> n 
    | n -> fib (n - 1) + fib (n - 2)

// Scala method returning a function
def fib: Int => Int = {
  case n @ (0 | 1) => n
  case n => fib(n - 1) + fib(n - 2)
}

// Same thing without syntactic sugar:
def fib = new Function1[Int, Int] {
  def apply(param0: Int): Int = param0 match {
    case n @ (0 | 1) => n
    case n => fib.apply(n - 1) + fib.apply(n - 2)
  }
}

因此,在 Scala 中,所有函数都是实现 trait 的对象FunctionX,它定义了一个名为apply. 如这里和上面的列表创建所示,.apply可以省略,这使得函数调用看起来就像方法调用。

最后,Scala 中的一切都是一个对象——也是一个类的实例——每个这样的对象都属于一个类,所有的代码都属于一个方法,它会以某种方式被执行。即使match在上面的例子中曾经是一个方法,但是在很久以前已经被转换为一个关键字以避免一些问题。

那么,它的功能部分呢?F# 属于最传统的函数式语言家族之一。虽然它没有某些人认为对函数式语言很重要的特性,但事实是 F#默认情况下是函数式,可以这么说。

另一方面,创建 Scala 的目的是统一功能模型和 OO 模型,而不是仅仅将它们作为语言的单独部分提供。它成功的程度取决于您认为是函数式编程。以下是 Martin Odersky 关注的一些事情:

  • 函数就是值。它们也是对象——因为在 Scala 中所有的值都是对象——但是函数是一个可以被操作的值的概念是一个重要的概念,它的根源可以追溯到最初的 Lisp 实现。

  • 对不可变数据类型的强大支持。函数式编程一直关注减少对程序的副作用,即函数可以被分析为真正的数学函数。所以 Scala 让事情变得不可变变得很容易,但它并没有做两件 FP 纯粹主义者批评它的事情:

    • 它并没有使可变性变得更加困难
    • 它不提供可以静态跟踪可变性的效果系统。
  • 支持代数数据类型。代数数据类型(称为 ADT,令人困惑地也代表抽象数据类型,这是另一回事)在函数式编程中非常常见,并且在通常使用 OO 语言中的访问者模式的情况下最有用。

    与其他一切一样,Scala 中的 ADT 被实现为类和方法,并带有一些语法糖,使它们使用起来很轻松。但是,Scala 在支持它们方面比 F#(或其他函数式语言)要冗长得多。例如|,它使用 .而不是 F# 的 for case 语句case

  • 支持非严格性。非严格意味着只按需计算东西。它是 Haskell 的一个重要方面,它与副作用系统紧密集成。然而,在 Scala 中,非严格性支持是相当胆小的和初期的。它是可用和使用的,但以一种受限制的方式。

    例如,Scala 的非严格列表 theStream不支持真正的非严格列表foldRight,如 Haskell 所支持的。此外,非严格性的一些好处只有当它是语言中的默认值而不是选项时才能获得。

  • 支持列表理解。实际上,Scala 称之为for-comprehension,因为它的实现方式完全脱离了列表。用最简单的术语来说,列表推导可以被认为是map示例中显示的函数/方法,尽管通常需要嵌套 map 语句(flatMap在 Scala 中支持 with)以及过滤(filterwithFilter在 Scala 中,取决于严格性要求)。

    这是函数式语言中非常常见的操作,并且通常在语法上很简单——就像 Python 的in运算符一样。同样,Scala 比平时更冗长。

在我看来,Scala 在结合 FP 和 OO 方面是无与伦比的。它来自频谱的 OO 侧朝向 FP 侧,这是不寻常的。大多数情况下,我看到处理了面向对象的 FP 语言——我觉得它已经处理好了。我猜 Scala 上的 FP 可能对函数式语言程序员有同样的感觉。

编辑

阅读其他一些答案后,我意识到还有另一个重要主题:类型推断。Lisp 是一种动态类型语言,它几乎设定了对函数式语言的期望。现代静态类型的函数式语言都有强大的类型推断系统,最常见的是Hindley-Milner 1算法,它使类型声明本质上是可选的。

Scala 不能使用 Hindley-Milner 算法,因为 Scala 支持继承2。所以 Scala 必须采用一种功能更弱的类型推断算法——事实上,Scala 中的类型推断在规范中是有意未定义的,并且是持续改进的主题(它的改进是即将到来的 2.8 版本的最大特性之一)斯卡拉,例如)。

然而,最后,Scala 要求所有参数在定义方法时都声明其类型。在某些情况下,例如递归,还必须声明方法的返回类型。

不过,Scala 中的函数通常可以推断而不是声明其类型。例如,这里不需要类型声明:List(1, 2, 3) reduceLeft (_ + _),其中_ + _实际上是类型的匿名函数Function2[Int, Int, Int]

同样,变量的类型声明通常是不必要的,但继承可能需要它。例如,Some(2)None有一个共同的超类Option,但实际上属于不同的子类。因此,通常会声明var o: Option[Int] = None以确保分配了正确的类型。

这种类型推断的有限形式比通常提供的静态类型的 OO 语言要好得多,这给 Scala 一种轻盈的感觉,但比通常提供的静态类型的 FP 语言要糟糕得多,这给 Scala 一种沉重的感觉。:-)

笔记:

  1. 实际上,根据维基百科,该算法起源于 Damas 和 Milner,他们将其称为“算法 W” 。

  2. Martin Odersky 在此处的评论中提到:

    Scala 没有 Hindley/Milner 类型推断的原因是它很难与重载(即席变体,不是类型类)、记录选择和子类型等特性结合起来

    他接着说,这实际上可能并非不可能,并且归结为一种权衡。请转到该链接以获取更多信息,并且,如果您确实提出了更清晰的陈述,或者更好的是,以某种方式提出了一些论文,我将不胜感激。

    让我感谢Jon Harrop查找此内容,因为我认为这是不可能的。好吧,也许是这样,我找不到合适的链接。但是请注意,导致问题的不仅仅是继承。

于 2010-05-25T21:53:30.570 回答
14

F#是功能性的——它很好地支持OO,但设计和理念仍然是功能性的。例子:

  • Haskell 风格的函数
  • 自动咖喱
  • 自动泛型
  • 参数的类型推断

以主要面向对象的方式使用 F# 感觉相对笨拙,因此可以将主要目标描述为将 OO 集成到函数式编程中。

Scala是多范式,专注于灵活性。您可以根据当前最适合的方式在真实的 FP、OOP 和程序风格之间进行选择。这实际上是关于统一 OO 和函数式编程

于 2010-05-25T14:30:09.037 回答
13

Scala 和 F# 统一 OO 和 FP 范式的方法之间的主要区别是什么?

主要区别在于 Scala 试图通过牺牲(通常在 FP 方面)来混合范式,而 F#(和 OCaml)通常在范式之间划清界限,让程序员为每个任务在它们之间进行选择。

为了统一范式,Scala 不得不做出牺牲。例如:

  • 一等函数是任何函数式语言(ML、Scheme 和 Haskell)的基本特征。F# 中的所有函数都是一流的。成员函数在 Scala 中是二等的。

  • 重载和子类型会阻碍类型推断。F# 提供了一种大型子语言,它牺牲了这些 OO 特性,以便在不使用这些特性时提供强大的类型推断(使用时需要类型注释)。Scala 将这些特性推向了各个地方,以保持一致的 OO,但代价是到处都是糟糕的类型推断。

这样做的另一个结果是,F# 基于久经考验的想法,而 Scala 在这方面处于领先地位。这非常适合项目背后的动机:F# 是商业产品,Scala 是编程语言研究。

顺便说一句,由于他们选择的 VM(JVM)的限制,Scala 还出于实用的原因牺牲了 FP 的其他核心特性,例如尾调用优化。这也使得 Scala 比 FP 更 OOP。请注意,有一个将 Scala 引入 .NET 的项目,该项目将使用 CLR 来实现真正的 TCO。

每种方法的相对优缺点是什么?如果尽管支持子类型化,F# 可以推断函数参数的类型,那么为什么 Scala 不能?

类型推断与重载和子类型等以 OO 为中心的特性不一致。F# 在重载方面选择了类型推断而不是一致性。Scala 选择了无处不在的重载和子类型而不是类型推断。这使得 F# 更像 OCaml,而 Scala 更像 C#。特别是,Scala 不像 C# 那样是一种函数式编程语言。

当然,哪个更好完全是主观的,但我个人更喜欢一般情况下强大的类型推断所带来的巨大简洁和清晰。OCaml 是一门很棒的语言,但一个痛点是缺少运算符重载,这要求程序员将其+用于整数、+.浮点数、+/有理数等等。再一次,F# 选择实用主义而不是痴迷,通过牺牲类型推断来专门在数字上下文中重载,不仅在算术运算符上,而且在算术函数上,例如sin. F# 语言的每一个角落都是经过精心挑选的务实权衡的结果。尽管产生了不一致,但我相信这会使 F# 更加有用。

于 2010-06-04T21:35:20.083 回答
13

您可以使用很多点来比较两者(或三个)。首先,这里有一些我能想到的值得注意的点:

  • 语法
    在 语法上,F#OCaml基于函数式编程传统(空间分隔且更轻量级),而Scala基于面向对象的风格(尽管 Scala 使其更轻量级)。

  • 集成 OO 和 FP F#Scala
    都非常顺利地将 OO 与 FP 集成(因为这两者之间没有矛盾!!)您可以声明类来保存不可变数据(功能方面)并提供与处理数据相关的成员,您可以还使用抽象接口(面向对象方面)。我对OCaml不太熟悉,但我认为它更加强调 OO 方面(与 F# 相比)

  • F#
    中的编程风格我认为 F# 中使用的常用编程风格如果您不需要编写 .NET 库并且没有其他限制)可能更具功能性,并且仅在需要时才使用 OO 功能. 这意味着您可以使用函数、模块和代数数据类型对功能进行分组。

  • Scala 中的编程风格 在
    Scala 中,默认的编程风格更加面向对象(在组织中),但是您仍然(可能)编写函数式程序,因为“标准”方法是编写避免突变的代码。

于 2010-05-25T20:14:44.230 回答
7

这篇关于编程语言的文章:

Scala 是 Java 的坚固、富有表现力、绝对优越的替代品。Scala 是我将用于编写 Web 服务器或 IRC 客户端等任务的编程语言。 与 OCaml [或 F#](一种移植了面向对象系统的函数式语言)相比,Scala 感觉更像是面向对象和函数式编程的真正混合体。(也就是说,面向对象的程序员应该能够立即开始使用 Scala,只选择他们选择的功能部分。)

我第一次了解 Scala 是在 2006 年的 POPL 上,当时 Martin Odersky 做了一个关于它的受邀演讲。当时我认为函数式编程严格优于面向对象编程,因此我认为不需要一种融合函数式编程和面向对象编程的语言。(那可能是因为我当时写的只是编译器、解释器和静态分析器。)

直到我从头开始编写并发 HTTPD 以支持 yaplet 的长轮询 AJAX 之前,对 Scala 的需求对我来说并不明显。为了获得良好的多核支持,我用 Java 编写了第一个版本。作为一门语言,我认为 Java 并没有那么糟糕,而且我可以享受做得好的面向对象编程。然而,作为一名函数式程序员,当我用 Java 编程时,对函数式编程特性(如高阶函数)缺乏(或不必要的冗长)支持让我感到恼火。所以,我给了 Scala 一个机会。

Scala 在 JVM 上运行,因此我可以逐步将现有项目移植到 Scala 中。这也意味着 Scala 除了它自己相当大的库之外,还可以访问整个 Java 库。这意味着您可以在 Scala 中完成真正的工作。

当我开始使用 Scala 时,我对函数式世界和面向对象世界的巧妙融合印象深刻。特别是,Scala 有一个强大的案例类/模式匹配系统,它解决了我在使用标准 ML、OCaml 和 Haskell 的经验中挥之不去的烦恼:程序员可以决定对象的哪些字段应该是可匹配的(而不是被迫匹配所有这些),并且允许使用可变参数。事实上,Scala 甚至允许程序员定义模式。我写了很多在抽象语法节点上操作的函数,很高兴能够只匹配语法子节点,但在原始程序中仍然有诸如注释或行之类的字段。案例类系统允许将代数数据类型的定义拆分到多个文件或同一文件的多个部分,这非常方便。

Scala 还通过称为特征的类类设备支持定义良好的多重继承。

Scala 还允许相当程度的重载;甚至函数应用程序和数组更新也可以重载。根据我的经验,这会使我的 Scala 程序更加直观和简洁。

与类型类在 Haskell 中保存代码的方式相同,可以节省大量代码的一个特性是隐式。您可以将implicits 想象为类型检查器的错误恢复阶段的API。简而言之,当类型检查器需要一个 X 但得到一个 Y 时,它将检查范围内是否存在将 Y 转换为 X 的隐式函数;如果找到一个,它会使用隐式“强制转换”。这使得您可以看起来像在 Scala 中扩展几乎任何类型,并且它允许更紧密地嵌入 DSL。

从上面的摘录中可以清楚地看出,Scala 统一 OO 和 FP 范式的方法远优于 OCaml 或 F#。

HTH。

问候,
埃里克。

于 2010-05-26T07:02:07.363 回答
5

F# 的语法取自 OCaml,但 F# 的对象模型取自 .NET。这为 F# 提供了函数式编程语言特有的轻巧简洁的语法,同时允许 F# 通过其对象模型与现有的 .NET 语言和 .NET 库进行非常顺畅的互操作。

Scala 在 JVM 上的工作与 F# 在 CLR 上的工作类似。然而,Scala 选择采用更类似于 Java 的语法。这可能有助于面向对象的程序员采用它,但对于函数式程序员来说,它可能会感觉有点沉重。它的对象模型类似于 Java 允许与 Java 无缝互操作,但有一些有趣的差异,例如对特征的支持。

于 2010-05-25T16:37:46.170 回答
3

如果函数式编程意味着使用函数进行编程,那么 Scala 会稍微改变一下。在 Scala 中,如果我理解正确的话,您正在使用方法而不是函数进行编程。

当方法背后的类(和该类的对象)无关紧要时,Scala 会让你假装它只是一个函数。也许 Scala 语言律师可以详细说明这种区别(如果它甚至是一种区别)以及任何后果。

于 2010-05-25T20:49:50.313 回答