为什么我更喜欢函数式编程?小白指南_编程入门攻略

在学习 Haskell 之前,作者一直使用主流语言,如 Java、C 和 C++——现在他仍然喜欢它们。那么,一个命令式开发人员如何转变成了一个 Haskell 开发者?他将在本文中将对此做出解释——尤其是对那些在函数式编程方面经验较少的开发人员。本文最初发布于 Mario Morgenthum 的个人博客,由 InfoQ 中文站翻译并分享。首先,我将通过对

为什么我更喜欢函数式编程?小白指南

在学习 Haskell 之前,作者一直使用主流语言,如 Java、C 和 C++——现在他仍然喜欢它们。那么,一个命令式开发人员如何转变成了一个 Haskell 开发者?他将在本文中将对此做出解释——尤其是对那些在函数式编程方面经验较少的开发人员。

为什么我更喜欢函数式编程?小白指南_编程入门攻略

本文最初发布于 Mario Morgenthum 的个人博客,由 InfoQ 中文站翻译并分享。

首先,我将通过对一些主题的讨论比较函数式编程和面向对象编程,因为它是最流行的范式。在第一个代码示例中,我将简要介绍 Haskell 的语法,因为我将在本文中使用它。

控制流

控制流描述你如何告诉程序做什么——形成算法。基本控制元素有以下三种:

  • 顺序——顺序执行代码

  • 重复——重复执行代码

  • 选择——根据条件将代码划分成分支

面向对象编程

  • 顺序是语句逐行执行

  • 重复是循环,如 for 或 while 语句,或递归

  • 选择是 if … else 或 switch 语句

下面这个简单的例子使用 Java 实现文本居中显示。该文本是作为一个字符串数组传入的。每一行是这个数组的一个元素:

void alignCenter(String[] text)
{
    int maxLength = 0;
    for (String line : text) {
        if (line.length() > maxLength) {
            maxLength = line.length();
        }
    }
    for (int i = 0; i < text.length; ++i)
    {
        int spaceCount = (maxLength - text[i].length()) / 2;
        StringBuilder builder = new StringBuilder();
        for (int j = 0; j < spaceCount; ++j)
        {
            builder.append(' ');
        }
        builder.append(text[i]);
        text[i] = builder.toString();
    }
}

函数式编程

  • 顺序是链式调用

  • 重复是递归

  • 选择是模式匹配,或 case … of 或 if … else 表达式

下面是同一个例子的 Haskell 实现,展示模式匹配和递归的用法:

alignCenter :: [String] -> [String]
alignCenter xs = alignCenter' maxLength xs
    where maxLength = maximum (map length xs)
alignCenter' :: Int -> [String] -> [String]
alignCenter' _ [] = []
alignCenter' n (x:xs) = (replicate spaceCount ' ' ++ x) : alignCenter' n xs
    where spaceCount = div (n - length x) 2

下面是一个没有使用递归的简化版本,使用了 map 和 lambda 函数:

alignCenter :: [String] -> [String]
alignCenter xs = map (\x -> replicate (div (n - length x) 2) ' ' ++ x) xs
    where n = maximum (map length xs)

Haskell 简介

函数的第一行是签名。签名 alignCenter :: [String] -> [String] 告诉我们这是一个名为 alignCenter 的函数,其输入是一个字符串列表,输出是一个新字符串列表(从左往右读)。

第一个函数确定字符串列表中最长的行,并调用第二个函数。我们通过一个简单的表达式 maximum (map length xs) 终止第一个循环。那么它是如何工作的?让我们看下涉及到的所有函数的签名。

length :: [a] -> Int
map :: (a -> b) -> [a] -> [b]
maximum :: [a] -> a

length 函数的输入是一个任意类型的列表,输出是一个 Int 值。类型签名中的所有小写类型都是类型变量,类似于 Java 中 List 里的 T。我认为函数的功能非常明了。

map 函数接收两个参数,第一个是 a -> b 类型的函数,第二个是 [a],返回值是 [b]。 那么,“它接收一个函数作为参数”是什么意思呢?是的,这是真的。你可以将函数作为参数传递,不过不能是函数指针(如 C 语言中),也不能是方法引用(如 Java 语言中),要是作为第一类值的真正函数。以函数为参数或返回新函数作为结果的函数称为高阶函数。那么,这个函数是干什么用的呢?它将 [a] 的每个元素传递给 a -> b 函数,后者将 a 转换为 b,并把它们汇集到一个新列表 [b] 中。

现在让我们解析下类型变量 map length xs,其中,xs 是 [String] 类型。

map :: (String -> Int) -> [String] -> [Int]

你需要知道 String 是 [Char] 类型的同义词,表示字符列表。这就是为什么它兼容 length 函数。表达式 map length [“Hello”, “World!”] 会被解析成 [5, 6]。我们感兴趣的是列表中最长字符串的长度,因此,我们将结果列表传给 maximum,它会返回列表中长度最大的元素,即 6。

我们看下第二个函数:

alignCenter' :: Int -> [String] -> [String]

你可能已经注意到函数名末尾的’。没有什么特别的,它只是 Haskell 中一个有效的标识符字符,因为它在数学中是一个常用符号,表示与先前标识符相关的名称。该函数是递归的,我们遍历文本的每一行,进行转换,并将转换后的行放在所有剩余元素的递归调用之前。

alignCenter’ _[] =[] 这行代码是递归基本型。它的意思是:如果第二个参数是空列表,那么返回一个空列表,因为没有什么可做。在这种情况下,我们对第一个参数的值不感兴趣,所以我们不需要为它命名而只需要以 _ 表示。

以下几行代码就完成了整个工作:

alignCenter' n (x:xs) = (replicate spaceCount ' ' ++ x) : alignCenter' n xs
    where spaceCount = div (n - length x) 2

我们将第一个参数绑定到 n,将第二个参数(一个列表)与模式 (x:xs) 进行匹配,这意味着:将列表的第一个元素绑定到 x,其余所有元素绑定到 xs。我们会根据需要复制空格,将它们与当前元素 x 串在一起,并在所有剩余的元素 xs 递归调用的结果列表前加上:。就这些。

在递归操作(reduction step)之前声明递归的结束条件(base case)非常重要,因为编译器自顶向下运行,并采用它找到的第一个匹配模式。

小结

与相同代码的 OOP 版本相比,我们使用模式匹配和抽象函数节省了大量代码。好了,现在你可能会抱怨:“嗯,你只是把整个代码隐藏在库函数里了,比如 replicate、map 和 maximum”——我告诉你:“是的,当然!因为我不需要成千上万次地重复编写同样的 for 循环!”老实说,Java 代码可以使用 leftPad 之类的东西来复制空格,但它是一个非常具体的函数,专门用于填充字符串,没有其他用途。

在函数式编程中,你能够以一种简单的方式抽象常见的循环用例来执行映射、过滤、折叠和展开等任务。在 OOP 中,如果没有大量的样板代码(如后台 接口 和内置语法糖),你将无法实现这样优雅的解决方案。

概 念

这些概念描述了构建应用程序的基本思想。代码、数据及其交互在各自的范式中是如何表示的?

面向对象编程

面向对象编程引入了接口、类、继承和对象的概念。对象包含数据字段和方法代码,这些方法通过操作字段来更改对象状态。

函数式编程

函数式编程的核心是函数。与 OOP 中的方法相比,你能用它做的事情更多:

  • 把函数传递给其他函数

  • 将新函数作为函数的求值结果返回

  • 将两个函数组合成一个新函数

  • 使用函数的一部分构建一个新函数

函数求值的输出只取决于它的输入。这意味着不存在可以影响函数结果的隐藏变量。这大大提高了可测试性。

数据由代数数据类型 表示。在函数式编程中,你不需要像类那样将数据和代码放在容器中。你将构建一组数据类型和一组单独的函数,这些函数对这些类型进行操作。数据类型不知道它们被哪些函数使用,因为它们对函数一无所知,而且每个函数都不知道还有其他函数也对相同的数据类型进行操作。

下面是 Haskell 中数据类型的一些例子,只是让你感受下它们是如何定义的:

data Bool = True | False
data Customer = Customer Int String
data Customer' = Customer' {
    customerId :: Int,
    customerName :: String
}

总是有一个数据类型名称和一个以|分隔的构造函数名称列表,其中包含可选参数。第一个示例很简单。第二个示例有一个与类型同名的构造函数和两个参数。最后一个示例与前面的示例相同,但是使用了命名参数,这称为记录语法。

Haskell 中的数据是不可变的,这意味着你不能更改 Customer 的姓名,而是需要用新姓名创建一个客户。

小结

假设,你有一个现实世界的问题需要解决。第一步做什么?试着把问题分解成更小的问题,然后再进一步细分下去。然后,描述你的问题,这意味着将你的问题放入你选择的编程语言的俚语中。

在 OOP 的情况下,你需要发现类及其字段和方法,找到相似性,将它们放入抽象类中,并最终通过派生这些抽象类来构建可以供使用的具体类。

FP 则是从函数开始。一个函数处理一个非常小的问题,它操作非常小的类型。在理想情况下,类型完全包含函数所需的信息,不多不少。这可以保证类型和函数几乎不需要更改,即使你完全重构了应用程序的其余部分,除非你的问题发生了变化。事实证明,你还会将你的逻辑类型或业务实体分解为小的技术类型,从而实现无痛且安全的重构。

耦 合

耦合描述组件之间依赖关系以及一个组件的变化对其他组件的影响。

面向对象编程

彼此通信的对象是紧耦合的。限制耦合的一种方法是应用诸如 依赖倒置 之类的原则,即你应该通过抽象(如接口)而不是实现(如类)进行通信。

为我们希望其交换信息的实现定义接口。为了避免出现很大的通用接口,一个接口应该只包含几个高内聚方法——这称为 接口隔离 。从长远来看,如果做得不对,你很可能会遇到虚拟接口实现,比如抛出 UnsupportedOperationException 异常或在空方法体中返回虚拟值。

当涉及到接口实现时,你经常添加抽象类来实现接口的某些部分,未受影响的接口方法仍由具体实现来实现——这就是继承的原理,这是 OOP 中最紧密的耦合。

面向对象和继承的思想是为了使编程更接近现实世界。我们都知道这样的例子:“对于 Car 和 Truck 这两个派生类,有两个基类 Vehicle 和 Ship。可是,Amphibian 怎么处理?”它有两个基类的特征——所以你需要 多重继承 ,但因为钻石问题,这是一个坏主意。为了解决这些问题,开发人员引入了 组合优于继承 的原则,这意味着你应该用可替换的组件组合对象。显然,组合优于继承有点违背 OOP 的原始关键概念之一——继承。

如你所见,一切都关乎正确的类和接口结构——为了设计出一个好的软件设计,还有很多 原则、 反原则 和 模式 需要你关注。

最后但同样重要的是,下面这个简单的例子展示了如何使用依赖倒置原则实现排序算法与比较逻辑的松耦合,该例子使用接口作为抽象:

interface Comparator<T>
{
    int compare(T o1, T o2);
}
class ArcaneComparator<T> implements Comparator<T>
{
    public int compare(T o1, T o2)
    {
        // 在这里插入晦涩难懂的比较实现
    }
}
class Arrays
{
    static <T> void sort(T[] a, Comparator<? super T> c)
    {
        // 使用比较器 c,
        // 不需要了解具体实现
    }
}

函数式编程

FP 是组合组件而不是耦合组件。FP 中的松耦合函数是指通过识别相似性来抽象函数,提取细节,构建高阶函数,并用细节参数化它们。

让我们来看看下面的情况:

sortById :: [Customer] -> [Customer]
sortByName :: [Customer] -> [Customer]

有两个函数做同样的事情——它们按照某些标准进行排序。那么,为什么我们不把相似点放到一个新的函数中来防止重复呢?

data Ordering = LT | EQ | GT
...
sort :: (Customer -> Customer -> Ordering) -> [Customer] -> [Customer]
compareId :: Customer -> Customer -> Ordering
compareName :: Customer -> Customer -> Ordering

或使用一个类型同义词:

type Compare = Customer -> Customer -> Ordering
sort :: Compare -> [Customer] -> [Customer]
compareId :: Compare
compareName :: Compare

sort 的第一个参数是 Customer -> Customer -> ordering 类型的函数,这意味着它接收两个客户,对于小于、等于或大于的情况,分别返回 LT、EQ 或 GT。这有什么不同呢?我们分解出了上述用于对列表进行排序的标准。我们现在可以写成 sort compareId 而不是 sortById。如果你还想叫它 sortById,也很容易做到:

sortById :: [Customer] -> [Customer]
sortById customers = sort compareId customers

或者:

sortById :: [Customer] -> [Customer]
sortById = sort compareId

如果你是最近才接触函数式编程,那么第二个版本在你看来可能有点不够清晰,所以我建议你好好看看第一个版本。如果你对第二种方法感兴趣,你可以进一步阅读,这称为 Eta 变换 。

sort 函数仍然依赖于 Customer 类型,这已经不重要了,因为这些细节被分解了。只有 compare 函数对类型的细节感兴趣。所以我们可以用一个类型变量替换它:

sort :: (a -> a -> Ordering) -> [a] -> [a]

小结

我们可以用任何一种方式表达相同的功能。在 OOP 中,我们使用了一些语言特性,比如接口以及实现该接口的类。在 FP 中,我们有函数。类型 a -> a ->Ordering 表示接口,与该类型匹配的每个函数都可能是该接口的实现。

结束语

在我个人看来,我觉得函数式编程比面向对象编程干净得多。

在编写相同的功能时,你可以:

  • 更抽象

  • 编写更少代码

  • 使用更少的样板特性

而且:

  • 更可维护

  • 更稳定

  • 更有趣

非常感性您耐心地读完这篇文章!

海计划公众号
(0)
上一篇 2020/03/24 05:36
下一篇 2020/03/24 05:36

您可能感兴趣的内容

  • Docker指南教程_一个开源的应用容器引擎

    Docker指南教程 官方网址:https://www.docker.com/ GitHub:# 简介描述:一个开源的应用容器引擎 Docker 是一个开源的应用容器引擎,让开发者…

    2020/03/10
  • 浅谈前端mock基础知识教程_mock菜鸟教程网

    引言前端开发经常需要等待后端的接口,严重影响了开发效率,我们一般采用mock方式来避免这个问题。本人参考了大量文章,结合自己的经验,给出自己在mock上的一些理解。由于作者刚参加工作,水平有限,如果哪里写到不对,请评论指出。1. 原理何为mock,我认为mock主要就是通过正常请求在后端接口进度落后的情况下,还能获取到和后端约定数据结构一样的模拟数据的一门技

    2020/03/31
  • Vue.js动画 基础入门_动画使用攻略

    组件的过渡条件的渲染(使用v-if)条件的展示(使用v-show)动态组件组件根节点链接地址下载:css过渡下面是一个运用css点击显示隐藏显示的2s动画效果: <

    2020/03/24
  • Bodymovin小白帮助_一个After Effects 插件

    Bodymovin小白帮助 官方网址:http://airbnb.io/lottie/ GitHub:https://github.com/airbnb/lottie-web 简介…

    2020/03/10
  • Node学习笔记:优化crud增删改查使用教程_优化小白教程

    本篇文章结合前文《Node学习笔记 Mongodb和Mongoose》对 curd 示例 进行优化MongoDB 安装安装文件下载地址:[https://www.mongodb.com/download-center/community]Windows 平台安装 MongoDB:https://www.runoob.com/mongodb/mongodb-w

    2020/03/20
  • b站flv.js使用基础入门由原生js开发、实现在html5播放flv格式视频的js库_flv.js小白攻略

    Flv.js是什么?Flv.js 就是由 bilibili 网站开源的 HTML5 Flash 视频(FLV)播放器,纯原生 JavaScript 开发(ECMAScript 6 编写) ,没有用到 Flash。它的工作原理是 Flv.js 在 JavaScript 中流式解析 flv 文件流,并实时转封装为 fmp4 ,通过 Media Source Ex

    2020/04/05
  • Canvas接口和动画效果大全小白入门_canvas基础指南

    Canvas接口和动画效果大全小白入门 概述 <canvas>元素用于生成图像。它本身就像一个画布,JavaScript 通过操作它的 API,在上面生成图像。它的底层…

    2020/03/20
  • 免费下载视频、图片素材的网站基础知识教程_网站小白知识

    videezy高清视频下载,免登录,就可以下载,不用翻墙带宽很足; 是一个成立于2006年的平面设计师素材分享站点,平面设计师Shawn发现要想在网络中找一些不错的素材很麻烦,于是就出版了针对平面设计师的素材站点,2010年开始团队运营,现在网站以分享免费的高清视频素材为主,用户可根据许可证免费使用。网站:https://www.videezy.com/p

    2020/04/03
  • 理解Javascript的变量提升小白指南_变量指南攻略

    正文Javascript中的变量提升说的是在程序中可以在变量声明之前就进行使用:console.log(a); // undefined
    var a = 1;可以看到,在变量a声明之前我们可以正常调用a,代码的实际的表现更像是这样的:var a;
    console.log(a); // undefined
    a = 1;但实际上,代码并没有被改变,上面的代码只是

    2020/03/20
  • 前端命令模块及其执行方法入门基础_模块入门知识

    一、创建一个命令模块1、package.json{“name”: “@uad/nat-cli”,”version”: “0.0.2”,”description”: “Demo”,”main”: “index.js”,”bin”: {“artisan”: “./src/artisan.js”},”scripts”: {“test”: “echo \”Error

    2020/03/23
  • layui 获取radio单选框选中的值使用攻略_layui使用指南

    ​layui form 表单获取radio选中的值首先准备两个radio
    <input type="radio" name="sex" value="女" title="女" checked lay-filt

    2020/03/26
  • Js算法:计算两数之和使用指南_算法使用指南

    题目描述:给出两个 非空 的链表用来表示两个非负的整数。其中,它们各自的位数是按照 逆序 的方式存储的,并且它们的每个节点只能存储 一位 数字。如果,我们将这两个数相加起来,则会返回一个新的链表来表示它们的和。您可以假设除了数字 0 之外,这两个数都不会以 0 开头。 示例给定 nums = [2,4,5,6], target = 9
    因为 nums[1]

    2020/03/29
  • ShowFont小白帮助_英文字体下载网站,预览直观

    ShowFont小白知识 官方网址:http://www.showfont.net/ 简介描述:英文字体下载网站,预览直观 fonts,Download free fonts. C…

    2020/03/06
  • 新手学习Web前端的高效学习方法菜鸟教程_学习菜鸟攻略

    作为新手,出于对风险的担心,不免在学习一项新技能或者转投一个新行业的时候,有所犹豫与徘徊。毕竟,在这场类似冒险的选择中,我们需要投入时间、精力以及承受相关的经济损失。但是,只有勇敢迈出第一步,才能为生活注入新活力,面对机遇,我们要及时抓住。就像现在IT行业火热,其中Web前端无论是发展前景还是就业形势都十分可观。那么作为一名新手怎么才能高效学习这门编程语言呢

    2020/03/29
  • node如何更新?使用攻略_node小白攻略

    node的更新方法:先使用npm的命令npm install -g n安装n模块,然后使用n stable可以将node更新到最新版本,使用n+node版本号可以将node更新到指定版本。更新node的方法:一、使用npm 安装一个模块 n 到全局 npm install -g n二、使用 n 安装最新版本:n stable三、使用 n 加版本号就可

    2020/03/20
  • 事务的四大特性和隔离级别入门知识_事务入门指南

    一.什么是事务定义:数据库执行过程中的一个逻辑单位,由一个有限的数据库操作序列构成。本质:由一个或多个sql语句组成。这些sql语句在执行过程中被当作一个整体,要么全部的sql语句执行成功,要么全部失败。不存在一部分执行成功,一部分执行失败。二.数据库中事务的有四大特性(ACID)(1)原子性(Atomic,或称不可分割性)将事务中进行的操作捆绑成一个不可分

    2020/03/26