百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 编程网 > 正文

为什么我更喜欢函数式编程

yuyutoo 2024-12-01 06:08 3 浏览 0 评论

在学习 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
{1}

下面是一个没有使用递归的简化版本,使用了 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 表示接口,与该类型匹配的每个函数都可能是该接口的实现。

未讨论的话题

  • Monads——因为我不想陷入 monad 谬误
  • 类型类——没有理由

一些事实

  • Haskell 是有类型 lambda 演算的实现。Haskell 中的所有东西都有数学支持,比如范畴和类型理论。
  • Haskell 是强静态类型,但你很少需要自己编写类型,因为编译器在大多数情况下可以从全局推断它的类型。
  • 因为抽象性和不可变性,Haskell 的速度比你想的要快。

结束语

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

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

  • 更抽象
  • 编写更少代码
  • 使用更少的样板特性

而且:

  • 更可维护
  • 更稳定
  • 更有趣

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

相关推荐

一篇文章认识JavaScript中的Web API

在了解webapi之前先要明白在我们声明变量三个关键字(var、let、const),我们到底该用哪一个?首先排除var,它是老牌的写法,会有很多问题,可以淘汰掉...我们在开发中建议const...

html5大神结合js带你研究古老读心术,你的心思早被猜透

javascript/HTML5课题:javascript开发读心术游戏PS:大爆料!javascript解密读心术游戏背后故事知识点:读心术原理算法独家揭秘,HTML5最新选择器,...

纯CSS实现3个圆圈横向排列不断闪烁的Loading特效

这个纯CSS实现的Loading特效是一个非常简单而实用的选择,它由三个圆圈横向排列,并不断闪烁。这种动画效果很适合用于页面加载过程中,为用户提供一个愉悦的等待体验。在这个特效的设计中,使用了CSS的...

网站建设知识分享系列文章三:符合用户体验的网页设计应如何做?

上篇文章向各位讲述了网站建设知识分享系列文章二:如何选择建站公司,今天我们来讲述下建站公司选定后,网站制作流程是怎样的,有哪些需要注意的细节性问题。选定建站公司,签订正规劳务合同后,最先开始的是设计环...

「更新」微信小程序 Lottie 动画组件 lottiejs-miniapp V1.1.0 发布

lottiejs-miniapp当前版本号:1.1.0npm地址:...

Web开发基础之jQuery javascript web开发

jQuery是一个JavaScript库。jQuery极大地简化了JavaScript编程。jQuery的语法设计可以使开发更加便捷,例如操作文档对象、选择DOM元素、制作动画效果、事件处理、使用Aj...

Web前端:JavaScript最强总结,最全面的零基础入门教程

JavaScript是网景(Netscape)公司开发的一种基于客户端浏览器、面向(基于)对象、事件驱动式的网页脚本语言。JavaScript语言的前身叫作Livescript。JavaScript...

jQuery 动画制作与特效 jquery的动画函数

使用show()和hide()方法在普通的javascript编程中,要实现元素的显示、隐藏通常是利用其CSS的display属性或者visibility属性。在jQuery中提供了show()和hi...

JavaScript+css实现的登录注册页面web前端html源码

大家好,今天给大家介绍一款,JavaScript+css实现的登录注册页面web前端html源码(图1),布局合理。送给大家哦,获取方式在本文末尾。文本框获取焦点动画特效(图2)源码完整,需要的朋友可...

CSS 3.0+HTML5.0制作各种网页特效

?1、C33实现点击图片渐渐放大特效??2、CSS3实现图片全屏背景特效?3、CSS3实现的鼠标移动到图片上不规则放大??3、jQuery+CSS3模拟苹果桌面系统??4、CSS3+jQuery照片...

js+css实现的按钮悬停动画特效html前端源码,随机元素弹出效果

大家好,今天给大家介绍一款,js+css实现的按钮悬停动画特效html页面前端源码,随机元素弹出(图1)。送给大家哦,获取方式在本文末尾。鼠标经过按钮区域的时候,会随机从不同位置上弹出很多小元素,效果...

Swiper - 免费开源、功能强大的触摸滑动js特效插件

简单配置就能实现手机、PC网页中滑动、焦点轮播图、tab切换和触摸导航等大部分功能。js滑动特效插件Swiper是一款纯javascript打造的滑动特效插件,主要用对移动端web开发...

html5精选特效代码分享(收藏) html酷炫特效

在网页设计过程中,我们会经常用到一些HTML5特效代码,下面就是为大家整理分享的一些好看炫酷且实用的HTML5特效代码,可以放心在您的应用程序中使用。一、Canvas跟随鼠标光标动画特效演示、下载地址...

玩转Markdown(2)——抽象语法树的提取与操纵

上一篇玩转Markdown——数据的分离存储与组件的原生渲染发布,转眼已经鸽了大半年了。最近在操纵mdast生成md文件的时候,心血来潮,把玩转Markdown(2)给补上了。...

任由文字肆意流淌,更自由的开源 Markdown 编辑器

对于创作平台来说内容编辑器是十分重要的功能,强大的编辑器可以让创作者专注于创作“笔”下生花。而最好取悦程序员创作者的方法之一就是支持Markdown写作,因为大多数程序员都是用Markdown...

取消回复欢迎 发表评论: