【问题标题】:Can someone explain to me why I would need functional programming instead of OOP? [duplicate]有人可以向我解释为什么我需要函数式编程而不是 OOP 吗? [复制]
【发布时间】:2011-06-09 01:19:43
【问题描述】:

可能重复:
Functional programming vs Object Oriented programming

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

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

函数式编程比 OOP 有什么优势?

【问题讨论】:

标签: oop programming-languages functional-programming paradigms


【解决方案1】:

我更喜欢函数式编程的一大特点是没有“远距离的诡异动作”。所见即所得——仅此而已。这使得代码更容易推理。

让我们用一个简单的例子。假设我在 Java(OOP)或 Erlang(函数式)中遇到了代码 sn-p X = 10。在 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(或其他命令式)语言中的常见操作如何比在函数式语言中复杂得多。它也没有解决不涉及可变状态等的函数式编程的好处,例如高阶函数。

【讨论】:

    【解决方案2】:

    我认为 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
    

    Java:

    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);
            }
        }
    }
    

    【讨论】:

    • 您将惰性求值与惰性 IO 混合在一起。两者是不同的东西,虽然惰性 IO 在您的示例中很棒,但它也可以让您字节 - 例如,如果您尝试从已经关闭的文件中读取。当您说 In fact, we really don't have to run f and g at all until their values are needed in function h. 时,您正确地描述了惰性评估
    【解决方案3】:

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

    OOP 与任何编程范式都是正交的,并且有一些将 FP 与 OOP 相结合的语言,OCaml 最受欢迎,several Haskell implementations 等。

    【讨论】:

    • 好吧,我的意思是,如果有人要使用 C++ 中的全局变量编写程序,可能很难调试。幸运的是,您也不会在 OOP 中这样做。那里的功能没有胜利;这只是糟糕的 C 类。
    • 如果您将“全局状态”替换为“可变状态”,我会为您的答案 +1。用 OOP 语言在没有全局状态的情况下进行编程是很可能的(实际上其中一些强制这样做)。但国家的全球性并不是主要问题。正是状态可以改变的事实造成了问题。
    最近更新 更多