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

ewg*_*egw 0 oop paradigms programming-languages functional-programming

可能重复:
功能编程与面向对象编程

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

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

功能编程优于OOP有什么优势?

JUS*_*ION 14

在函数式编程中我更喜欢的一件大事就是缺乏"远距离的怪异动作".你所看到的就是你得到的 - 而不是更多.这使得代码更容易推理.

让我们用一个简单的例子.假设我遇到了X = 10Java(OOP)或Erlang(功能)中的代码片段.在Erlang中,我可以很快地了解这些事情:

  1. 变量X在我所处的直接上下文中.期间.它是传入我正在阅读的函数的参数,或者被分配给第一个(以及仅在下面的cf)时间.
  2. X从这一点开始,变量的值为10.它在我正在阅读的代码块中不会再次改变.这不可以.

在Java中,它更复杂:

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

Java是一种相对简单的 OOP语言.X在C++ 中可以使用的方法数量更多,而且可能更加模糊.

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


Cli*_*ler 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
Run Code Online (Sandbox Code Playgroud)

在纯函数式语言中,我们知道函数f和g没有副作用.所以,没有理由必须在g之前运行f.订单可以交换,也可以同时运行.事实上,我们真的不需要运行f和g,直到它们在函数h中需要它们的值.传统语言中的情况并非如此,因为对f和g的调用可能会产生副作用,可能需要我们按特定顺序运行它们.

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

3)关于Haskell的最后一个非常酷的事情也可能是最微妙的:懒惰的评估.要理解这一点,请考虑编写一个读取文本文件的程序的问题,并打印出文件每行上单词"the"的出现次数.假设你用传统的命令式语言写作.

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

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

尝试2:所以,让我们解耦IO和逻辑.首先,将整个文件读入内存中的大字符串.然后,将字符串传递给将字符串分成行的函数,计算每行上的"the",并返回计数列表.最后,程序可以遍历计数并输出它们.现在很容易测试核心逻辑,因为它不涉及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
Run Code Online (Sandbox Code Playgroud)

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);
        }
    }
}
Run Code Online (Sandbox Code Playgroud)