Scalaz(12)- Monad:再述述flatMap,顺便了解MonadPlus

2024-04-09 05:08

本文主要是介绍Scalaz(12)- Monad:再述述flatMap,顺便了解MonadPlus,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在前面的几篇讨论里我们初步对FP有了些少了解K:FP嘛,不就是F[A]吗?也是,FP就是在F[]壳子(context)内对程序的状态进行更改,也就是在F壳子(context)内施用一些函数。再直白一点就是在F壳子内进行OOP惯用的行令编程(imperative programming)。当然,既然是在壳子(context)内进行编程这种新的模式,那么总需要些新的函数施用方法吧。我们再次审视一下以前了解过的FP函数施用方法:

1 // Functor    :  map[A,B]    (F[A])(f:   A => B):  F[B]
2 // Applicative:  ap[A,B]     (F[A])(f: F[A => B]): F[B] 
3 // Monad      :  flatMap[A,B](F[A])(f: A => F[B]): F[B]


它们分别代表了scalaz的三个typeclass。对于FP编程来讲,函数施用(function application)就是改变程序状态,也就是map。那么从map角度分析,如果直接对F[A=>B], A=>F[B]进行map会产生不同的结果类型,如直接map A=>F[B]结果是F[F[B]]。所以我们会想办法把结果类型对齐了,使最终结果类型为F[B]:

 def ap[A,B](ma: F[A])(mf: F[A => B]): F[B] = mf.flatMap(f => ma.flatMap(a => point(f(a)))  def flatMapByJoin[A,B](ma: M[A])(f: A => M[B]): M[B] = join(map(ma)(a => f(a)))def join[A](mma: M[M[A]]): M[A]

从上面的代码中我们看到:在flatMap我们通过join施用了map。而这个join好像就是为了把F[F[B]]打平到F[B]而设计的,这点从join函数款式(signature)可以看出。难道FP就是为了实现类型匹配吗?绝不是!我们不能把眼光局限在如何取得类型匹配上,而是应该放阔到函数施用的目的上。我们从上面函数map,ap,flatMap的类型款式可以看出:map,ap都是在F[]壳(context)内施用的,而flatMap是在壳外对输入的类型A值进行施用的,但把结果放入了壳内。这可以说是flatMap与map,ap的根本不同之处。那么flatMap代表着什么呢?如果从flatMap的函数款式(function signature)分析:它是一个递归算法: 给F[A]一个A产生F[B],再给F[B]一个B再产生F[C]...如此类推。这样看来flatMap是一个持续算法(computational continuation),如果把flatMap串联起来就可以实现某种编程语法(syntax)。这个推论在scala的for-comprehension中得到证实:flatMap可以被视作一种简单的FP语法,它使我们可以在for-comprehension中使用我们熟悉的行令编程,其结果是FP模式的行令编程。flatMap是Monad的标识函数,而Monad又具备所有的FP函数施用方法因为它继承了Functor和Applicative,所以有些人把FP编程称为Monadic programming。从这里也可以看到flatMap在FP编程里的重要性。

如果从flatMap代表持续算法这个角度分析:flatMap实际连接了两个算法F[A] => F[B]。我们应该可以在运算flatMap的过程中实现一些附加的效果。这个要求应该可以在实现flatMap函数时做到。我们这篇讨论的重点就是在示范如何在实现flatMap时增加一些效果。当把一串算法用flatMap链接起来时这些附加效果是如何积累的。

我想没什么比logger更能示范串接算法前面算法的一些效果是如何流转到下面的算法里的。我们来设计一个例子:模拟一个输入装置,每接收一次输入代表一次运算,用一个logger把每次运算的输入都记录下来。当然,这个例子用State Monad就很容易实现。不过我们的目的是去示范如何通过flatMap把效果传递下去的,所以还是应该紧贴着如何实现flatMap:

trait KeyLog[K] {def value: Kdef log: Stringoverride def toString = "["+value+","+log+"]"
}
object KeyLog {def apply[K](k: K, msg: String): KeyLog[K] = new KeyLog[K] {def value = kdef log = msg}
}KeyLog(3,"Entered Number 3")                      //> res0: Exercises.keylog.KeyLog[Int] = [3,Entered Number 3]
KeyLog("Hello", "Entered String 'Hello'")         //> res1: Exercises.keylog.KeyLog[String] = [Hello,Entered String 'Hello']

我们用KeyLog[K]来代表这个输入算法。每个算法都包含一个K类型的value和String类型的log。对于类型参数K我们可以直接用普通的flatMap K => KeyLog[I]来转变value。而我们的目的是如何通过flatMap把前一个KeyLog的log累积到下个算法的log。挺简单,是吧?在KeyLog结构里转变log并把结果留在KeyLog里,听着像是map,不过map是针对K的。所以我们要先加个mapLog:

trait KeyLog[K] {def value: Kdef log: Stringoverride def toString = "["+value+","+log+"]"def mapLog(preLog: String): KeyLog[K] = KeyLog(value,preLog +";"+log)
}

我们试着实现flatMap:

trait KeyLog[K] {def value: Kdef log: Stringoverride def toString = "["+value+","+log+"]"def mapLog(preLog: String): KeyLog[K] = KeyLog(value,preLog +";"+log)def flatMap[I](f: K => KeyLog[I]): KeyLog[I] =f(value).mapLog(log)
}

确实简单又直接:f(value) 产生 KeyLog[I] 然后在这个接着的算法中调用 mapLog 把上一个算法KeyLog[K]的log并入KeyLog[I]的log。

我们试着用一下flatMap:

KeyLog(3,"Entered Number 3").flatMap(a => KeyLog("Hello", "Entered String 'Hello'"))//> res2: Exercises.keylog.KeyLog[String] = [Hello,Entered Number 3;Entered Stri//| ng 'Hello']

最终log值:"Entered Number 3;Entered String 'Hello'。我们实现了在运算flatMap过程中对log进行的累积。

现在我们可以先获取KeyLog的Monad实例,然后进行flatMap串联及使用for-comprehension进行行令编程了:

object KeyLog {def apply[K](k: K, msg: String): KeyLog[K] = new KeyLog[K] {def value = kdef log = msg}
import scalaz._
import Scalaz._implicit object keylogMonad extends Monad[KeyLog] {def point[K](k: => K): KeyLog[K] = KeyLog(k,"")def bind[K,I](kk: KeyLog[K])(f: K => KeyLog[I]): KeyLog[I] = kk flatMap f}
}

在KeyLog Monad实例里bind使用了我们设计的flatMap函数。看看flatMap串接和for-comprehension效果:

def enterInt(k: Int): KeyLog[Int] = KeyLog(k, "Number:"+k.toString)//> enterInt: (k: Int)Exercises.keylog.KeyLog[Int]
def enterStr(k: String): KeyLog[String] = KeyLog(k,"String:"+k)//> enterStr: (k: String)Exercises.keylog.KeyLog[String]
enterInt(3) >>= {a => enterInt(4) >>= {b => enterStr("Result:") map {c => c + (a * b).toString} }}//> res3: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String://| Result:;]
for {a <- enterInt(3)b <- enterInt(4)c <- enterStr("Result:")
} yield c + (a * b).toString                      //> res4: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String//| :Result:;]

value和log都按照要求实现了转变。

在使用for-comprehension时突然想到守卫函数(guard function)。我想既然已经得到了KeyLog的Monad实例,是不是可以在它的for-comprehension里使用守卫函数呢?就像这样:

for {a <- enterInt(3)b <- enterInt(4)  if b > 0c <- enterStr("Result:")
} yield c + (a * b).toString

不过无法通过编译。提示需要filter函数。查了一下MonadPlus typeclass可以提供这个函数。那么我们就沿着惯用的套路获取一下KeyLog的MonadPlus实例。MonadPlus trait的定义如下:scalaz/MonadPlus.scala

trait MonadPlus[F[_]] extends Monad[F] with ApplicativePlus[F] { self =>/** Remove `f`-failing `A`s in `fa`, by which we mean: in the* expression `filter(filter(fa)(f))(g)`, `g` will never be invoked* for any `a` where `f(a)` returns false.*/def filter[A](fa: F[A])(f: A => Boolean) =bind(fa)(a => if (f(a)) point(a) else empty[A])
...

MonadPlus又继承了ApplicativePlus:scalar/ApplicativePlus.scala

trait ApplicativePlus[F[_]] extends Applicative[F] with PlusEmpty[F] { self =>

ApplicativePlus又继承了PlusEmpty: scalaz/PlusEmpty.scala

trait PlusEmpty[F[_]] extends Plus[F] { self =>def empty[A]: F[A]
...

PlusEmpty定义了抽象成员empty[A],又继承了Plus: scalar/Plus.scala

trait Plus[F[_]]  { self =>/**The composition of Plus `F` and `G`, `[x]F[G[x]]`, is a Plus */def compose[G[_]](implicit G0: Plus[G]): Plus[({type λ[α] = F[G[α]]})#λ] = new CompositionPlus[F, G] {implicit def F = selfimplicit def G = G0}/**The product of Plus `F` and `G`, `[x](F[x], G[x]])`, is a Plus */def product[G[_]](implicit G0: Plus[G]): Plus[({type λ[α] = (F[α], G[α])})#λ] = new ProductPlus[F, G] {implicit def F = selfimplicit def G = G0}def plus[A](a: F[A], b: => F[A]): F[A]def semigroup[A]: Semigroup[F[A]] = new Semigroup[F[A]] {def append(f1: F[A], f2: => F[A]): F[A] = plus(f1, f2)}
...

Plus又定义了抽象成员plus[A],那么获取MonadPlus实例必须实现empty[A]: F[A]和plus[A](a: F[A], b: F[A]): F[A]。看来这个PlusEmpty就是一种Monoid,只不过是针对高阶类型的。我们知道Monad实例类型必须是高阶的M[_],那么如果Monad实例同时又具备Monoid特性的话,那么就可以使用MonadPlus来描述它的性质。

好了,现在我想获取KeyLog[K]的MonadPlu实例,那么我必须实现empty[A]:F[A]和plus[A](a:F[A],b:F[A]):F[A]。KeyLog[K]的empty[K]是什么呢?想了半天没得到答案,可能KeyLog[K]就没有empty[K]吧。也许我们想取得KeyLog MonadPlu实例的目的还没搞清楚。看看上面的需求:

for {a <- enterInt(3)b <- enterInt(4)  if b > 0c <- enterStr("Result:")
} yield c + (a * b).toString

从字面上看是希望通过守卫函数过滤数字为0的数字。等等,enterInt(4)已经确定了输入为4,是 > 0,还过滤什么?不是找事吗。所以我们的目的应该聚焦在过滤需求上。Scalaz为List,Option提供了MonadPlus实例,我们看看这两种类型的守卫函数使用:

for { //lista <- 1 |-> 50 if a.shows contains '7'
} yield a                                         //> res5: List[Int] = List(7, 17, 27, 37, 47)
for { //optiona <- Some(3)b <- Some(4) if a < b
} yield b                                         //> res6: Option[Int] = Some(4)
for { //optiona <- Some(3)b <- Some(4) if a > b
} yield b                                         //> res7: Option[Int] = None


先分析List例子:一个List可能是空的,又可能有多过一个元素,有多种可能。守卫函数的功能就是在这些可能里进行选择。

再分析Option:可能是None或者Some,这本身就是一种筛选。对于KeyLog[K],它只有一种状态,没有选择的需要,所以我无法实现KeyLog[K]的empty[K]。

List和Option的empty分别是:Nil和None,这个很容易理解。那么plus呢?把 plus(list1,list2):list3 = list1 ++ list2这个倒是容易理解,但plus(option1,option2):option3这个又应该怎么理解呢?我们还是看看在scalaz里是怎么定义plus的吧:scalaz.std/List.scala

  implicit val listInstance = new Traverse[List] with MonadPlus[List] with Each[List] with Index[List] with Length[List] with Zip[List] with Unzip[List] with Align[List] with IsEmpty[List] with Cobind[List] {
...def empty[A] = Nildef plus[A](a: List[A], b: => List[A]) = a ++ b
...

List的plus就是把两个List接起来(concat)

scalaz.std/Option.scala

  implicit val optionInstance = new Traverse[Option] with MonadPlus[Option] with Each[Option] with Index[Option] with Length[Option] with Cozip[Option] with Zip[Option] with Unzip[Option] with Align[Option] with IsEmpty[Option] with Cobind[Option] with Optional[Option] {
...def empty[A]: Option[A] = Nonedef plus[A](a: Option[A], b: => Option[A]) = a orElse b
...


Option的plus意思是如果a是None就取b否则取a,无论b是否None。我们用MonadPlus提供的操作符号<+>来示范:

List(1,2,3) <+> List(4,5,6)                       //> res4: List[Int] = List(1, 2, 3, 4, 5, 6)
Nil <+> List(1,2,3)                               //> res5: List[Int] = List(1, 2, 3)
List(1,2,3) <+> Nil                               //> res6: List[Int] = List(1, 2, 3)
none <+> 2.some                                   //> res7: Option[Int] = Some(2)
2.some <+> 3.some                                 //> res8: Option[Int] = Some(2)
2.some <+> none                                   //> res9: Option[Int] = Some(2)
none <+> none                                     //> res10: Option[Nothing] = None


为了实现KeyLog MonadPlus实例,我们必须对KeyLog类型重新定义使之包含多过一种状态:

import scalaz._
import Scalaz._
object keylog {
trait KeyLog[+K] {override def toString = this match {case KeyIn(value,log) => "["+value+","+log+"]"case _ => "[Keypad Locked]"}def mapLog(preLog: String): KeyLog[K] = this match {case KeyIn(value,log) => KeyIn(value,preLog +";"+log)case _ => KeyLock}def flatMap[I](f: K => KeyLog[I]): KeyLog[I] = this match {case KeyIn(value,log) => f(value).mapLog(log)case _ => KeyLock}
}
case class KeyIn[K](value: K, log: String) extends KeyLog[K]
case object KeyLock extends KeyLog[Nothing]
object KeyLog {
/*	def apply[K](k: K, msg: String): KeyLog[K] = new KeyLog[K] {def value = kdef log = msg} */implicit object keylogMonad extends Monad[KeyLog] {def point[K](k: => K): KeyLog[K] = KeyIn(k,"")def bind[K,I](kk: KeyLog[K])(f: K => KeyLog[I]): KeyLog[I] = kk flatMap f}
}


我们增加了KeyIn和KeyLock两种状态。然后我们只需要通过模式匹配(pattern matching)在实现前面逻辑的时候把多种KeyLog状态考虑进去。

运行前面的例子:

def enterInt(k: Int): KeyLog[Int] = KeyIn(k, "Number:"+k.toString)//> enterInt: (k: Int)Exercises.keylog.KeyLog[Int]
def enterStr(k: String): KeyLog[String] = KeyIn(k,"String:"+k)//> enterStr: (k: String)Exercises.keylog.KeyLog[String]
enterInt(3) >>= {a => enterInt(4) >>= {b => enterStr("Result:") map {c => c + (a * b).toString} }}//> res0: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String//| :Result:;]
for {a <- enterInt(3)b <- enterInt(4)c <- enterStr("Result:")
} yield c + (a * b).toString                      //> res1: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String//| :Result:;]

现在把KeyLock效果加进去:

enterInt(3) >>= {a => (KeyLock: KeyLog[Int]) >>= {b => enterStr("Result:") map {c => c + (a * b).toString} }}//> res2: Exercises.keylog.KeyLog[String] = [Keypad Locked]
for {a <- enterInt(3)b <- enterInt(4)x <- (KeyLock: KeyLog[String])c <- enterStr("Result:")
} yield c + (a * b).toString                      //> res3: Exercises.keylog.KeyLog[String] = [Keypad Locked]

正是我们期待的效果。

现在我们可以把MonadPlus特质混入keylogMonad实例(trait mix-in):

	implicit object keylogMonad extends Monad[KeyLog] with MonadPlus[KeyLog] {def point[K](k: => K): KeyLog[K] = KeyIn(k,"")def bind[K,I](kk: KeyLog[K])(f: K => KeyLog[I]): KeyLog[I] = kk flatMap fdef empty[K]: KeyLog[K] = KeyLockdef plus[K](a: KeyLog[K], b: => KeyLog[K]): KeyLog[K] = a match {case KeyIn(value,log) => KeyIn(value,log)case KeyLock => b}}

在实例中我们实现了empty和plus。

那么现在我们可以使用守卫函数了吧:

for {a <- enterInt(3)b <- enterInt(4)c <- enterStr("Result:")
} yield c + (a * b).toString                      //> res3: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;String//| :Result:;]
for {a <- enterInt(3)b <- enterInt(4) if b > 0c <- enterStr("Result:")
} yield c + (a * b).toString                      //> res4: Exercises.keylog.KeyLog[String] = [Result:12,Number:3;Number:4;;Strin//| g:Result:;]
for {a <- enterInt(3)b <- enterInt(4) if b > 5c <- enterStr("Result:")
} yield c + (a * b).toString                      //> res5: Exercises.keylog.KeyLog[String] = [Keypad Locked]

守卫函数按要求对KeyLog状态进行了过滤。





这篇关于Scalaz(12)- Monad:再述述flatMap,顺便了解MonadPlus的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



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

相关文章

关于数据埋点,你需要了解这些基本知识

产品汪每天都在和数据打交道,你知道数据来自哪里吗? 移动app端内的用户行为数据大多来自埋点,了解一些埋点知识,能和数据分析师、技术侃大山,参与到前期的数据采集,更重要是让最终的埋点数据能为我所用,否则可怜巴巴等上几个月是常有的事。   埋点类型 根据埋点方式,可以区分为: 手动埋点半自动埋点全自动埋点 秉承“任何事物都有两面性”的道理:自动程度高的,能解决通用统计,便于统一化管理,但个性化定

速了解MySQL 数据库不同存储引擎

快速了解MySQL 数据库不同存储引擎 MySQL 提供了多种存储引擎,每种存储引擎都有其特定的特性和适用场景。了解这些存储引擎的特性,有助于在设计数据库时做出合理的选择。以下是 MySQL 中几种常用存储引擎的详细介绍。 1. InnoDB 特点: 事务支持:InnoDB 是一个支持 ACID(原子性、一致性、隔离性、持久性)事务的存储引擎。行级锁:使用行级锁来提高并发性,减少锁竞争

PHP: 深入了解一致性哈希

前言 随着memcache、redis以及其它一些内存K/V数据库的流行,一致性哈希也越来越被开发者所了解。因为这些内存K/V数据库大多不提供分布式支持(本文以redis为例),所以如果要提供多台redis server来提供服务的话,就需要解决如何将数据分散到redis server,并且在增减redis server时如何最大化的不令数据重新分布,这将是本文讨论的范畴。 取模算法 取模运

Weex入门教程之1,了解Weex

【资料合集】Weex Conf回顾集锦:讲义PDF+活动视频! PDF分享:链接:http://pan.baidu.com/s/1hr8RniG 密码:fa3j 官方教程:https://weex-project.io/cn/v-0.10/guide/index.html 用意 主要是介绍Weex,并未涉及开发方面,好让我们开始开发之前充分地了解Weex到底是个什么。 以下描述主要摘取于

Java了解相对较多!

我是对Java了解相对较多,而对C#则是因工作需要才去看了一下,C#跟Java在语法上非常相似,而最初让我比较困惑的就是委托、事件部分,相信大多数初学者也有类似的困惑。经过跟Java的对比学习,发现这其实跟Java的监听、事件是等同的,只是表述上不同罢了。   委托+事件是观察者模式的一个典型例子,所谓的委托其实就是观察者,它会关心某种事件,一旦这种事件被触发,这个观察者就会行动。   下

使用WebP解决网站加载速度问题,这些细节你需要了解

说到网页的图片格式,大家最常想到的可能是JPEG、PNG,毕竟这些老牌格式陪伴我们这么多年。然而,近几年,有一个格式悄悄崭露头角,那就是WebP。很多人可能听说过,但到底它好在哪?你的网站或者项目是不是也应该用WebP呢?别着急,今天咱们就来好好聊聊WebP这个图片格式的前世今生,以及它值不值得你花时间去用。 为什么会有WebP? 你有没有遇到过这样的情况?网页加载特别慢,尤其是那

初步了解VTK装配体

VTK还不太了解,根据资料, vtk.vtkAssembly 是 VTK库中的一个重要类,允许通过将多个vtkActor对象组合在一起来创建复杂的3D模型。 import vtkimport mathfrom vtk.util.colors import *filenames = ["cylinder.stl","sphere.stl","torus.stl"]dt = 1.0renW

Post-Training有多重要?一文带你了解全部细节

1. 简介 随着LLM学界和工业界日新月异的发展,不仅预训练所用的算力和数据正在疯狂内卷,后训练(post-training)的对齐和微调方法也在不断更新。InstructGPT、WebGPT等较早发布的模型使用标准RLHF方法,其中的数据管理风格和规模似乎已经过时。近来,Meta、谷歌和英伟达等AI巨头纷纷发布开源模型,附带发布详尽的论文或报告,包括Llama 3.1、Nemotron 340

了解elementUI的底层源码, 进行二次开发

Element UI 是一个基于 Vue.js 的桌面端组件库,广泛用于构建美观、交互友好的用户界面。要深入理解 Element UI 的底层源码并进行二次开发,你需要掌握以下几个关键点: Vue.js 原理 Element UI 是基于 Vue.js 构建的,因此首先需要熟悉 Vue.js 的核心概念和机制,包括: ● 组件系统:Vue.js 的组件化思想,如何定义组件、使用组件、传递属性和事

【JavaScript】在循环体中了解定时器工作机制

for (var i = 0; i < 5; i++) {setTimeout(function() {console.log(i);}, 1000);}console.log(i);   如果我们约定,用箭头表示其前后的两次输出之间有 1 秒的时间间隔,而逗号表示其前后的两次输出之间的时间间隔可以忽略,代码实际运行的结果该如何描述?会有下面两种答案: A. :5 -> 5 -> 5 ->