0

可能的重复:
函数式编程与面向对象编程

有人可以向我解释为什么我需要函数式编程而不是 OOP 吗?

例如,为什么我需要使用 Haskell 而不是 C++(或类似语言)?

函数式编程相对于 OOP 的优势是什么?

4

3 回答 3

14

我在函数式编程中更喜欢的一件大事是缺乏“远距离幽灵般的动作”。所见即所得——仅此而已。这使得代码更容易推理。

让我们用一个简单的例子。假设我遇到了X = 10Java(OOP)或 Erlang(函数式)中的代码片段。在 Erlang 中,我可以很快知道这些事情:

  1. 该变量X在我所处的直接上下文中。期间。它要么是传递给我正在阅读的函数的参数,要么是第一次(也是唯一的——参见下文)时间。
  2. X从此时起,该变量的值为 10。它不会在我正在阅读的代码块中再次更改。这不可以。

在Java中它更复杂:

  1. 变量X可能被定义为参数。
  2. 它可能在方法的其他地方定义。
  3. 它可能被定义为该方法所在类的一部分。
  4. 无论如何,由于我没有在这里声明它,所以我正在更改它的值。X这意味着如果不不断地向后扫描代码以找到它被显式或隐式分配或修改的最后一个位置(如在 for 循环中),我不知道 的值是多少。
  5. 当我调用另一个方法时,如果X碰巧是一个类变量,它可能会从我下面改变,如果不检查该方法的代码,我就无法知道这一点。
  6. 在线程程序的上下文中,情况更糟。 X可以被我什至在我的直接环境中看不到的东西改变。另一个线程可能正在调用 #5 中修改X.

而Java是一种比较简单的OOP语言。在 C++中可以使用的方法数量X甚至更多,而且可能更加晦涩难懂。

问题是?这只是一个简单的示例,说明在 OOP(或其他命令式)语言中的常见操作如何比在函数式中复杂得多。它也没有解决不涉及可变状态等的函数式编程的好处,例如高阶函数。

于 2011-01-14T10:27:38.140 回答
5

我认为 Haskell 有三点非常酷:

1) 它是一种静态类型语言,极具表现力,让您可以快速构建高度可维护和可重构的代码。在 Java 和 C# 等静态类型语言与 Python 和 Ruby 等动态语言之间存在很大争议。Python 和 Ruby 让您可以快速构建程序,只使用 Java 或 C# 等语言所需行数的一小部分。因此,如果您的目标是快速进入市场,Python 和 Ruby 是不错的选择。但是,因为它们是动态的,重构和维护你的代码是很困难的。在 Java 中,如果您想为方法添加参数,使用 IDE 很容易找到该方法的所有实例并修复它们。如果你错过了一个,编译器会捕捉到它。使用 Python 和 Ruby,重构错误只会被捕获为运行时错误。因此,对于传统语言,您一方面可以在快速开发和糟糕的可维护性之间进行选择,另一方面可以在缓慢的开发和良好的可维护性之间进行选择。两种选择都不是很好。

但是使用 Haskell,您不必做出这种选择。Haskell 是静态类型的,就像 Java 和 C# 一样。因此,您可以获得所有可重构性、IDE 支持的潜力和编译时检查。但同时,编译器可以推断类型。因此,它们不会像使用传统静态语言那样妨碍您。此外,该语言还提供了许多其他功能,让您只需几行代码即可完成很多工作。因此,您可以获得 Python 和 Ruby 的开发速度以及静态语言的安全性。

2) 并行性。因为函数没有副作用,所以编译器可以更轻松地并行运行,而无需您作为开发人员进行大量工作。考虑以下伪代码:

a = f x
b = g y
c = h a b

在纯函数式语言中,我们知道函数 f 和 g 没有副作用。因此,没有理由必须在 g 之前运行 f。订单可以交换,也可以同时运行。事实上,在函数 h 需要它们的值之前,我们根本不需要运行 f 和 g。这在传统语言中是不正确的,因为对 f 和 g 的调用可能会产生副作用,可能需要我们以特定的顺序运行它们。

随着计算机上的内核越来越多,函数式编程变得更加重要,因为它允许程序员轻松利用可用的并行性。

3)关于 Haskell 的最后一个非常酷的事情也可能是最微妙的:惰性求值。要理解这一点,请考虑编写一个程序来读取文本文件并打印出文件每一行中单词“the”出现的次数的问题。假设您正在使用传统的命令式语言进行编写。

尝试 1:您编写了一个打开文件并一次读取一行的函数。对于每一行,您计算“the's”的数量,然后将其打印出来。这很好,除了您的主要逻辑(计算单词)与您的输入和输出紧密耦合。假设您想在其他上下文中使用相同的逻辑?假设您想从套接字读取文本数据并计算字数?或者您想从 UI 中读取文本?你将不得不重新重写你的逻辑!

最糟糕的是,如果您想为新代码编写自动化测试怎么办?您必须构建输入文件、运行代码、捕获输出,然后将输出与预期结果进行比较。这是可行的,但它是痛苦的。通常,当您将 IO 与逻辑紧密耦合时,测试逻辑变得非常困难。

尝试 2:那么,让我们将 IO 和逻辑解耦。首先,将整个文件读入内存中的一个大字符串。然后,将字符串传递给一个函数,该函数将字符串分成几行,计算每行的“the's”,并返回一个计数列表。最后,程序可以遍历计数并输出它们。现在很容易测试核心逻辑,因为它不涉及 IO。现在可以轻松地将核心逻辑与来自文件、套接字或 UI 的数据一起使用。所以,这是一个很好的解决方案,对吧?

错误的。如果有人传入一个 100GB 的文件怎么办?由于必须将整个文件加载到字符串中,因此您会耗尽内存。

尝试 3:围绕读取文件和产生结果构建抽象。您可以将这些抽象视为两个接口。第一个有方法 nextLine() 和 done()。第二个有 outputCount()。您的主程序实现 nextLine() 和 done() 以从文件中读取,而 outputCount() 只是直接打印出计数。这允许您的主程序在恒定内存中运行。您的测试程序可以使用此抽象的替代实现,其中 nextLine() 和 done() 从内存中提取测试数据,而 outputCount() 检查结果而不是输出结果。

这第三次尝试很好地分离了逻辑和 IO,它允许您的程序在恒定内存中运行。但是,它比前两次尝试要复杂得多。

简而言之,传统的命令式语言(无论是静态的还是动态的)经常让开发人员在

a) IO 和逻辑的紧密耦合(难以测试和重用)

b)将所有内容加载到内存中(效率不是很高)

c) 构建抽象(复杂,并且会减慢实现速度)

在读取文件、查询数据库、读取套接字等时会出现这些选择。通常情况下,程序员似乎更喜欢选项 A,因此单元测试会受到影响。

那么,Haskell 如何在这方面提供帮助?在 Haskell 中,您将像在尝试 2 中一样解决这个问题。主程序将整个文件加载到一个字符串中。然后它调用一个函数来检查字符串并返回一个计数列表。然后主程序打印计数。由于它与 IO 隔离,因此测试和重用核心逻辑非常容易。

但是内存使用情况呢?Haskell 的惰性求值会为您解决这个问题。因此,即使您的代码看起来像是将整个文件内容加载到字符串变量中,但实际上并未加载整个内容。相反,仅在使用字符串时读取文件。这允许它一次读取一个缓冲区,并且您的程序实际上将在恒定内存中运行。也就是说,你可以在一个 100GB 的文件上运行这个程序,它会消耗很少的内存。

同样,您可以查询数据库,构建包含大量行的结果列表,然后将其传递给函数进行处理。处理函数不知道这些行来自数据库。因此,它与其 IO 分离。在幕后,行列表将被懒惰而有效地获取。因此,即使您在查看代码时看起来像这样,但完整的行列表永远不会同时全部在内存中。

最终结果,您可以测试处理数据库行的函数,甚至无需连接到数据库。

懒惰的评估真的很微妙,你需要一段时间才能理解它的力量。但是,它允许您编写易于测试和重用的简单代码。

这是最终的 Haskell 解决方案和 Approach 3 Java 解决方案。两者都使用常量内存并将 IO 与处理分开,以便于测试和重用。

哈斯克尔:

module Main
    where

import System.Environment (getArgs)
import Data.Char (toLower)

main = do
  (fileName : _) <- getArgs
  fileContents <- readFile fileName
  mapM_ (putStrLn . show) $ getWordCounts fileContents

getWordCounts = (map countThe) . lines . map toLower
    where countThe = length . filter (== "the") . words

爪哇:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.Reader;

class CountWords {
    public interface OutputHandler {
        void handle(int count) throws Exception;
    }

    static public void main(String[] args) throws Exception {
        BufferedReader reader = null;
        try {
            reader = new BufferedReader(new FileReader(new File(args[0])));

            OutputHandler handler = new OutputHandler() {
                public void handle(int count) throws Exception {
                    System.out.println(count);
                }
            };

            countThe(reader, handler);
        } finally {
            if (reader != null) reader.close();
        }
    }

    static public void countThe(BufferedReader reader, OutputHandler handler) throws Exception {
        String line;
        while ((line = reader.readLine()) != null) {
            int num = 0;
            for (String word: line.toLowerCase().split("([.,!?:;'\"-]|\\s)+")) {
                if (word.equals("the")) {
                    num += 1;
                }
            }
            handler.handle(num);
        }
    }
}
于 2011-01-14T20:49:35.500 回答
1

如果我们比较 Haskell 和 C++,函数式编程让调试变得非常容易,因为没有像 C、Python 等中的可变状态和变量那样你应该始终关心的,并且可以确保,给定一些参数,函数将始终无论您评估多少次,都返回相同的结果。

OOP 与任何编程范式都是正交的,并且有一些语言将 FP 与 OOP 结合在一起,OCaml是最流行的,还有几种 Haskell 实现等。

于 2011-01-14T10:23:06.667 回答