深圳scala-meetup-20180902(2)- Future vs Task and ReaderMonad依赖注入

  在对上一次3月份的scala-meetup里我曾分享了关于Future在函数组合中的问题及如何用Monix.Task来替代。具体分析可以查阅这篇博文。在上篇示范里我们使用了Future来实现某种non-blocking数据库操作,现在可以用Task替换Future部分:

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

Task是一个真正的Monad,我们可以放心的用来实现函数组合:

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

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

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


  def cookSauce(qty: Quantity)(get: (FoodName,Quantity) => Task[Quantity],
                               put: (FoodName,Quantity) => Task[Quantity]): Task[Quantity] = for {
    tomato <- get("Tomato",qty)
    vaggies <- get("Veggies",qty)
    _ <- get("Galic",10)
    sauceQ = tomato/2 + vaggies * 3 / 2
    _ <- put("Sauce",sauceQ)
  } yield sauceQ

  def cookPasta(qty: Quantity)(get: (FoodName,Quantity) => Task[Quantity],
                               put: (FoodName,Quantity) => Task[Quantity]): Task[Quantity] = for {
    pasta <- get("Pasta", qty)
    sauce <- get("Sauce", qty)
    _ <- get("Spice", 3)
    portions = Math.min(pasta, sauce)
    _ <- put("Meal", portions)
  } yield portions

跟上次我们使用Future时的方式没有两样。值得研究的是如何获取Task运算结果,及如何更精确的控制Task运算如取消运行中的Task:

  implicit val refridge = new FoodStore

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

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

 import scala.util._
  import monix.execution.Scheduler.Implicits.global

  val cancellableCooking = Cooking.runOnComplete { result =>
    result match {
      case Success(meals) => println(s"we have $meals pasta meals for the day.")
      case Failure(err) => println(s"cooking trouble: ${err.getMessage}")
    }
  }

  global.scheduleOnce(1 second) {
    println(s"its taking too long, cancelling cooking ...")
    cancellableCooking.cancel()
  }

在上面例子里的addFood,takeFood函数中都有个fs:FoodStore参数。这样做可以使函数更加通用,可以对用不同方式实施的FoodStore进行操作。这里FoodStore就是函数的依赖,我们是通过函数参数来传递这个依赖的。重新组织一下代码使这种关系更明显:

  class Refridge {
    def addFood(food: FoodName, qty: Quantity): FoodStore => Task[Quantity] = { foodStore =>
      for {
        current <- foodStore.read(food)
        newQty = current.map(c => c + qty).getOrElse(qty)
        _ <- foodStore.update(food, newQty)
      } yield newQty
    }

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

  }

现在我们用一个函数类型的结果来代表依赖注入。这样做的好处是简化了函数主体,彻底把依赖与函数进行了分割,使用函数时不必考虑依赖。

scala的函数式组件库cats提供了一个Kleisli类型,reader monad就是从它推导出来的:

 final case class Kleisli[M[_], A, B](run: A => M[B]) { self =>
 ...
 trait KleisliFunctions {
   /**Construct a Kleisli from a Function1 */
   def kleisli[M[_], A, B](f: A => M[B]): Kleisli[M, A, B] = Kleisli(f)
 …
  def >=>[C](k: Kleisli[M, B, C])(implicit b: Bind[M]): Kleisli[M, A, C] =  
          kleisli((a: A) => b.bind(this(a))(k.run))
 …
 Kleisli的用途就是进行函数的转换
 // (A=>M[B]) >=> (B=>M[C]) >=> (C=>M[D]) = M[D]

实际上Kleisli就是ReaderT:

 type ReaderT[F[_], E, A] = Kleisli[F, E, A]
 val ReaderT = Kleisli
 val reader = ReaderT[F,B,A](A => F[B]) 
 val readerTask = ReaderT[Task,B,A](A => Task[B])
 val injection = ReaderT { foodStore => Task.delay { foodStore.takeFood } }
 val food = injection.run(db) // run(kvs), run(dbConfig) …

这段代码里我们也针对上面的例子示范了ReaderT的用法。现在我们可以把例子改成下面这样:

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

  class Refridge {
    def addFood(food: FoodName, qty: Quantity): ReaderT[Task,FoodStore,Quantity] = ReaderT{ foodStore =>
      for {
        current <- foodStore.read(food)
        newQty = current.map(c => c + qty).getOrElse(qty)
        _ <- foodStore.update(food, newQty)
      } yield newQty
    }

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

  }

ReaderT[F[_],E,A]就是ReaderT[Task,FoodStore,Quantity]. FoodStore是注入的依赖,ReaderT.run返回Task:

  val cooking: ReaderT[Task,FoodStore,Quantity] = for {
    _ <- shopping
    sauce <- cooker.cookSauce(10)
    pasta <- cooker.cookPasta(10)
  } yield pasta

  import scala.concurrent.duration._
  import scala.util._
  import monix.execution.Scheduler.Implicits.global
  val timedCooking = cooking.run(foodStore).timeoutTo(1 seconds, Task.raiseError( new RuntimeException(
    "oh no, take too long to cook ...")))
  val cancellableCooking = timedCooking.runOnComplete { result =>
    result match {
      case Success(meals) => println(s"we have $meals specials for the day.")
      case Failure(exception) => println(s"kitchen problem! ${exception.getMessage}")
    }
  }
  global.scheduleOnce(3 seconds) {
    println("3 seconds passed,cancelling ...")
    cancellableCooking.cancel()
  }

我们知道cooking是个ReaderT,用run(foodStore)来注入依赖foodStore。那么如果我们还有一个kvStore或者jdbcDB,mongoDB可以直接用run(kvStore), run(jdbcDB), run(mongoDB) ... 返回的结果都是Task。

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

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏大内老A

Delegate如何进行类型转换?

我们知道对于两个不具有继承关系的两个类型,如果没有为它们定义转换器,两这之间的类型转换是不允许的,Delegate也是如此。但是有时候我们却希望“兼容”的两种D...

2038
来自专栏小勇DW3

Future FutrueTask Callable类源码说明以及原理使用

  JDK内置的Future主要使用到了Callable接口和FutureTask类。

1782
来自专栏菩提树下的杨过

silverlight中如何得到ComboBox的选中值(SelectedValue)?

用惯了Asp.Net中的ComboBox和ListBox,想当然的以为SL/Winform/WPF中的ComboBox也应该有类似SelectedValue的属...

1958
来自专栏张善友的专栏

学习altas笔记[客户端JS和Altas环境初始化关系和DataTable返回数据的客户端处理]

    1、在客户端调用WebService,脚本执行顺序出现了问题(报错了:XXX没有定义!).问题出在Atlas还没有初始化完成.     解决办法Alta...

2098
来自专栏程序员的SOD蜜

不使用反射的实体类方案

看过很多ORM的实体类方案,大多是用反射来读数据库数据,这样当频繁操作实体类的时候效率很低,我借鉴了一位朋友的思路,采用.NET 2.0的泛型技术,为实体类提供...

2178
来自专栏小樱的经验随笔

Codeforces 706B Interesting drink

B. Interesting drink time limit per test:2 seconds memory limit per test:256 meg...

2958
来自专栏木宛城主

SharePoint CAML In Action——Part II

在SharePoint中,相对于Linq to SharePoint而言,CAML是轻量化的。当然缺点也是显而易见的,"Hard Code"有时会让你抓狂。在实...

1955
来自专栏技术博客

编写高质量代码改善C#程序的157个建议[IEnumerable<T>和IQueryable<T>、LINQ避免迭代、LINQ替代迭代]

本文已更新至http://www.cnblogs.com/aehyok/p/3624579.html 。本文主要学习记录以下内容:

1125
来自专栏行者常至

019.数据交换格式:Json、XML

数据交换格式中,最核心的就是Json和XML。 其中,Json是一种轻量级数据交换格式,XML是一种重量级的数据交换格式。 相比于xml这种数据交换格式来说...

1653
来自专栏风口上的猪的文章

.NET面试题系列[14] - LINQ to SQL与IQueryable

"理解IQueryable的最简单方式就是,把它看作一个查询,在执行的时候,将会生成结果序列。" - Jon Skeet

1251

扫码关注云+社区

领取腾讯云代金券