Scalaz(11)- Monad:你存在的意义

2024-04-09 05:08
文章标签 存在 意义 monad scalaz

本文主要是介绍Scalaz(11)- Monad:你存在的意义,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

    前面提到了scalaz是个函数式编程(FP)工具库。它提供了许多新的数据类型、拓展的标准类型及完整的一套typeclass来支持scala语言的函数式编程模式。我们知道:对于任何类型,我们只需要实现这个类型的typeclass实例就可以在对这个类型施用所对应typeclass提供的所有组件函数了(combinator)。突然之间我们的焦点好像都放在了如何获取typeclass实例上了,从而忽略了考虑为什么要使用这些typeclass及使用什么样的typeclass这些问题了。所以可能有人会问我:如何获取Int的Monad实例。我会反问:傻B,你疯了吗(are you insane)?你到底想干什么?这时傻B可能忽然会醒悟还没真正了解自己这样问的目的。看来我们还是回到问题的源头,从使用scalaz的基本目的开始考虑分析了。

    我们就围绕scalaz提供的我们都熟悉的typeclass Functor, Applicative, Monad来分析说明吧,因为我们在前面对它们都进行了讨论介绍,为了与scalaz提供的众多其它typeclass有所区分,我们就暂时把它们统称为Monadic typeclass吧。首先,这几个Monadic typeclass不是数据类型,而是代表着某些编程的模式。我们知道FP编程和OOP编程最大的区别就是FP编程的状态不可变性(immutability)、无副作用(no-side-effect)、纯函数组合能力(pure code composability),这就要求FP编程在某种壳子(context)里进行状态转变(transformation)。形象点表达就是F[T]。F[]就是各种独特的壳子(context)而T就是需要运算转变的某种类型值。FP程序的结果形象描述就好像F[T] => F[U]: 代表在F[]壳子内对T进行运算,并把结果U保存在F[]内。既然FP编程相对于OOP编程是种全新的编程方式,那么自然需要一套新的程序状态转变方法,也就是一套新的操作函数施用模式了。Scalaz通过Functor, Applicative, Monad提供了三种基本的函数施用方式,它们都是针对F[T]里的T值:

// Functor    :  map[T,U]    (F[T])(f:   T => U):  F[U]
// Applicative:  ap[T,U]     (F[T])(f: F[T => U]): F[U] 
// Monad      :  flatMap[T,U](F[T])(f: T => F[U]): F[U]

以上函数施用方式产生同样的效果:F[T] => F[U],都是典型的FP编程方式。所以可以说Monadic typeclass提供了规范的FP编程框架(template),程序员可以使用这些框架进行FP编程。如果这样解释使用scalaz的目的,是不是更清楚一点了?

从另一个角度解释:scalaz typeclass 代表着抽象编程概念。typeclass是通过即兴多态来实现针对各种类型值的FP式计算的。回到开头傻B的问题:Int是一种基础类型,换句话说就是FP函数施用的目标。Monadic typeclass针对的类型是高阶的F[T]类型。我们需要对在F[]的作用环境里T类型值计算方式进行概括。我们真正需要获取的实例实际上是针对高阶类型F[_]的。所以傻B问了个错误的问题,肯定她当时不知自己在干什么。

现在我们可以分析一下应该使用什么typeclass了。总体来说,我的理解是可以把scalaz typeclass分成种类和特质:

种类定义了FP编程的各种模式。比如Functor, Applicative, Monad都代表不同的编程方式或者说它们都具备不同的程序运算模式。特质是指不同的数据类型所定义的typeclass实例控制着程序的具体运算行为。如Option Monad可以None状态中途终止运算、State Monad确保状态值一直随着程序运算。它们都因为基于不同类型的实例而表现不同的运算行为。Functor, Applicative, Monad的特质则由它们的实例中map, ap, flatMap这三个驱动函数的具体实现方式所决定。我们先看看现成的Option Functor,它的实现方式如下:

implicit object optionFunctor extends Functor[Option] {def map[T,U](ot: Option[T])(f: T => U): Option[U] = ot match {case Some(t) => Some(f(t))case None => None}
}

Option Functor实例驱动函数map的意思是说如果目标类型F[T]的值是个Some,那么我们就在Some壳内施用参数提供的一般函数f;如果目标值是None就不施用函数。我们再看看List Functor:

implicit object listFunctor extends Functor[List] {def map[T,U](lt: List[T])(f: T => U): List[U] = lt match {case Nil => Nilcase head :: tail => f(head) :: map(tail)(f)}
}

List Functor的map函数彰显出对一串在壳内元素逐个转变的特性。从List操作方式就很容易理解:list.map(t => transform(t))

我们再看看Option Applicative的实例:

implicit object objectApplicative extends Applicative[Option] {def point[T](t: T): Option[T] = Some(t)def ap[T,U](ot: Option[T])(of: Option[T => U]): Option[U] = (ot, of) match {case (Some(t), Some(f)) => Some(f(t))case _ => None }
}

Option Applicative的驱动函数ap又一次凸显了Option的特别处理方式:只有在目标值和操作函数都不为None时才施用通过壳提供的操作函数。

再看看Option Monad实例:

implicit object optionMonad extends Monad[Option] {def flatMap[T,U](ot: Option[T])(f: T => Option[U]): Option[U] = ot match {case Some(t) => f(t)case _ => None}
}

这个flatMap函数可以告诉我们更多东西:如果我们把Option[T]视作一个运算的话,那么只要这个运算结果不为None就可以选择连续运算,因为:f: T => Option[U],用文字描述即为给一个T值进行计算后产生另一个运算Option[U],如果再给Option[U]一个值进行计算的话就又会产生另一个运算Opton[V]... 如此持续:

F[A](a => F[B](b => F[C](c => F[D])...))。用flatMap链表示:

  fa.flatMap(a => fb.flatMap(b => fc.flatMap(c => fd.map(...))))


从flatMap串联就比较容易观察到Monad运算的关联依赖性和串联行:后面一个运算需要前面那个运算的结果。而在Option Monad里如果前面的运算产生结果是None的话,串联运算终止并直接返回None作为整串运算的结果。

值得提醒的是连串的flatMap其实也是一种递归算法,但又不属于尾递归,所以拥有和其它FP算法一样的通病:会消耗堆栈,超长的flatMap链条很容易造成堆栈溢出错误(stack overflow)。所以,直接使用Monad编程是不安全的,必须与Trampling数据结构配合使用才行。正确安全的Monad使用方式是通过Trampling结构存放原本在堆栈上的函数调用参数,以heap替换stack来防止stack-overflow。我们会在将来详细讨论Trampling原理机制。

我们可以从上面的flatMap串中推导出for-comprehension:

//   for {
//      a <- (fa: F[A])
//      b <- (fb: F[A])
//      c <- (fc: F[A])
//   } yield { ... }

从for-comprehension能够更容易看出:我们可以选择在for loop内按要求连续运算F[T]。只要我们能提供a,b,c ...作为运算元素。

按理来说除了Option Monad,其它类型的Monad都具备这种连续运算的可选择性。而Option Monad的特点就在于在运算结果为None时可以立即终止运算。

现在我们可以试着自定义一个类型然后获取个什么实例。不过我们还是要谨记自定义类型的目的何在。我看多数可能是实现Monad实例,这样我们就可以在自定义类型的控制下进行Monadic编程了,即在for-comprehension内进行熟悉的行令编程(imperative programming)。我们应该没什么需要去获取Functor或Applicative实例,而且Monad trait也继承了Functor及Applicative trait,因为map和ap都可以用flatMap来实现:

def map[A,B](fa: F[A])(f: A => B): F[B] =fa flatMap {a => point(f(a))}
def ap[A,B](fa: F[A])(ff: F[A => B]): F[B] =ff flatMap { f => fa flatMap {a => f(a) }}


值得注意的是:flatMap有着很明显的串性,适合于运算流程管理(workflow)。但实现并行运算就会困难了。这就是Applicative存在的主要原因。如果自定义Monad需要进行并行运算的话就要避免用flatMap实现ap。正确的方式是不用其它的组件函数,直接单独实现ap函数。

很多人自定义Monad可能就是简单希望能用for-comprehension。它是一种简单的FP编程语言(Monadic language):能在一个自定义类型的壳内(context)进行行令编程来实现程序状态转变。如上面强调的那样,我们必须先要搞清楚自定义Monad类型的目的:一开始我们希望能用FP方式实现一些简单的行令编程,如下:

var a = 3
var b = 4
var c = a + b

就是这么简单。不过我们希望用FP方式来实现。那么可不可以这么描述需求:对同样某一种种数据类型的变量进行赋值,然后对这些变量实施操作,在这里是相加操作。那么我们需要一个高阶类型F[T],用F来包嵌一种类型数据T。在壳内运算T后结果还是一个T类型值。

我们先定义一下这个类型吧:

trait Bag[A] {def content: A
}
object Bag {def apply[A](a: A) = new Bag[A] { def content = a }
}

形象点解释:一个袋子Bag里装一种可以是任何类型A的东西。

用scalaz来实现Bag类型的Monad实例很简单:

trait Bag[A] {def content: A
}
object Bag {def apply[A](a: A) = new Bag[A] { def content = a }implicit object bagMonad extends Monad[Bag] {def point[A](a: => A) = Bag(a)def bind[A,B](ba: Bag[A])(f: A => Bag[B]): Bag[B] = f(ba.content)}
}

只要定义了point,bind函数即可。point能把一个普通类型A的值套入壳子Bag。bind既是flatMap,它决定了从一个运算连接到下一个运算过程中对壳中数据进行的附加处理。可以看到以上bagMonad的bind函数没有附加任何处理,直接对目标壳内数据(ba.content)施用传入函数f。

现在Bag已经是个Monad实例了,我们可以使用所有Monad typeclass提供的函数:

val chainABC = Bag(3) flatMap {a => Bag(4) flatMap {b => Bag(5) flatMap  {c => Bag(a+b+c) }}}//> chainABC  : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@c8e4bb0
chainABC.content                                  //> res0: Int = 12val bagABC = Bag(3) >>= {a => Bag(4) >>= {b => Bag(5) map {c => (a+b+c) }}}//> bagABC  : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@29626d54
bagABC.content                                    //> res1: Int = 12
val bagHello = Bag("Hello") >>= {a => Bag(" John,") >>= {b => Bag("how are you?") map {c => (a+b+c) }}}//> bagHello  : Exercises.monad.Bag[String] = Exercises.monad$Bag$$anon$1@5a63f5//| 09
bagHello.content                                  //> res2: String = Hello John,how are you?


注意我们是如何把壳内变量a,b,c从前面传导到后面的加法操作里的。我们已经实现了Monad的流程式运算。

现在我们可以使用最希望用的for-comprehension来实现上面的行令编程了:

val addABC: Bag[Int] = for {a <- Bag(3)b <- Bag(4)c <- Bag(5)
} yield a+b+c                                     //> addABC  : Exercises.monad.Bag[Int] = Exercises.monad$Bag$$anon$1@10e41621
addABC.content                                    //> res2: Int = 12val concatABC: Bag[String] =
for {a <- Bag("hello")b <- Bag(" jonh,")c <- Bag("how are you ?")
} yield ( a+b+c)                                  //> concatABC  : Exercises.monad.Bag[String] = Exercises.monad$Bag$$anon$1@353d0//| 772
concatABC.content                                 //> res3: String = hello jonh,how are you ?

不要看上面的程序好像很简单,但它代表的意义却是重大的:首先我们实现了FP方式的状态转变:我们虽然使用了行令编程,但最终壳Bag内部的数据content运算结果正是我们编程时所期望的。再就是我们通过flatMap串联持续对多个变量一一进行了赋值,然后用普通的函数把这些变量进行了结合yield (a+b+c)。可以说我们初步尝试实现了FP编程模式(在一个什么壳内进行运算)。

前面说过,for-comprehension可以是一种简单的FP编程语言Monadic language。用它编制的程序运算行为可以受定义它的Monad实例所控制。那么我们就试着为我们的Bag Monad增加一点影响:

trait Bag[+A] {}
case class Bagged[+A](content: A) extends Bag[A]
case object Emptied extends Bag[Nothing]

我们稍微调整了一下Bag类型。现在Bag由两种状态组成:有东西的袋子Bagged和空袋子Emptied。如果希望我们的Monadic程序在遇到Emptied时能像Option Monad那样立即终止运算并直接返回Emptied结果,我们必须在bind函数里设定这种行为:

trait Bag[+A] {}
case class Bagged[+A](content: A) extends Bag[A]
case object Emptied extends Bag[Nothing]object Bag {implicit object bagMonad extends Monad[Bag] {def point[A](a: => A) = Bagged(a)def bind[A,B](ba: Bag[A])(f: A => Bag[B]): Bag[B] = ba match {case Bagged(a) => f(a)case _ => Emptied}}
}

在bind函数里我们用模式匹配方式判断输入Bag状态:如果是有装东西的(Bagged)那么像上面的设计一样直接运算f获取下一个Bag状态,如果是空袋子Emptied的话就不做任何运算直接返回Emptied。我们现在可以测试一下上面定义的运算:

val chainABC = Monad[Bag].point(3) flatMap {a => Monad[Bag].point(4) flatMap {b => Monad[Bag].point(5) flatMap  {c => Bagged(a+b+c) }}}//> chainABC  : Exercises.monad.Bag[Int] = Bagged(12)
val bagABC = Monad[Bag].point(3) >>= {a => Monad[Bag].point(4) >>= {b => Monad[Bag].point(5) map {c => (a+b+c) }}}//> bagABC  : Exercises.monad.Bag[Int] = Bagged(12)
val bagHello = Monad[Bag].point("Hello") >>= {a => Monad[Bag].point(" John,") >>= {b => Monad[Bag].point("how are you?") map {c => (a+b+c) }}}//> bagHello  : Exercises.monad.Bag[String] = Bagged(Hello John,how are you?)
val addABC: Bag[Int] = for {a <- Monad[Bag].point(3)b <- Monad[Bag].point(4)c <- Monad[Bag].point(5)
} yield a+b+c                                     //> addABC  : Exercises.monad.Bag[Int] = Bagged(12)val concatABC: Bag[String] =
for {a <- Monad[Bag].point("hello")b <- Monad[Bag].point(" jonh,")c <- Monad[Bag].point("how are you ?")
} yield ( a+b+c)                                  //> concatABC  : Exercises.monad.Bag[String] = Bagged(hello jonh,how are you ?)//| 


我们可以看到在Bag不是Emptied时,以上这些程序运算行为与上一个版本的Monad程序没有区别。但是如果我们增加了Emptied呢:

val bagABC = Monad[Bag].point(3) >>= {a => (Bagged(4): Bag[Int]) >>= {b => Monad[Bag].point(5) >>= { c => (Emptied: Bag[Int]) map {c => (a+b+c) }}}}//> bagABC  : Exercises.monad.Bag[Int] = Emptied


flatMap链条中间出现了Emptied,运算终断,返回Emptied结果。注意下面的表达形式:

Monad[Bag].point(3)

(Bagged(3): Bag[Int]) 

意思都是一样的。但Bagged(3).flatMap这样写是不行的,因为Bagged(3)不明确是Bag。

再看看在for-comprehension程序中加上Emptied情况:

val addABC: Bag[Int] = for {a <- Monad[Bag].point(3)x <- (Emptied: Bag[Int])b <- Monad[Bag].point(4)c <- Monad[Bag].point(5)
} yield a+b+c                                     //> addABC  : Exercises.monad.Bag[Int] = Emptiedval concatABC: Bag[String] =
for {a <- Monad[Bag].point("hello")x <- (Emptied: Bag[Int])b <- Monad[Bag].point(" jonh,")c <- Monad[Bag].point("how are you ?")
} yield ( a+b+c)                                  //> concatABC  : Exercises.monad.Bag[String] = Emptied


不错,正是我们期待的运算行为。

现在我们可以用简单的语言来描述Monad存在的意义:它提供了一套规范的模式来支持FP编程。






这篇关于Scalaz(11)- Monad:你存在的意义的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/887249

相关文章

easyui同时验证账户格式和ajax是否存在

accountName: {validator: function (value, param) {if (!/^[a-zA-Z][a-zA-Z0-9_]{3,15}$/i.test(value)) {$.fn.validatebox.defaults.rules.accountName.message = '账户名称不合法(字母开头,允许4-16字节,允许字母数字下划线)';return fal

【408DS算法题】039进阶-判断图中路径是否存在

Index 题目分析实现总结 题目 对于给定的图G,设计函数实现判断G中是否含有从start结点到stop结点的路径。 分析实现 对于图的路径的存在性判断,有两种做法:(本文的实现均基于邻接矩阵存储方式的图) 1.图的BFS BFS的思路相对比较直观——从起始结点出发进行层次遍历,遍历过程中遇到结点i就表示存在路径start->i,故只需判断每个结点i是否就是stop

UVM:callback机制的意义和用法

1. 作用         Callback机制在UVM验证平台,最大用处就是为了提高验证平台的可重用性。在不创建复杂的OOP层次结构前提下,针对组件中的某些行为,在其之前后之后,内置一些函数,增加或者修改UVM组件的操作,增加新的功能,从而实现一个环境多个用例。此外还可以通过Callback机制构建异常的测试用例。 2. 使用步骤         (1)在UVM组件中内嵌callback函

LeetCode题练习与总结:存在重复元素Ⅱ--219

一、题目描述 给你一个整数数组 nums 和一个整数 k ,判断数组中是否存在两个 不同的索引 i 和 j ,满足 nums[i] == nums[j] 且 abs(i - j) <= k 。如果存在,返回 true ;否则,返回 false 。 示例 1: 输入:nums = [1,2,3,1], k = 3输出:true 示例 2: 输入:nums = [1,0,1,1], k

思考自己写博客的意义

感想 从今年2月份开始,我就要求自己以每周4篇的速度写博客。然而问题是:每周的空闲时间是不稳定的,这导致我没法保证花费相同的时间去输出稳定质量的博客。当时间不够时,我将面临选择:是减少数量来保证稳定的质量,还是降低质量来保证稳定的数量? 我选择的是——牺牲质量。因为,相比于“质量”,“数量”是准确可见的。我担心:一旦有一次没能完成目标数量,便会出现“破窗效应”,让我潜意识里再也不认同“目标”了

从“法线贴图的意义”到“切线空间公式的推导与验证”

目录 目标1. 法线贴图1.1 “法线”的意义1.2 “法线贴图”的意义 2. 切线空间2.1 法线贴图中数据的含义2.2 “切线空间”的定义 3. 切线空间计算公式3.1 构造几何关系等式3.2 切线空间计算公式 4. 代码5. 验证——与其他美术软件计算的结果进行比较总结 目标 本篇的重点是 讨论法线贴图的意义讨论切线空间的意义推导切线空间的计算公式根据公式编写代码将其计算

用友UFIDA NC portal/pt/file/upload接口存在任意文件上传漏洞 附POC

@[toc] 用友UFIDA NC portal/pt/file/upload接口存在任意文件上传漏洞 附POC 免责声明:请勿利用文章内的相关技术从事非法测试,由于传播、利用此文所提供的信息或者工具而造成的任何直接或者间接的后果及损失,均由使用者本人负责,所产生的一切不良后果与文章作者无关。该文章仅供学习用途使用。 1. 用友UFIDA NC 简介 微信公众号搜索:南风漏洞复现文库 该文

DataOps 对企业的数据管理有和意义?该如何落地?

DataOps,即数据开发运营一体化,能够帮助企业构建集开发、治理、运营于一体的自动化数据流水线,进而提高数据产品的交付效率和质量。对企业的数据管理主要有以下帮助: 数据开发更加敏捷高效,快速响应业务需求。在数据生产端,通过数据加工流程中的自动化技术或工具,能够实现数据的快速、可靠和高效交付,在数据消费端,支持自助服务的形式,帮助用数人员自主获取和处理数据。 重构研发治理一体化流水线,对数据研

mongodb document使用的数据结构中存在List结构导致查询速度异常缓慢

最近使用的mongodb 数据库,项目中查询某个列表异常缓慢,甚至造成cpu突然上升,后经过排查,发现  ,因为文档中存在List结构的数据,所以造成查询异常缓慢,以后使用mongodb数据库的项目,慎用list结构,一部小心就是一个大坑

java常用算法之返回目标数字在有序数组中的位置(假设有序数组中不存在重复数字)

/*** @Description 返回目标数字在有序数组中的位置(假设有序数组中不存在重复数字)* @param source* @param target* @return*/public static Integer numIndex(int[] source, int target) {int index = 0;if (source == null || source.length =