编程语言
首页 > 编程语言> > Scala函数式编程中的Monoid

Scala函数式编程中的Monoid

作者:互联网

在本文中,我们将会从一个简单的需求出发,尝试通过对代码的多次重构,逐步帮你理解什么是Monoid。

基本概念

群(category)的基本概念

群(category)有两个组成要素:

其示意图如下所示:
在这里插入图片描述

另外,一个群还需要满足两个公理:

半群的概念

一个不满足同一律不存在一种态设关系的群就叫做一个半群,即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)))
}

如上述代码所示,我们分别定义了两个方法来实现我们的需求,分别是summulti,但是这个版本的代码的问题在于其扩展性很差,目前只支持Int类型的累加和累乘,无法实现Double或其他类型的累加和累乘。所以我们需要使用函数式编程中的半群(Semigroup)和幺半群(Monoid)来对其进行改造。我们这里不会使用Cats库中提供的SemigroupMonoid,而是通过尝试自己实现一个简单的SemigroupMonoid,来达到理解半群和幺半群的目的。

版本2

通过上述章节中对半群和幺半群的定义的介绍,我们可以很轻松的将其转换成scala代码。

半群的代码如下所示,通过类型参数A可以实现由一组对象组成这个要素,通过combine方法可以实现半群中的结合律:

trait Semigroup[A] {
  def combine(x: A, y: A): A
}

根据定义,幺半群比半群多了一个幺元,所以幺半群的代码如下:

trait Monoid[A] extends Semigroup[A]{
	def zero:A
}

使用代码定义了SemigroupMonoid后,我们将尝试通过幺半群的概念来重构上述代码,使其更加的FP

通过观察版本1的代码,累加累乘两个函数的定义非常的相似,他们的本质都是对集合中的元素进行迭代,并根据某种算法(加或者乘)求其结果。结合幺半群的定义我们可以发现,foldLeft函数的第一个参数就是幺元,定义了某个算法的起始值,第二个参数定义了如何将两个对象进行合并

在这里插入图片描述

经过上面的分析,我们可以编写一个命名为fold的函数对summulti函数进行抽象,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的一群对象,它并不能胜任类型为DoubleString的一群对象,所以我们需要引入类型参数对其再次进行优化,优化后的代码如下:

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