Scalaz(3)- 基础篇:函数概括化-Generalizing Functions

  Scalaz是个通用的函数式编程组件库。它提供的类型、函数组件都必须具有高度的概括性才能同时支持不同数据类型的操作。可以说,scalaz提供了一整套所有编程人员都需要的具有高度概括性的通用函数,它是通过随意多态(ad-hoc polymorphism)来帮助用户使用这些函数的。随意多态就是trait+implicit parameters+implicit conversions。简单的说就是scalaz提供一个概括化的函数,用户可以在各种类型上施用这个同一函数。概括化(generalizing)函数最基本的技巧应该是类型参数变量(parametric type variable)的使用了。如下:

1 def head[T](xs: List[T]): T = xs(0)               //> head: [T](xs: List[T])T
2 head(List(1,2,3,4))                               //> res0: Int = 1
3 head(List("a","b","c"))                           //> res1: String = a
4 case class Car(manu: String)
5 head(Car("Honda") :: Car("Toyota") :: Nil)        //> res2: scalaz.learn.ex2.Car = Car(Honda)

无论T是任何类型,Int, String, Car,都可以使用这个head函数。

但作为一个标准库的开发者,除了使用类型变量去概括函数外还必须考虑函数的使用方式以及组件库的组织结构。这篇讨论里我们将从一个组件库开发者的角度来思考、体验如何设计概括化的通用函数。

我们试着从概括化一个函数sum的过程中了解scalaz的设计思路:

针对List,Int的函数:sum(xs: List[Int]): Int

概括化后变成:sum[M[_],A](xs: M[A]): A

针对Int的sum如下:

def sum(xs: List[Int]): Int = xs.foldLeft(0){_ + _}
                                                  //> sum: (xs: List[Int])Int
sum(List(1,2,3))                                  //> res0: Int = 6
//你不能这样:sum(List(1.0,2.0,3.0)

我们看到:sum只能接受一个List[Int],其它类型过不了编译。

我们先看看这个foldLeft: 它需要一个起始值(在这里是Int 0)和一个两个值的操作(在这里是两个Int的加法)。我们可以先把这部分抽象一下:

1 object intMonoid {
2     def mappend(i1: Int, i2: Int): Int = i1 + i2
3     def mzero = 0
4 }
5 def sum(xs: List[Int]): Int = xs.foldLeft(intMonoid.mzero)(intMonoid.mappend)
6                                                   //> sum: (xs: List[Int])Int
7 sum(List(1,2,3))                                  //> res0: Int = 6

我们把这个intMonoid抽了出来。那么现在的sum已经具有了一些概括性了,因为foldLeft的具体操作依赖于我们如何定义intMonoid。

如果我们对String进行sum操作的话我可以这样:

1 object stringMonoid {
2     def mappend(s1: String, s2: String): String = s1 + s2
3     def mzero = ""
4 }
5 def sum(xs: List[String]): String = xs.foldLeft(stringMonoid.mzero)(stringMonoid.mappend)
6                                                   //> sum: (xs: List[String])String
7 sum(List("Hello,"," how are you"))                //> res0: String = Hello, how are you

按这样推敲,我们可以对任何类型A进行sum操作,只要用一个类型参数的trait就行了:

1 trait Monoid[A] {
2     def mappend(a1: A, a2: A): A
3     def mzero: A
4 }

注意具体的操作mappend和起始值都没有定义,这个会留待trait Monoid各种类型的实例里:

 1 trait Monoid[A] {
 2     def mappend(a1: A, a2: A): A
 3     def mzero: A
 4 }
 5 object intMonoid extends Monoid[Int]{
 6     def mappend(i1: Int, i2: Int): Int = i1 + i2
 7     def mzero = 0
 8 }
 9 object stringMonoid extends Monoid[String]{
10     def mappend(s1: String, s2: String): String = s1 + s2
11     def mzero = ""
12 }

现在我们可以这样改变sum:

1 def sum[A](xs: List[A])(m: Monoid[A]): A = xs.foldLeft(m.mzero)(m.mappend)
2                                                   //> sum: [A](xs: List[A])(m: scalaz.learn.ex2.Monoid[A])A
3 sum(List(1,2,3))(intMonoid)                       //> res0: Int = 6
4 sum(List("Hello,"," how are you"))(stringMonoid)  //> res1: String = Hello, how are you

现在这个sum是不是概括的多了。现在我们可以利用implicit使sum的调用表达更精炼:

 1 trait Monoid[A] {
 2     def mappend(a1: A, a2: A): A
 3     def mzero: A
 4 }
 5 implicit object intMonoid extends Monoid[Int]{
 6     def mappend(i1: Int, i2: Int): Int = i1 + i2
 7     def mzero = 0
 8 }
 9 implicit object stringMonoid extends Monoid[String]{
10     def mappend(s1: String, s2: String): String = s1 + s2
11     def mzero = ""
12 }
13 def sum[A](xs: List[A])(implicit m: Monoid[A]): A = xs.foldLeft(m.mzero)(m.mappend)
14                                                   //> sum: [A](xs: List[A])(implicit m: scalaz.learn.ex2.Monoid[A])A
15 sum(List(1,2,3))                                  //> res0: Int = 6
16 sum(List("Hello,"," how are you"))                //> res1: String = Hello, how are you

现在调用sum是不是贴切多了?按照scalaz的惯例,我们把implicit放到trait的companion object里:

 1 trait Monoid[A] {
 2     def mappend(a1: A, a2: A): A
 3     def mzero: A
 4 }
 5 object Monoid {
 6  implicit object intMonoid extends Monoid[Int]{
 7      def mappend(i1: Int, i2: Int): Int = i1 + i2
 8      def mzero = 0
 9  }
10  implicit object stringMonoid extends Monoid[String]{
11      def mappend(s1: String, s2: String): String = s1 + s2
12      def mzero = ""
13  }
14 }

这样,用户可以定义自己的Monoid实例在sum中使用。

但现在这个sum还是针对List的。我们必须再进一步概括到任何M[_]。我们先把用一个针对List的foldLeft实例来实现sum:

1 object listFoldLeft {
2     def foldLeft[A,B](xs: List[A])(b: B)(f:(B,A) => B):B = xs.foldLeft(b)(f)
3 }
4 def sum[A](xs: List[A])(implicit m: Monoid[A]): A = listFoldLeft.foldLeft(xs)(m.mzero)(m.mappend)
5                                                   //> sum: [A](xs: List[A])(implicit m: scalaz.learn.ex2.Monoid[A])A

我们可以像上面对待Monoid一样用个trait来概括M[_]:

 1 trait FoldLeft[M[_]] {
 2     def foldLeft[A,B](xs: M[A])(b: B)(f: (B,A) => B): B
 3 }
 4 object FoldLeft {
 5  implicit object listFoldLeft extends FoldLeft[List] {
 6     def foldLeft[A,B](xs: List[A])(b: B)(f:(B,A) => B):B = xs.foldLeft(b)(f)
 7  }
 8 }
 9 def sum[M[_],A](xs: M[A])(implicit m: Monoid[A], fl: FoldLeft[M]): A =
10    fl.foldLeft(xs)(m.mzero)(m.mappend)            //> sum: [M[_], A, B](xs: M[A])(implicit m: scalaz.learn.ex2.Monoid[A], implicit
11                                                   //|  fl: scalaz.learn.ex2.FoldLeft[M])A
12 sum(List(1,2,3))                                  //> res0: Int = 6
13 sum(List("Hello,"," how are you"))                //> res1: String = Hello, how are you

现在这个sum[M[_],A]是个全面概括的函数了。上面的sum也可以这样表达: 

1 def sum1[A: Monoid, M[_]: FoldLeft](xs: M[A]): A = {
2     val m = implicitly[Monoid[A]]
3     val fl = implicitly[FoldLeft[M]]
4     fl.foldLeft(xs)(m.mzero)(m.mappend)
5 }                                                 //> sum1: [A, M[_]](xs: M[A])(implicit evidence$1: scalaz.learn.ex2.Monoid[A], i
6                                                   //| mplicit evidence$2: scalaz.learn.ex2.FoldLeft[M])A

这样表达清晰多了。

在scalaz里为每个类型提供了足够的操作符号。使用这些符号的方式与普通的操作符号没有两样如 a |+| b,这是infix符号表述形式。scalaz会用方法注入(method injection)方式把这些操作方法集中放在类型名称+后缀op的trait里如,MonoidOp。我们在下面示范一下method injection。假如我设计一个使用Monoid的加法:

1 def plus[A: Monoid](a1: A, a2: A): A = implicitly[Monoid[A]].mappend(a1,a2)
2                                                   //> plus: [A](a1: A, a2: A)(implicit evidence$3: scalaz.learn.ex2.Monoid[A])A
3 plus(1,2)                                         //> res2: Int = 3
4 plus("hello ","world")                            //> res3: String = hello world

假如我想为所有类型提供一个操作符|+|,然后用 a |+| b这种方式代表plus(a,b),那么我们可以增加一个Monoid的延伸trait:MonoidOp,再把这个|+|放入:

1 trait MonoidOp[A]{
2     val M : Monoid[A]
3     val a1: A
4     def |+|(a2: A) = M.mappend(a1,a2)
5 }

现在可以用infix方式调用|+|如 a |+| b。下一步是用implicit把这个|+|方法加给任何类型A:

 1 trait MonoidOp[A]{
 2     val M : Monoid[A]
 3     val a1: A
 4     def |+|(a2: A) = M.mappend(a1,a2)
 5 }
 6 implicit def toMonoidOp[A: Monoid](a: A) = new MonoidOp[A] {
 7     val M = implicitly[Monoid[A]]
 8     val a1 = a
 9 }                                                 //> toMonoidOp: [A](a: A)(implicit evidence$3: scalaz.learn.ex2.Monoid[A])scala
10                                                   //| z.learn.ex2.MonoidOp[A]
11 1 |+| 2                                           //> res2: Int = 3
12 "hello " |+| "world"                              //> res3: String = hello world

以上所见,implicit toMonoidOp的意思是对于任何类型A,如果我们能找到A类型的Monoid实例,那么我们就可以把类型A转变成MonoidOp类型,然后类型A就可以使用操作符号|+|了。现在任何类型具备Monoid实例的类型都可以使用|+|符号了。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏计算机视觉与深度学习基础

Leetcode 117 Populating Next Right Pointers in Each Node II

Follow up for problem "Populating Next Right Pointers in Each Node". What if t...

1625
来自专栏Java学习之路

Hibernate学习---单表查询

我们都知道SQL是非常强大的,为什么这么说呢?相信学过数据库原理的同学们都深有体会,SQL语句变化无穷,好毫不夸张的说可以实现任意符合我们需要的数据库操作,既然...

2597
来自专栏Python

表的数据类型

一 介绍 存储引擎决定了表的类型,而表内存放的数据也要有不同的类型,每种数据类型都有自己的宽度,但宽度是可选的 详细参考: http://www.runoob....

1847
来自专栏闻道于事

Hibernate框架HQL语句

这篇随笔将会记录hql的常用的查询语句,为日后查看提供便利。 在这里通过定义了三个类,Special、Classroom、Student来做测试,Special...

2715
来自专栏码云1024

mysql 数据类型

3404
来自专栏醉生梦死

MySQL常用函数 原

SELECT FROM_UNIXTIME(UNIX_TIMESTAMP(NOW()));

702
来自专栏LanceToBigData

MySQL(四)之MySQL数据类型

一、数据类型概述  MySQL的数据类型有大概可以分为5种,分别是 整数类型、浮点数类型和定点数类型、日期和时间类型、字符串类型、二进制类型。现在可以来看看你对...

19510
来自专栏祥子的故事

sql | 基础总结 | 思维导图

3246
来自专栏xiaoxi666的专栏

Mybatis foreach标签含义

这种方式非常方便,我们只要把查询条件写出来,剩下的操作都由mysql来处理。而在实际场景中,为了减少底层耦合,我们一般不通过mysql中的子查询方式联表查询,而...

531
来自专栏杨建荣的学习笔记

当Python字符串遇上MySQL

学习的时候我喜欢对比,MySQL和Oracle比,Python和MySQL比,总能有一些收获,也有了新的理解。 今天整理这部分内容的时候,我发现Python和...

40610

扫码关注云+社区