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)))
}

如上述代码所示,我们分别定义了两个方法来实现我们的需求,分别是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库中的写法非常的相似了。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
 这本书绝不轻易放过每个知识点,全书包含有大量习题,要求你自己实现 Scala 标准库或者 Scalaz 的既有功能。所以,当你读完本书,做完习题后,虽然你的应用开发能力并不会直接提升,但你会体会到构建函数式语言和框架时的难点和取舍,从而增进你的框架开发和语言设计的能力。   ——ThoughtWorks Lead Consultant 杨博   这本书所讲授的,正是基于 Scala函数式编程基础。基于 Scheme、Haskell 等老牌函数式语言的传统教材的问题在于,相关语言的语法和思维方式与读者现有的知识体系迥异,容易造成较为陡峭的入门门槛。此外,由于这些语言本身的实际应用机会不多,初学者也难以在实战获得宝贵的直觉和经验。而在 Scala 的帮助下,这本书并不要求你抛开现有的思维方式另起炉灶,它所做的更像是为你现有的思维方式添砖加瓦,从而令你如虎添翼。   ——Spark committer from Databricks 连城   尽管函数式编程在近十多年用得越来越多,但市面上介绍其高阶特性的书却并不多。这本书在这方面是个重要的补充,它不仅仅面向 Scala 程序员,同样面向用任何编程语言开发的程序员,只要你充满好奇心。   ——挖财网首席架构师 王宏江   “让你洞察计算的本质。”   ——Martin Odersky, Scala的作者   “Scala和Java8开发者的函数式编程指南!”   ——William E. Wheeler, TekSystems   “本书向你展示了提升Scala技能的方法和理念,它已超过‘更好的Java’。”   ——Fernando Dobladez, Code54   “里面的练习有些挑战,很有趣,对你在真实世界使用它很有益。”   ——Chris Nauroth, Hortonworks   “边干边学,而非只是阅读。”   ——Douglas Alan、Eli和Edythe L. Broad,哈佛和麻省理工学院

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值