Scalaz(14)- Monad:函数组合-Kleisli to Reader

简介:

  Monad Reader就是一种函数的组合。在scalaz里函数(function)本身就是Monad,自然也就是Functor和applicative。我们可以用Monadic方法进行函数组合:


import scalaz._
import Scalaz._
object decompose {
//两个测试函数
val f = (_: Int) + 3                              //> f  : Int => Int = <function1>
val g = (_: Int) * 5                              //> g  : Int => Int = <function1>
//functor
val h = f map g  // f andThen g                   //> h  : Int => Int = <function1>
val h1 = g map f  // f compose g                  //> h1  : Int => Int = <function1>
h(2)  //g(f(2))                                   //> res0: Int = 25
h1(2) //f(g(2))                                   //> res1: Int = 13
//applicative
val k = (f |@| g){_ + _}                          //> k  : Int => Int = <function1>
k(10) // f(10)+g(10)                              //> res2: Int = 63
//monad
val m = g.flatMap{a => f.map(b => a+b)}           //> m  : Int => Int = <function1>
val n = for {
  a <- f
  b <- g
} yield a + b                                     //> n  : Int => Int = <function1>
m(10)                                             //> res3: Int = 63
n(10)                                             //> res4: Int = 63
}

 以上的函数f,g必须满足一定的条件才能实现组合。这个从f(g(2))或g(f(2))可以看出:必需固定有一个输入参数及输入参数类型和函数结果类型必需一致,因为一个函数的输出成为另一个函数的输入。在FP里这样的函数组合就是Monadic Reader。 

但是FP里函数运算结果一般都是M[R]这样格式的,所以我们需要对f:A => M[B],g:B => M[C]这样的函数进行组合。这就是scalaz里的Kleisli了。Kleisli就是函数A=>M[B]的类封套,从Kleisli的类定义可以看出:scalaz/Kleisli.scala


1 final case class Kleisli[M[_], A, B](run: A => M[B]) { self =>
2 ...
3 trait KleisliFunctions {
4   /**Construct a Kleisli from a Function1 */
5   def kleisli[M[_], A, B](f: A => M[B]): Kleisli[M, A, B] = Kleisli(f)
6 ...

Kleisli的目的是把Monadic函数组合起来或者更形象说连接起来。Kleisli提供的操作方法如>=>可以这样理解:

(A=>M[B]) >=> (B=>M[C]) >=> (C=>M[D]) 最终运算结果M[D]

可以看出Kleisli函数组合有着固定的模式:

1、函数必需是 A => M[B]这种模式;只有一个输入,结果是一个Monad M[_]

2、上一个函数输出M[B],他的运算值B就是下一个函数的输入。这就要求下一个函数的输入参数类型必需是B

3、M必须是个Monad;这个可以从Kleisli的操作函数实现中看出:scalaz/Kleisli.scala


/** alias for `andThen` */
  def >=>[C](k: Kleisli[M, B, C])(implicit b: Bind[M]): Kleisli[M, A, C] =  kleisli((a: A) => b.bind(this(a))(k.run))

  def andThen[C](k: Kleisli[M, B, C])(implicit b: Bind[M]): Kleisli[M, A, C] = this >=> k

  def >==>[C](k: B => M[C])(implicit b: Bind[M]): Kleisli[M, A, C] = this >=> kleisli(k)

  def andThenK[C](k: B => M[C])(implicit b: Bind[M]): Kleisli[M, A, C] = this >==> k

  /** alias for `compose` */
  def <=<[C](k: Kleisli[M, C, A])(implicit b: Bind[M]): Kleisli[M, C, B] = k >=> this

  def compose[C](k: Kleisli[M, C, A])(implicit b: Bind[M]): Kleisli[M, C, B] = k >=> this

  def <==<[C](k: C => M[A])(implicit b: Bind[M]): Kleisli[M, C, B] = kleisli(k) >=> this

  def composeK[C](k: C => M[A])(implicit b: Bind[M]): Kleisli[M, C, B] = this <==< k

拿操作函数>=>(andThen)举例:implicit b: Bind[M]明确了M必须是个Monad。

kleisli((a: A) => b.bind(this(a))(k.run))的意思是先运算M[A],接着再运算k,以M[A]运算结果值a作为下一个函数k.run的输入参数。整个实现过程并不复杂。

实际上Reader就是Kleisli的一个特殊案例:在这里kleisli的M[]变成了Id[],因为Id[A]=A >>> A=>Id[B] = A=>B,就是我们上面提到的Reader,我们看看Reader在scalaz里是如何定义的:scalar/package.scala


type ReaderT[F[_], E, A] = Kleisli[F, E, A]
  val ReaderT = Kleisli
  type =?>[E, A] = Kleisli[Option, E, A]
  type Reader[E, A] = ReaderT[Id, E, A]

  type Writer[W, A] = WriterT[Id, W, A]
  type Unwriter[W, A] = UnwriterT[Id, W, A]

  object Reader {
    def apply[E, A](f: E => A): Reader[E, A] = Kleisli[Id, E, A](f)
  }

  object Writer {
    def apply[W, A](w: W, a: A): WriterT[Id, W, A] = WriterT[Id, W, A]((w, a))
  }

  object Unwriter {
    def apply[U, A](u: U, a: A): UnwriterT[Id, U, A] = UnwriterT[Id, U, A]((u, a))
  }

type ReaderT[F[_], E, A] = Kleisli[F, E, A] >>> type Reader[E,A] = ReaderT[Id,E,A]

好了,说了半天还是回到如何使用Kleisli进行函数组合的吧:


//Kleisli款式函数kf,kg
val kf: Int => Option[String] = (i: Int) => Some((i + 3).shows)
                                                  //> kf  : Int => Option[String] = <function1>
val kg: String => Option[Boolean] = { case "3" => true.some; case _ => false.some }
                                                  //> kg  : String => Option[Boolean] = <function1>
//Kleisli函数组合操作
import Kleisli._
val kfg = kleisli(kf) >=> kleisli(kg)             //> kfg  : scalaz.Kleisli[Option,Int,Boolean] = Kleisli(<function1>)
kfg(1)                                            //> res5: Option[Boolean] = Some(false)
kfg(0)                                            //> res6: Option[Boolean] = Some(true)

例子虽然很简单,但它说明了很多重点:上一个函数输入的运算值是下一个函数的输入值 Int=>String=>Boolean。输出Monad一致统一,都是Option。

那么,Kleisli到底用来干什么呢?它恰恰显示了FP函数组合的真正意义:把功能尽量细分化,通过各种方式的函数组合实现灵活的函数重复利用。也就是在FP领域里,我们用Kleisli来组合FP函数。

下面我们就用scalaz自带的例子scalaz.example里的KleisliUsage.scala来说明一下Kleisli的具体使用方法吧:

下面是一组地理信息结构:


1   // just some trivial data structure ,
2   // Continents contain countries. Countries contain cities.
3   case class Continent(name: String, countries: List[Country] = List.empty)
4   case class Country(name: String, cities: List[City] = List.empty)
5   case class City(name: String, isCapital: Boolean = false, inhabitants: Int = 20)

分别是:洲(Continent)、国家(Country)、城市(City)。它们之间的关系是层级的:Continent(Country(City))

下面是一组模拟数据:


val data: List[Continent] = List(
    Continent("Europe"),
    Continent("America",
      List(
        Country("USA",
          List(
            City("Washington"), City("New York"))))),
    Continent("Asia",
      List(
        Country("India",
          List(City("New Dehli"), City("Calcutta"))))))

从上面的模拟数据也可以看出Continent,Country,City之间的隶属关系。我们下面设计三个函数分别对Continent,Country,City进行查找:


def continents(name: String): List[Continent] =
    data.filter(k => k.name.contains(name))       //> continents: (name: String)List[Exercises.kli.Continent]
  //查找名字包含A的continent
  continents("A")                                 //> res7: List[Exercises.kli.Continent] = List(Continent(America,List(Country(U
                                                  //| SA,List(City(Washington,false,20), City(New York,false,20))))), Continent(A
                                                  //| sia,List(Country(India,List(City(New Dehli,false,20), City(Calcutta,false,2
                                                  //| 0))))))
  //找到两个:List(America,Asia)
  def countries(continent: Continent): List[Country] = continent.countries
                                                  //> countries: (continent: Exercises.kli.Continent)List[Exercises.kli.Country]
  //查找America下的国家
  val america =
      Continent("America",
      List(
        Country("USA",
          List(
            City("Washington"), City("New York")))))
                                                  //> america  : Exercises.kli.Continent = Continent(America,List(Country(USA,Lis
                                                  //| t(City(Washington,false,20), City(New York,false,20)))))
  countries(america)                              //> res8: List[Exercises.kli.Country] = List(Country(USA,List(City(Washington,f
                                                  //| alse,20), City(New York,false,20))))
  def cities(country: Country): List[City] = country.cities
                                                  //> cities: (country: Exercises.kli.Country)List[Exercises.kli.City]
  val usa = Country("USA",
            List(
              City("Washington"), City("New York")))
                                                  //> usa  : Exercises.kli.Country = Country(USA,List(City(Washington,false,20), 
                                                  //| City(New York,false,20)))
  cities(usa)                                     //> res9: List[Exercises.kli.City] = List(City(Washington,false,20), City(New Y
                                                  //| ork,false,20))

从continents,countries,cities这三个函数运算结果可以看出它们都可以独立运算。这三个函数的款式如下:

String => List[Continent]

Continent => List[Country]

Country => List[City]

无论函数款式或者类封套(List本来就是Monad)都适合Kleisli。我们可以用Kleisli把这三个局部函数用各种方法组合起来实现更广泛功能:


val allCountry = kleisli(continents) >==> countries
                                                  //> allCountry  : scalaz.Kleisli[List,String,Exercises.kli.Country] = Kleisli(<
                                                  //| function1>)
  val allCity = kleisli(continents) >==> countries >==> cities
                                                  //> allCity  : scalaz.Kleisli[List,String,Exercises.kli.City] = Kleisli(<functi
                                                  //| on1>)
  allCountry("Amer")                              //> res10: List[Exercises.kli.Country] = List(Country(USA,List(City(Washington,
                                                  //| false,20), City(New York,false,20))))
  allCity("Amer")                                 //> res11: List[Exercises.kli.City] = List(City(Washington,false,20), City(New 
                                                  //| York,false,20))

还有个=<<符号挺有意思:


1   def =<<(a: M[A])(implicit m: Bind[M]): M[B] = m.bind(a)(run)

意思是用包嵌的函数flatMap一下输入参数M[A]:


1   allCity =<< List("Amer","Asia")                 //> res12: List[Exercises.kli.City] = List(City(Washington,false,20), City(New 
2                                                   //| York,false,20), City(New Dehli,false,20), City(Calcutta,false,20))

那么如果我想避免使用List(),用Option[List]作为函数输出可以吗?Option是个Monad,第一步可以通过。下一步是把函数款式对齐了:

List[String] => Option[List[Continent]]

List[Continent] => Option[List[Country]]

List[Country] => Option[List[City]]

下面是这三个函数的升级版:


//查找Continent List[String] => Option[List[Continent]]
  def maybeContinents(names: List[String]): Option[List[Continent]] =
    names.flatMap(name => data.filter(k => k.name.contains(name))) match {
       case h :: t => (h :: t).some
       case _ => none
    }                                             //> maybeContinents: (names: List[String])Option[List[Exercises.kli.Continent]]
                                                  //| 
  //测试运行
  maybeContinents(List("Amer","Asia"))            //> res13: Option[List[Exercises.kli.Continent]] = Some(List(Continent(America,
                                                  //| List(Country(USA,List(City(Washington,false,20), City(New York,false,20))))
                                                  //| ), Continent(Asia,List(Country(India,List(City(New Dehli,false,20), City(Ca
                                                  //| lcutta,false,20)))))))
  //查找Country  List[Continent] => Option[List[Country]]
  def maybeCountries(continents: List[Continent]): Option[List[Country]] =
    continents.flatMap(continent => continent.countries.map(c => c)) match {
       case h :: t => (h :: t).some
       case _ => none
    }                                             //> maybeCountries: (continents: List[Exercises.kli.Continent])Option[List[Exer
                                                  //| cises.kli.Country]]
   //查找City  List[Country] => Option[List[Country]]
  def maybeCities(countries: List[Country]): Option[List[City]] =
    countries.flatMap(country => country.cities.map(c => c)) match {
       case h :: t => (h :: t).some
       case _ => none
    }                                             //> maybeCities: (countries: List[Exercises.kli.Country])Option[List[Exercises.
                                                  //| kli.City]]
  
  val maybeAllCities = kleisli(maybeContinents) >==> maybeCountries >==> maybeCities
                                                  //> maybeAllCities  : scalaz.Kleisli[Option,List[String],List[Exercises.kli.Cit
                                                  //| y]] = Kleisli(<function1>)
  maybeAllCities(List("Amer","Asia"))             //> res14: Option[List[Exercises.kli.City]] = Some(List(City(Washington,false,2
                                                  //| 0), City(New York,false,20), City(New Dehli,false,20), City(Calcutta,false,
                                                  //| 20)))

我们看到,只要Monad一致,函数输入输出类型匹配,就能用Kleisli来实现函数组合。


相关文章
|
2天前
|
存储 JavaScript 前端开发
JavaScript基础
本节讲解JavaScript基础核心知识:涵盖值类型与引用类型区别、typeof检测类型及局限性、===与==差异及应用场景、内置函数与对象、原型链五规则、属性查找机制、instanceof原理,以及this指向和箭头函数中this的绑定时机。重点突出类型判断、原型继承与this机制,助力深入理解JS面向对象机制。(238字)
|
1天前
|
云安全 人工智能 安全
阿里云2026云上安全健康体检正式开启
新年启程,来为云上环境做一次“深度体检”
1470 6
|
3天前
|
安全 数据可视化 网络安全
安全无小事|阿里云先知众测,为企业筑牢防线
专为企业打造的漏洞信息收集平台
1304 2
|
3天前
|
缓存 算法 关系型数据库
深入浅出分布式 ID 生成方案:从原理到业界主流实现
本文深入探讨分布式ID的生成原理与主流解决方案,解析百度UidGenerator、滴滴TinyID及美团Leaf的核心设计,涵盖Snowflake算法、号段模式与双Buffer优化,助你掌握高并发下全局唯一ID的实现精髓。
319 160
|
3天前
|
人工智能 自然语言处理 API
n8n:流程自动化、智能化利器
流程自动化助你在重复的业务流程中节省时间,可通过自然语言直接创建工作流啦。
365 4
n8n:流程自动化、智能化利器
|
11天前
|
机器学习/深度学习 安全 API
MAI-UI 开源:通用 GUI 智能体基座登顶 SOTA!
MAI-UI是通义实验室推出的全尺寸GUI智能体基座模型,原生集成用户交互、MCP工具调用与端云协同能力。支持跨App操作、模糊语义理解与主动提问澄清,通过大规模在线强化学习实现复杂任务自动化,在出行、办公等高频场景中表现卓越,已登顶ScreenSpot-Pro、MobileWorld等多项SOTA评测。
1476 7
|
5天前
|
人工智能 API 开发工具
Skills比MCP更重要?更省钱的多!Python大佬这观点老金测了一周终于懂了
加我进AI学习群,公众号右下角“联系方式”。文末有老金开源知识库·全免费。本文详解Claude Skills为何比MCP更轻量高效:极简配置、按需加载、省90% token,适合多数场景。MCP仍适用于复杂集成,但日常任务首选Skills。推荐先用SKILL.md解决,再考虑协议。附实测对比与配置建议,助你提升效率,节省精力。关注老金,一起玩转AI工具。
|
1天前
|
Linux 数据库
Linux 环境 Polardb-X 数据库 单机版 rpm 包 安装教程
本文介绍在CentOS 7.9环境下安装PolarDB-X单机版数据库的完整流程,涵盖系统环境准备、本地Yum源配置、RPM包安装、用户与目录初始化、依赖库解决、数据库启动及客户端连接等步骤,助您快速部署运行PolarDB-X。
228 1
Linux 环境 Polardb-X 数据库 单机版 rpm 包 安装教程
|
12天前
|
人工智能 Rust 运维
这个神器让你白嫖ClaudeOpus 4.5,Gemini 3!还能接Claude Code等任意平台
加我进AI讨论学习群,公众号右下角“联系方式”文末有老金的 开源知识库地址·全免费
1342 17
|
3天前
|
自然语言处理 监控 测试技术
互联网大厂“黑话”完全破译指南
互联网大厂黑话太多听不懂?本文整理了一份“保姆级”职场黑话词典,涵盖PRD、A/B测试、WLB、埋点、灰度发布等高频术语,用大白话+生活化类比,帮你快速听懂同事在聊什么。非技术岗也能轻松理解,建议收藏防踩坑。
274 161

热门文章

最新文章