Scala函数式编程中的Monoid
作者:互联网
在本文中,我们将会从一个简单的需求出发,尝试通过对代码的多次重构,逐步帮你理解什么是Monoid。
基本概念
群(category)的基本概念
群(category)有两个组成要素:
-
由一些同一个类型的对象组成
-
一种态设关系(map),可以将范畴中的任何一个对象转换成另外一个对象,转换之后的类型还是class的。
其示意图如下所示:
另外,一个群还需要满足两个公理:
- 结合律,以加法为例,(a+b)+c = a+(b+c)
- 同一律,群中存在一个特殊的对象e,使得 e * y = y = y * e,这个对象e被称为幺元。
半群的概念
一个不满足同一律且不存在一种态设关系的群就叫做一个半群,即semigroup。
幺半群的概念
如果在半群定义的基础上增加一个幺元,那么这个半群就叫做幺半群,即Monoid。
通过代码来理解上面两个概念
需求:输入一串数字,输出其累加和累乘的结果。例如,输入为1,2,3,4,5这五个数字,其累加的结果为1+2+3+4+5=15,其累乘的结果为1 * 2 * 3 * 4 * 5=120,
版本1
版本1将通过scala集合类中提供的api来完成这个需求
object Version1 extends App {
//定义一个累加的方法
def sum(list: List[Int]): Int = {
list.foldLeft(0)((res, a) => res + a)
}
//定义一个累乘的方法
def multi(list: List[Int]): Int = {
list.foldLeft(1)((res, a) => res * a)
}
println(sum(List(1, 2, 3, 4, 5)))
println(multi(List(1, 2, 3, 4, 5)))
}
如上述代码所示,我们分别定义了两个方法来实现我们的需求,分别是sum
和multi
,但是这个版本的代码的问题在于其扩展性很差,目前只支持Int类型的累加和累乘,无法实现Double或其他类型的累加和累乘。所以我们需要使用函数式编程中的半群(Semigroup)和幺半群(Monoid)来对其进行改造。我们这里不会使用Cats
库中提供的Semigroup
和Monoid
,而是通过尝试自己实现一个简单的Semigroup
和Monoid
,来达到理解半群和幺半群的目的。
版本2
通过上述章节中对半群和幺半群的定义的介绍,我们可以很轻松的将其转换成scala代码。
半群的代码如下所示,通过类型参数A
可以实现由一组对象组成
这个要素,通过combine
方法可以实现半群中的结合律:
trait Semigroup[A] {
def combine(x: A, y: A): A
}
根据定义,幺半群比半群多了一个幺元,所以幺半群的代码如下:
trait Monoid[A] extends Semigroup[A]{
def zero:A
}
使用代码定义了Semigroup
和Monoid
后,我们将尝试通过幺半群的概念来重构上述代码,使其更加的FP
。
通过观察版本1的代码,累加
和累乘
两个函数的定义非常的相似,他们的本质都是对集合中的元素进行迭代,并根据某种算法(加或者乘)求其结果。结合幺半群的定义我们可以发现,foldLeft函数的第一个参数就是幺元,定义了某个算法的起始值,第二个参数定义了如何将两个对象进行合并
。
经过上面的分析,我们可以编写一个命名为fold
的函数对sum
和multi
函数进行抽象,fold
函数的的定义如下:
def fold(list: List[Int], m: Monoid[Int]): Int = {
list.foldLeft(m.zero)(m.combine)
}
这个版本的代码引入了我们自定义的幺半群Monoid,幺元zero为结合
算法的起始值。
为了使用这个版本的代码,我们需要为Monoid提供两种实现分别用于做累加
和累乘
:
//加法幺半群的实现
val addMonoid: Monoid[Int] = new Monoid[Int] {
override def zero: Int = 0//0在加法中可以满足同一律
override def combine(x: Int, y: Int): Int = x + y
}
//乘法幺半群的实现
val multiMonoid: Monoid[Int] = new Monoid[Int] {
override def zero: Int = 1//1在加法中可以满足同一律
override def combine(x: Int, y: Int): Int = x * y
}
最终我们可以通过调用一个fold函数完成累加
和累乘
的需求:
fold(List(1, 2, 3, 4, 5), addMonoid)
fold(List(1, 2, 3, 4, 5), multiMonoid)
版本3
对版本2中fold函数的定义进行观察可以发现,这个版本的fold函数只能处理类型为Int的一群对象,它并不能胜任类型为Double
或String
的一群对象,所以我们需要引入类型参数对其再次进行优化,优化后的代码如下:
def fold[A](list: List[A], m: Monoid[A]): A = {
list.foldLeft(m.zero)(m.combine)
}
这时的fold
方法可以处理任何类型的一群对象了,假如我现在需要对一组字符串进行拼接,那么我的代码应该是这个样子的:
首先实现一个字符串类型的Monoid:
val stringAppendMonoid: Monoid[String] = new Monoid[String] {
override def zero: String = ""//在字符串中幺元是""
override def combine(x: String, y: String): String = x concat y//定义字符串的结合算法为拼接
}
通过调用fold方法完成对List("he", "llo", ",wor", "ld")
集合中的字符串进行拼接:
fold(List("he", "llo", ",wor", "ld!"), stringAppendMonoid)
其输出的结果为:
hello,world!
版本4
通过观察版本3
中的代码,我们发现fold方法参数中存在一个参数list: List[A]
,因为在scala中不只List类型具有foldleft
方法,所以我们希望能够对List类型也进行一次抽象,使用一个类型参数F将其替代,所以这时的代码应该是这个样子的:
//这个代码会编译报错
def fold[F[_], A](list: F[A])(m: Monoid[A]): A = {
list.foldleft(list)(m.zero)(m.combine)
}
并不是每一个传进来的F类型都可以拥有foldleft
方法,所以我们需要对这个F类型进行一定的限制,我们希望只有具备了foldleft能力的F才能被传入。所以我们创建一个特质Foldable
,其定义如下:
trait Foldable[F[_]] {
def foldleft[A](fa: F[A])(zero: A)(f: (A, A) => A): A
}
Foldable
特质定义了一个foldleft方法,标识参数类型F应该是一个具备foldleft
能力的类型,当然你也可以在这里定义其他的方法,这里我们仅定义一个foldleft
方法来完成我们的需求。
定义完Foldable
特质后,接下来可以对上面的fold
方法进行改造,使其能够被编译,改造后的代码如下:
def fol1[F[_], A](list: F[A])(m: Monoid[A])(implicit f: Foldable[F]): A = {
f.foldleft(list)(m.zero)(m.combine)
}
这里通过使用 Context Bound来简化代码的写法,优化后的代码如下:
//注意 F[_]: Foldable
def fold[F[_]: Foldable, A](list: F[A])(m: Monoid[A]): A = {
implicitly[Foldable[F]].foldleft(list)(m.zero)(m.combine)
}
为了能使用了这个版本的代码,我们需要定义一个Foldable的隐式转换和一个累加的Monoid实例:
implicit val listFoldable: Foldable[List] = new Foldable[List] {
override def foldleft[A](fa: List[A])(zero: A)(f: (A, A) => A): A =
fa.foldLeft(zero)(f)
}
累加的Monoid实例
private val addMonoid: Monoid[Int] = new Monoid[Int] {
override def zero: Int = 0
override def combine(x: Int, y: Int): Int = x + y
}
最后通过调用fold方法即可完成需求
版本5
与版本4类似,我们也可以使用Context Bound语法糖来将类型A进行优化,优化后的代码如下:
def fold[F[_]: Foldable, A: Monoid](list: F[A]): A = {
//通过implicitly方法查找当前作用域中对应的隐式转换
val m = implicitly[Monoid[A]]
val f = implicitly[Foldable[F]]
f.foldleft(list)(m.zero)(m.combine)
}
版本6
在版本5的基础上使用隐式转换参数
再次对其进行优化,优化后的代码如下:
def fold[F[_], A](as: F[A])(implicit evf: Foldable[F], eva: Monoid[A]): A = {
evf.foldleft(as)(eva.zero)(eva.combine)
}
版本5
和版本6
的代码的优势在于,我可以通过提供一个单例的类,来集中管理所有的隐式转换定义。用户只需要导入对应的类,即可拥有相应的能力。
以版本6
的实现为例,为了使用fold函数,我需要定义一个名为MonoidInstance
单例和一个名为FoldableInstance
单例,用于集中管理Monoid
的隐式转换和Foldable
的隐式转换,其代码分别如下:
//用于集中管理Monoid的隐式转换
object MonoidInstance {
implicit val addMonoid: Monoid[Int] = new Monoid[Int] {
override def zero: Int = 0
override def combine(x: Int, y: Int): Int = x + y
}
}
//用于集中管理Foldable的隐式转换
object FoldableInstance {
implicit val listFoldable: Foldable[List] = new Foldable[List] {
override def foldleft[A](fa: List[A])(zero: A)(f: (A, A) => A): A =
fa.foldLeft(zero)(f)
}
}
定义完这两个单例类后,当我们需要使用版本6
代码的时候,只需要导入这两个类即可,其测试代码如下(请注意上面的import语句):
import category._
import MonoidInstance._
import FoldableInstance._
object Version6 extends App {
def fold[F[_], A](as: F[A])(implicit evf: Foldable[F], eva: Monoid[A]): A = {
evf.foldleft(as)(eva.zero)(eva.combine)
}
println(fold(List(1, 2, 3, 4, 5)))
}
版本6中的写法已经和Cats库中的写法非常的相似了。
标签:fold,Scala,Monoid,编程,Int,zero,半群,def 来源: https://blog.csdn.net/qq_35835624/article/details/113453010