You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
这里的 Functor 并非 C++ 中的仿函数,而是范畴论当中的术语,称为函子。范畴论中,我们的输入数据和输出数据就称为范畴,Functor 是将一个范畴转换为另一个范畴的方式,它是比 Function 更高阶的函数。代码中 Functor 就提供了所谓的上下文,能够将 A 作为输入,B 作为输出,因为输入输出都是 Functor,所以就具备了组合多个函数的作为。
若是输入类型与输出类型相同,即源范畴与目的范畴相同,就称为幺元,此时函子就称为自函子(endofunctors),它是 end of functors 的组合词。幺元构成了一个幺半群,就是概念中所说的 Monoid,我们的类型只要满足这些约束,就称为一个 Monad。代码中的输入输出类型都是 Functor,所以精确点说它是 endofunctors,也是 Monad。
Monads in Modern C++, What, Why, and How
https://ift.tt/zmgAUIW
Li Miu
今天这篇讲 Monads,对 C++ devs 来说是一个比较新的概念,也是一个比较难理解的概念。本文将概念和实践串起来讲,先讲抽象理论,后面讲具体实现和使用,以全面理解其定位。
Language
编程语言用于操作机器以辅助人类解决问题,主体是人和机器,目的是解决问题。人通过机器解决问题,编程语言是人和机器之间沟通的桥梁。
问题解决最基本的流程为定义问题、分析问题、寻找策略及选择策略。简单来说,你有一些东西,想通过一些行为,再得到一些东西。问题就是理想和现实之间的差距。
这其中最基本的构成要素就是东西和行为,也就是编程语言当中的数据和函数。一个大问题不可能通过一个行为就能解决,往往需要拆分为一个个小问题,再逐个击破,故根据问题复杂程度,数据和函数的数量也会随之变化。
主体虽分为人和机器,但却是由机器来完成具体工作。最初的语言,靠机器更近,尽管效率高,人和机器沟通起来却是多有不便;随后的语言,便靠人更近,使人和机器的沟通方式,更加接近人与人之间的沟通,这才简化了编程难度。
常言道,月满则亏,水满则溢,凡事过犹不及。太靠近机器的语言,尽管效率高,但却难以理解;太靠近人的语言,虽说易理解,效率却颇低。因此,只有在明确仅注重效率或简易的情况下,才会使用这类语言,许多语言其实都处于中间,保持效率的同时也避免太过复杂。
Imperative versus Declarative
由机器到人,编程语言是把机器所能识别的具体指令,抽象为人所能理解的词句。
声明式语言相比命令式语言,离人更近,命令式更关注具体,声明式更关注抽象。抽象是把不变的都封装起来,从而避免去写一个个重复的指令,比如原本想要修改
std::vector
的元素,你需要使用if
、for
等等指令,这些再往上抽象一层就是过滤和变换两个步骤,直接使用v | filter | transform
则更加言简意赅。因此,声明式用起来要更加简洁,使人更加专注于问题解决步骤。具体中包含了变与不变,抽象中提取了不变的部分。具体就像是白话文,抽象就像是文言文,前者通俗易懂,后者简洁精炼。命令式让人更加专注于行为的细枝末节,事无具细,一切尽在掌握之中;声明式让人更加关注于行为步骤,抽离细节,一切环环相扣。
也可以说命令式注重问题解决过程,而声明式注重问题解决结果。不关心过程,只想要结果,展现在语言上就是只调用接口,不关心实现。因此,从视角上来看,命令式更偏向于微观,声明式更偏向于宏观。宏观适合思考问题,微观适合解决问题,前者像是将军,只谋战略,后者则像是小兵,具体实施。
将无兵而不行,兵无将而不动。抽象与具体当中,虽然真正起作用的是具体的指令,但使用的却是抽象的接口。接口使来结构清晰,能更加专注于问题逻辑,跳出问题细节。抽象与具体紧密连接,只要抽象,抽象从何来?只要具体,具体何以用?
是以范式不同,本质是解决问题思维的不同,是侧重点的不同。
What is Monads
C++ 有许多范式,面向过程、面向对象、泛型编程、函数式编程…… 许多人发现 Modern C++ 写来越发简单,这种简单写法就谓之 Modern,但这种简洁从何而来?本质就是范式的转变。
C++11 至今,C++ 已逐步从函数式编程中汲取了许多特性,Monads 就是其中非常重要的一个特性。C++23 中也加入不少 Monads,简化了一些原本较为繁琐的操作。
那么,什么是 Monads?
这个概念来自范畴论,称为单子,牵扯概念颇多,且不易理解,因此我们结合 C++ 来讲。 函数式编程当中使用 Monads 表示一个抽象数据类型,其目的在于表示计算过程。
既然是函数式编程的概念,它自然属于声明式,所以通过 Monads 能够减少重复代码,抽象处理逻辑,简化组合流程。
为了精确,这里还是提供一下 Monads 的定义:
用一个公式说明一下:
(a→mb)(a \rarr m b)
表示行为,aa
是输入类型,bb
是输出类型。行为必须满足某种上下文,才能知道怎样处理,mm
就指定了这个上下文。如果将其替换为代码,会更加容易理解:因此,其实说的就是有一个输入
aa
,通过一个函数,得到一个输出bb
。mm
表示的就是 Monad,aa
和bb
前面都有mm
,表示它们必须处于一定的上下文才能调用这个函数。我们为它提供一下上下文,变为:
这里的 Functor 并非 C++ 中的仿函数,而是范畴论当中的术语,称为函子。范畴论中,我们的输入数据和输出数据就称为范畴,Functor 是将一个范畴转换为另一个范畴的方式,它是比 Function 更高阶的函数。代码中 Functor 就提供了所谓的上下文,能够将 A 作为输入,B 作为输出,因为输入输出都是 Functor,所以就具备了组合多个函数的作为。
若是输入类型与输出类型相同,即源范畴与目的范畴相同,就称为幺元,此时函子就称为自函子(endofunctors),它是 end of functors 的组合词。幺元构成了一个幺半群,就是概念中所说的 Monoid,我们的类型只要满足这些约束,就称为一个 Monad。代码中的输入输出类型都是 Functor,所以精确点说它是 endofunctors,也是 Monad。
若类型只有
transform()
这一个函数,就称为一个 Functor,表示将一个范畴转换为另一个范畴。若输入输出类型还相同,就称为 endofunctor,但一般还是称为 Functor,要注意此时实际上指的是 endofunctor,也是 Monads。但因为 Functor 只有transform()
这一个函数,所以它只会进行转换操作,输入元素和输出元素的大小是相同的,而 Monad 除transform()
外,还有一些其他函数,比如过滤函数,此时输入元素和输出元素的大小可以是不同的。因此,需要分清二者,否则极易混淆 Functor 和 Monads。为什么要保证输入输出类型一致呢?范畴的基本本质是组合,只要类型相同,就能够将许多行为串起来,比如
type.transform(f).transform(g).transform(h)
,类型不同就缺失了上下文,无法组合函数。Monads in C++
C++ 中通常是将 Functor 定义成类,
transform()
作为成员出现。只有
transform()
这一个成员的时候,一般称之为 Functor;若是再增加一些额外操作,一般称之为 Monad。首先,函数名称不是随便起的,一般一个语言里面都有一些共识,比如 C++ 中使用的是
transform
和and_then
,Haskell 中则使用的是map
和fmap
。其次,为什么需要一些额外的操作呢?
看实际使用:
借由
Monad
,我们得以组合多个函数,这是正常情况。如果所要组合的函数返回类型本身就是Monad
,此时就会发生错误。发生了什么?
stoi
的返回类型由int
变为了Monad<int>
,于是第一次调用transform()
的返回类型变成了Monad<Monad<int>>
;第二次调用transform()
时,doubled()
传入的参数类型为Monad<int>
,与实际类型int
并不匹配,产生错误。此时就需要一个额外的工具来解决这个问题,每次都将
Monad<Monad<T>>
变为Monad<T>
,这个函数一般称为join()
。实现为:但是这样使用起来有点麻烦,打破了函数组合的连贯性,一般来讲要再封装一层,直接把
transform()
和join()
这两个调用合并成一个步骤,一般称为 Monadic bind 或是 mbind。在 C++ 中,被命名为and_then()
。下面是完整实现:在
and_then()
之中,除了解决调用链连贯性问题,还可以增加一些额外的检查,比如值是否有效,有效则执行调用后将Monad<Monad<T>>
变为Monad<T>
,无效则不执行本次调用,直接返回一个空的Monad
。因此,and_then()
这种命名表面上表达了检查再执行的意思,实际上主要目的还是在于调用的连贯性。也因此,
and_then()
所调用的函数必须返回一个Monad<T>
,这样实际调用的才是Monad<Monad<T>>
,否则函数调用不匹配。最后,这就是 C++ 中所要掌握的 Monads 实现技术,更多深入内容以后再扩展。
Monads, Monads, Monads Everywhere
在大家都没意识到时,Monads 已在 Modern C++ 中遍地开花。本节看 C++ 中的 Monads 类型。
C++23 Monadic std::optional
首先是
std::optional
,看看 C++23 新加的三个成员:transform()
and_then()
or_else()
想必也无需我再多加介绍,这里的
or_else()
是额外添加的另一个 Monadic 函数,用于处理错误,如果std::optional
为空,能够调用传入的函数处理,若非空,则什么也不做。std::optional
类型对应 Haskell 中的Maybe
类型。来对比一下 Monadic
std::optional
前后的操作。可以看到,Monadic
std::optional
消除了大量重复的检查代码,让我们直接专注于程序逻辑,而不必再被那些次要的细枝末节分散注意力。C++23 Monadic std::expected
std::expected
也是一个 Monad,它是std::optional
和std::variant
的结合体,接口和std::optional
非常相似。使用 Monadic operations 的一个例子:
例子中先读取数据,然后将 "cat-dog-duck" 转换为 "cat,dog,duck",期间通过
or_else()
处理错误,通过and_then()
完成实际转换,通过transform()
完成最终输出。如果你的函数不想返回任何值,那么只能调用
transform()
,and_then()
必须返回std::expected
类型。List Monads
Monads 有多种类型,像前面使用的
std::optional
用于处理可选值,std::expected
用于处理错误,还有一种广泛使用的是 List Monads。List Monads 就是 C++20 Ranges 为容器所带来的 Monadic 操作,它一般配备有 pipe,函数组合起来易读性更好。
一个例子:
以前编写类似的逻辑,需要不断遍历、判断,编写非常多的细节,借助 List Monads,只需关注算法步骤,将它们一个一个组合起来,一目了然。
其中,
transform()
和join()
已经是大家非常熟悉的 Monadic 操作,其他的是一些额外操作,根据 Monads 类型,也可以自行添加。借助 C++23ranges::to
,可以非常容易地将 Views 转换成任意容器,操作起来更加顺畅。Conclusion
关于 C++ Monads 的文章很少,它是属于函数式编程的概念,这方面的特性也都很新。本文从底层思维着手,讲解了 Modern C++ 近些年特性的原理,利于深入理解和运用这方面的特性。
同时,在可以观望的未来,C++26/29 还会再引入一些新的 Monads,已经存在不少相关提案,异步代码中也将充斥着大量 Monads。
Monads 并不容易讲解,何况是 C++ 中的 Monads。本文由语言及手,牵出命令式和声明式,再引出 Monads 的概念,次再由 C++ 代码实现一个基本的 Monads,以具象化这些抽象概念,降低理解难度,最后再展示标准中的 Monads,进一步加深理解,也使大家对标准中的这些相关特性有更加深入的认识。
如果读到这里,你还会产生“这不就是返回一个自身对象的引用嘛,我们早就用过”这种感想,那你只不过是在用旧概念理解新思想,相同的只是实现手段,不同的地方才是核心,理解不同的地方才算真正搞懂新概念。
更多内容,请见后续更新。
via CppMore https://www.cppmore.com
February 20, 2025 at 02:50PM
The text was updated successfully, but these errors were encountered: