深圳scala-meetup-20180902(1)- Monadic 编程风格

刚完成了9月份深圳scala-meetup,趁刮台风有空,把我在meetup里的分享在这里发表一下。我这次的分享主要分三个主题:“Monadic编程风格“、”Future vs Task and ReaderMonad应用方法“及”using heterogeneous monads in for-comprehension with MonadTransformer“。这篇想先介绍一下Monadic编程风格。

Monadic编程就是用Monad来编程,它的形式是:F[G],F是个Monad,然后G是具体的运算,G就是我们习惯的运算表达式如1+1、update('a','new content')等等,可能会产生副作用的,比如输入输出,更改数据等。形象点描述:如果我们把F[_]当作是一个管道,那么Monadic编程模式就像是在F这个管道里组装连接一些可能产生副作用的运算表达式。实际上真正产生运算结果的是管道内部的这些运算表达式。这是疯了吗?我们为什么不直接按序运算这些表达式来获取结果呢?我们先听听下面的分析:

看看下面这段程序:

行令编程模式(imperative programming)
def au(t:T): T      async update with result
val t2 = au(t1)
val t3 = au(t2)
val t4 = au(t2 + t3)         t4 = ???

如果上面每一行指令都在不同的线程里运算,那么完成运算的顺序就是不确定的。最后t4的结果是不可预料的了。为了保证这个运算顺序,我们可能要使用锁,这又回到在OO编程里最棘手的问题:运行低效、死锁、难以理解跟踪等。基本上OO编程的多线程程序不但难以理解而且运算难以捉摸,结果难以预览,很难做的对。我们再看看Monadic编程:

monadic programming : program with monads
val fp3 = F[p1] ⊕ F[p1] ⊕ F[p1] = F[p1+p2+p3] 
1、延迟运算 :val res = fp3.run
2、按序运算 :flatMap{a => flatMap{b => flatMap{c =>… 

我们看到:所谓的Monadic编程就是在F[_]管道内运算式p1,p2,p3的连接。这样做可以达到延迟运算和按序运算两个主要目的。延迟运算可以让我们完成对所有运算表达式的组合再一次性进行完整的运算。按序运算可以保证运算是按照编程人员的意图进行的,这里的flatMap是一种函数链,运算得到a后再运算b,得到b后再继续运算c 。。。

下面是我们自创的一个F[_]结构Tube[A]和它的使用示范:

 case class Tube[A](run: A) {
    def map[B](f: A => B): Tube[B] = Tube(f(run))
    def flatMap[B](f: A => Tube[B]): Tube[B] = f(run)
  }

  val value: Tube[Int] = Tube(10)
  def add(a: Int, b: Int): Tube[Int] = Tube(a+b)

  val f = for {
    a <- value
    b <- add(a , 3)
    c <- add(a,b)
  } yield c

  println(f)          //Tube(23)
  println(f.run)      //23

首先,Tube[A]是个Monad,因为它支持map和flatMap。对任何Tube类型我们都可以用for-comprehension来组合运算式,最后run来获取运算结果。以上a,b,c都是中间结果,可以在for{...}中任意使用。

值得注意的是:Monadic操作与scala里集合的操作很相似,不同的是Monadic操作类型只包含一个内部元素,而集合包含了多个元素,如List(1,2,3)有3个元素。

实际上,简单的一个Tube结构好像没什么特别用处,说白了它连中途终止运算的功能都没有。scala库里现成的Monad中Option,Either都有特别的作用:Option可以在遇到None值时中断运算并立即返回None值。Either在遇到Left值时立即返回Left,如下:

  val value: Option[Int] = Some(10)
  def add(a: Int, b: Int): Option[Int] = Some(a+b)

  val p = for {
    a <- value
    b <- add(a, 3)
    _ <- None
    c <- add(a,b)
  } yield a

  println(p)     //None


  val value: Either[String,Int] = Right(10)
  def add(a: Int, b: Int): Either[String,Int] = Right(a+b)

  val p = for {
    a <- value
    b <- add(a, 3)
    _ <- Left("oh no ...")
    c <- add(a,b)
  } yield c

  println(p)  //oh no ...

好了,下面我们就用一个形象点的例子来示范Monadic编程风格:这是一个模拟数据库操作的例子,我们用一个KVStore来模拟数据库:

  class KVStore[K,V] {
    private val s = new ConcurrentHashMap[K,V]()
    def create(k: K, v: V): Future[Boolean] = Future.successful(s.putIfAbsent(k,v) == null)
    def read(k: K): Future[Option[V]] = Future.successful(Option(s.get(k)))
    def update(k: K, v: V): Future[Unit] = Future.successful(s.put(k,v))
    def delete(k: K): Future[Boolean] = Future.successful(s.remove(k) == null)
  }

对KVStore的操作函数都采用了Future作为结果类型,这样可以实现non-blocking操作。Future是个Monad(虽然它不是一种纯函数impure function, 这个我们后面再解释),所以我们可以用for-comprehension来编程,如下:

 type FoodName = String
  type Quantity = Int
  type FoodStore = KVStore[String,Int]

  def addFood(food: FoodName, qty: Quantity )(implicit fs: FoodStore): Future[Unit] = for {
    current <- fs.read(food)
    newQty = current.map(cq => cq + qty ).getOrElse(qty)
    _ <-  fs.update(food, newQty)
  } yield ()

  def takeFood(food: FoodName, qty: Quantity)(implicit fs: FoodStore): Future[Quantity] = for {
    current <- fs.read(food)
    instock = current.getOrElse(0)
    taken = Math.min(instock,qty)
    left = instock - taken
    _ <- if (left > 0) fs.update(food,left) else fs.delete(food)
  } yield taken

  def cookSauce(qty: Quantity)(get: (FoodName,Quantity) => Future[Quantity],
                               put:(FoodName,Quantity) => Future[Unit]): Future[Quantity] = for {
    tomato <- get("Tomato",qty)
    veggie <- get("Veggie",qty)
    garlic <- get("Garlic", qty * 3)
    sauceQ = tomato / 2 + veggie * 3 / 2
    _ <- put("Sauce",sauceQ)
  } yield sauceQ

  def cookMeals(qty: Quantity)(get: (FoodName,Quantity) => Future[Quantity],
                               put: (FoodName,Quantity) => Future[Unit]): Future[Quantity] =
    for {
       pasta <- get("Pasta", qty)
       sauce <- get("Sauce", qty)
      _ <- get("Spice",10)

      meals = Math.min(pasta,sauce)
      _ <- put("Meal", meals)

    } yield meals

上面几个操作函数都是Future类型的,具体的操作都包含在for{...}里。我们看到:在for{...}里可以产生中间结果、也可以直接写运算表达式、也可以使用这些中间运算结果。for{...}里的情景就像正常的行令式编程。然后我们又对这些操作函数进行组合:

   implicit val refrigerator = new FoodStore

   val shopping: Future[Unit] = for {
     _ <- addFood("Tomato", 10)
     _ <- addFood("Veggie", 15)
     _ <- addFood("Garlic", 42)
     _ <- addFood("Spice", 100)
     _ <- addFood("Pasta", 6)
   } yield ()

   val cooking: Future[Quantity] = for {
     _ <- shopping
     sauce <- cookSauce(10)(takeFood(_,_),addFood(_,_))
     meals <- cookMeals(10)(takeFood(_,_),addFood(_,_))
   } yield (meals)

   val todaysMeals = Await.result(cooking,3 seconds)

  println(s"we have $todaysMeals pasta meals for the day.")

最后组合成这个cooking monad, 然后一次性Await.result(cooking...)获取最终结果。通过上面这个例子我们可以得到这么一种对Monadic编程风格的感觉,就是:用for-comprehension来组合,组合、再组合,然后run(Await.result)获取结果。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏java达人

HashMap庖丁解牛

感谢erixhao的作品,长文需细品: Code Walkthrough是我们新的一个系列,主要以阅读,分析源代码为主要目的,特此介绍一下。我们先以最经典的JD...

2159
来自专栏互联网杂技

基础JavaScript装逼指南

本文秉承着 你看不懂是你sb,我写的代码就要牛逼 的理念来介绍一些js的装逼技巧。 下面的技巧,后三个,请谨慎用于团队项目中(主要考虑到可读性的问题),不然,l...

3455
来自专栏工科狗和生物喵

【我的漫漫跨考路】数据结构·队列的链表实现

? 正文之前 今天看无穷级数这个数学内容实在看得头疼,索性看到八点多就不看了。愉快的写起了码,对我来说这个可有趣了!虽然有时候莫名其妙的就会Run succe...

2885
来自专栏C语言及其他语言

第一个 C 语言编译器是怎样编写的?

作者: 伯乐在线 - Chaobs 网址: http://blog.jobbole.com/94311/ 首先向C语言之父Dennis Ritchie致敬! ...

4229
来自专栏玄魂工作室

Python黑帽编程2.1 Python编程哲学

本节的内容有些趣味性,涉及到很多人为什么会选择Python,为什么会喜欢这门语言。我带大家膜拜下Python作者的Python之禅,然后再来了解下Python的...

3157
来自专栏数说工作室

2. PRXPARSE () | 正则表达式的“阿赖耶识”

阿赖耶识...为宇宙万有之本,含藏万有,使之存而不失,故称藏识。又因其能含藏生长万有之种子,故亦称种子识。 ——《佛光大辞典》 佛家说人有九识,除眼、耳、鼻、...

3566
来自专栏数说工作室

正则表达式的“阿赖耶识”| 【SAS Says·扩展篇】正则表达式

阿赖耶识...为宇宙万有之本,含藏万有,使之存而不失,故称藏识。又因其能含藏生长万有之种子,故亦称种子识。 ——《佛光大辞典》 佛家说人有九识,除眼、耳、鼻、舌...

3403
来自专栏HansBug's Lab

算法模板——并查集 2(支持快速即时查询本连通块内容,纯原创!)

实现功能:输入N,现在有N个数;接下来输入任意行,如果是"1 x y"则表示把x和y所在的块合并;如果是"2 x"则表示输出x所在的块的全部内容 原理:其实主要...

3166
来自专栏互扯程序

Java8函数式编程实践精华

现在是资源共享的时代,同样也是知识分享的时代,如果你觉得本文能学到知识,请把知识与别人分享。

1273
来自专栏xingoo, 一个梦想做发明家的程序员

Java程序员的日常—— 基于类的策略模式、List<?>与List、泛型编译警告、同比和环比

早晨起得太早,昨晚睡得太晚,一天都迷迷糊糊的。中午虽然睡了半个小时,可是依然没有缓过来。整个下午都在混沌中....不过今天下载了一款手游——《剑侠情缘》,感觉...

1897

扫码关注云+社区

领取腾讯云代金券