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

 def head[T](xs: List[T]): T = xs()               //> head: [T](xs: List[T])T
head(List(,,,)) //> res0: Int = 1
head(List("a","b","c")) //> res1: String = a
case class Car(manu: String)
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(){_ + _}
//> sum: (xs: List[Int])Int
sum(List(,,)) //> res0: Int = 6
//你不能这样:sum(List(1.0,2.0,3.0)

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

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

 object intMonoid {
def mappend(i1: Int, i2: Int): Int = i1 + i2
def mzero =
}
def sum(xs: List[Int]): Int = xs.foldLeft(intMonoid.mzero)(intMonoid.mappend)
//> sum: (xs: List[Int])Int
sum(List(,,)) //> res0: Int = 6

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

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

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

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

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

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

 trait Monoid[A] {
def mappend(a1: A, a2: A): A
def mzero: A
}
object intMonoid extends Monoid[Int]{
def mappend(i1: Int, i2: Int): Int = i1 + i2
def mzero =
}
object stringMonoid extends Monoid[String]{
def mappend(s1: String, s2: String): String = s1 + s2
def mzero = ""
}

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

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

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

 trait Monoid[A] {
def mappend(a1: A, a2: A): A
def mzero: A
}
implicit object intMonoid extends Monoid[Int]{
def mappend(i1: Int, i2: Int): Int = i1 + i2
def mzero =
}
implicit object stringMonoid extends Monoid[String]{
def mappend(s1: String, s2: String): String = s1 + s2
def mzero = ""
}
def sum[A](xs: List[A])(implicit m: Monoid[A]): A = xs.foldLeft(m.mzero)(m.mappend)
//> sum: [A](xs: List[A])(implicit m: scalaz.learn.ex2.Monoid[A])A
sum(List(,,)) //> res0: Int = 6
sum(List("Hello,"," how are you")) //> res1: String = Hello, how are you

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

 trait Monoid[A] {
def mappend(a1: A, a2: A): A
def mzero: A
}
object Monoid {
implicit object intMonoid extends Monoid[Int]{
def mappend(i1: Int, i2: Int): Int = i1 + i2
def mzero =
}
implicit object stringMonoid extends Monoid[String]{
def mappend(s1: String, s2: String): String = s1 + s2
def mzero = ""
}
}

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

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

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

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

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

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

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

这样表达清晰多了。

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

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

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

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

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

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

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

Scalaz(3)- 基础篇:函数概括化-Generalizing Functions的更多相关文章

  1. Template 基础篇-函数模板(待看

    Template 基础篇-函数模板 Template所代表的泛型编程是C++语言中的重要的组成部分,我将通过几篇blog对这半年以来的学习做一个系统的总结,本文是基础篇的第一部分. Template ...

  2. SQL基础篇---函数及其函数配套使用的关键字

    一.数值函数 知识点1 SUM 求总和 SELECT breakfast,sum(price) FROM my_foods GROUP BY breakfast ORDER BY SUM(price) ...

  3. 小猪猪C++笔记基础篇(六)参数传递、函数重载、函数指针、调试帮助

    小猪猪C++笔记基础篇(六) ————参数传递.函数重载.函数指针.调试帮助 关键词:参数传递.函数重载.函数指针.调试帮助 因为一些事情以及自己的懒惰,大概有一个星期没有继续读书了,已经不行了,赶紧 ...

  4. 前端总结·基础篇·JS(三)arguments、callee、call、apply、bind及函数封装和构造函数

    前端总结系列 前端总结·基础篇·CSS(一)布局 前端总结·基础篇·CSS(二)视觉 前端总结·基础篇·CSS(三)补充 前端总结·基础篇·JS(一)原型.原型链.构造函数和字符串(String) 前 ...

  5. HTML5和CSS3扁平化风格博客(基础篇)

    多学一点总是好的~ 自始至终都觉得的css和html效果比较美观,于是在看慕课网教程时,自己也跟着敲了深爱着的前端代码 这部分分为两部分:①基础篇:http://www.imooc.com/learn ...

  6. Python基础篇(三)_函数及代码复用

    Python基础篇_函数及代码复用 函数的定义.使用: 函数的定义:通过保留字def实现. 定义形式:def <函数名>(<参数列表>): <函数体> return ...

  7. PHP丨PHP基础知识之PHP基础入门——函数「理论篇」

    前两天讲过PHP基础知识的判断条件和流程控制,今天来讲讲PHP基础知识之PHP基础入门--函数! 一.函数的声明与使用 1.函数名是标识符之一,只能有数字字母下划线,开头不能是数字. 函数名的命名,须 ...

  8. 夯实Java基础系列1:Java面向对象三大特性(基础篇)

    本系列文章将整理到我在GitHub上的<Java面试指南>仓库,更多精彩内容请到我的仓库里查看 [https://github.com/h2pl/Java-Tutorial](https: ...

  9. 《量化投资:以MATLAB为工具》连载(2)基础篇-N分钟学会MATLAB(中)

    http://www.matlabsky.com/thread-43937-1-1.html   <量化投资:以MATLAB为工具>连载(3)基础篇-N分钟学会MATLAB(下)     ...

随机推荐

  1. Atitit 图像处理之仿油画效果 Oilpaint油画滤镜 水彩画 漫画滤镜 v2

    Atitit 图像处理之仿油画效果 Oilpaint油画滤镜 水彩画 漫画滤镜 v2 1.1. 具体源码参考1 2. ,油画 水彩画具有几个比较明显的特点如下:1 2.1. 明暗层次(灰度)较少  也 ...

  2. 开源MVCPager

    PagedList<EntityName> list = filterList.OrderBy(m => m.Exp1).Reverse().ToPagedList2(id, );/ ...

  3. Liferay7 BPM门户开发之41: Expando API入门

    Expando 是liferay的一种自定义表格扩展的方式,从5.0就已存在 , 可以在运行时新建表格\字段\行\值. 这是一种Service Builder之外的轻量级替代扩展方式,不必像Servi ...

  4. 第四节:Vue表单标签和组件的基本用法,父子组件间的通信

    vue表单标签和组件的基本用法,父子组件间的通信,直接看例子吧. <!DOCTYPE html> <html> <head> <meta charset=&q ...

  5. 快速入门系列--深入理解C#

    C#语言在近些年得到了长足的方法,代码风格越来越简洁美观,例如常用的泛型及其约束.可空类型.隐式类型.匿名类型和委托等,通过下面的表格可以对这部分相对简单的特性的使用有一个初步的了解. 特性 示例 泛 ...

  6. word-wrap: break-word; break-word: break-all;区别

    word-break:break-all和word-wrap:break-word都是能使其容器如DIV的内容自动换行. 它们的区别就在于: 1,word-break:break-all 例如div宽 ...

  7. Oracle参数篇

    1. ARCHIVE_LAG_TARGET forces a log switch after the specified amount of time elapses.有效值为0(disabled) ...

  8. C/C++:提升_头文件的使用

    C/C++:提升_头文件的使用 ◇写在前面 学到现在,很多人编写程序时只会使用一个文件.这样在代码量较小的时候,更利于表达程序,但是随着代码量的逐步增加,程序的思维逻辑不是我们一下子就可以完全理清的, ...

  9. 使用Html5+C#+微信 开发移动端游戏详细教程: (四)游戏中层的概念与设计

    众所周知,网站的前端页面结构一般是由div组成,父div包涵子div,子div包涵各种标签和项, 同理,游戏中我们也将若干游戏模块拆分成层,在后续的代码维护和游戏程序逻辑中将更加清晰和便于控制. We ...

  10. AngularJS入门心得4——漫谈指令scope

    上篇<AngularJS入门心得3——HTML的左右手指令>初步介绍了指令的概念和作用.已经和指令打过一个照面,就不会那么陌生了,今天主要介绍的是一个困扰了我很久终于想通的问题,这个问题与 ...