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. 模拟淘宝登录和购物车功能:使用cookie记录登录名,下次登录时能够记得上次的登录名,使用cookie模拟购物车功能,使用session记住登录信息并验证是否登录,防止利用url打开网站,并实现退出登录功能

    Login <%@ page language="java" contentType="text/html; charset=UTF-8" pageEnc ...

  2. Atitit  图像处理底色变红的解决

    Atitit  图像处理底色变红的解决 1.1. 原因  ImageIO  bug ,alpha通道应该在保存jpg的时候排除1 1.2. 解决,自己移除alpha通道即可1 2. Image sav ...

  3. bootstrap实现嵌入的button

    bootstrap实现嵌入的button 如下的效果: <div class="form-inline"> <div class="input-grou ...

  4. Vue+Webpack+Grunt集成

    说明 Vue.Grunt.Webpack的知识请看官方网站 Grunt Tasks:构建.开发调试.打包,命令:grunt build,grunt default,grunt zipall... We ...

  5. CSS两列高度自适应,右边自适应

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  6. KendoUI系列:TreeView

    1.加载本地数据 <link href="@Url.Content("~/Content/kendo/2014.1.318/kendo.common.min.css" ...

  7. WPF自定义控件与样式(15)-终结篇 & 系列文章索引 & 源码共享

    系列文章目录  WPF自定义控件与样式(1)-矢量字体图标(iconfont) WPF自定义控件与样式(2)-自定义按钮FButton WPF自定义控件与样式(3)-TextBox & Ric ...

  8. App乱世,3721离我们有多远

    [总结]根据众多网友的评论,看来还是WP比较给力,IOS太贵...会对手机进行优化,安卓还行,如果给中老年人用WP比较好 声明:合理讨论,禁止骂人言论,本人也不是5毛党,发表下个人看法而已. 快过年了 ...

  9. HTML5移动Web开发(三)——在移动网站中使用HTML5

    创建一个简单得HTML5页面ch01e2.html <html> <head> <meta name="viewport" content=" ...

  10. 【Java基础】方法

    Num1:检查参数的有效性 绝大多数的方法和构造器对于传递给它们的参数值都会有某些限制.比如:索引值必须是非负数,对象引用不能为null等等.这些都很常见,你应该在文档中清楚地指明所有这些限制,并在方 ...