2015/08/14

[ScaVa->Scala] Scalaz Writer Monad

[ScaVa->Scala] Scalaz Writer Monad

[ScaVa->Scala] Scalaz Writer Monad

這篇同步發佈在我的BlogGist

之前一篇 [ScaVa->Scala] Scala中使用Reader Monad來實現Dependency Injection ,裡面提到了Scalaz中的Reader Monad,有Reader當然也要有個Writer呀!!

使用情境

Writer Monad在scalaz的source code裡的註解是Computations that log a value
假設今天有一個需求,是要進行一個有多個步驟的處理,而這個處理的過程我們需要Log起來,也就是在每個步驟加上對應的記錄,並且在處理完成之後,要能把過程印在畫面上,在許多個步驟在處理時,這些Log要怎麼收集呢?

或許可以寫File或是塞DB啊!要能同時處理多個Request, 就是給不同的id產生不同的file,然後把log都寫進去,最後完成時,再把檔案讀出來(或是查db)回傳出去。但這樣寫File的I/O其實很浪費,用DB的話更浪費了。所以如果資料量小小的,我們存在記憶體裡就好了,但是在這種implement上,怎麼寫比較方便呢?其實用tuple可以做到(第一個放這個步驟處理完真正的回傳值,第二個放對應的描述log),但是每個步驟都要把結果串在一起,寫起來很辛苦,這時候就可以用上Writer Monad。

Writer Monad

Writer其實是WriterT的別名(alias),並且指定WriterT中的第一個型態scalaz.Id是scalaz的Identity Monad

Writer

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

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

WriterT這邊主要裡面有個run,他就是幫你把這兩個值(一個是你原本的值,另一個是你要寫的log)包在一起成tuple,然後放進Identity Monad,另外就是written和value了,分別是你所write的log,和本來要回傳的value。

WriterT

sealed trait WriterT[F[+_], +W, +A] { self =>
  val run: F[(W, A)]

  def written(implicit F: Functor[F]): F[W] =
    F.map(run)(_._1)
  def value(implicit F: Functor[F]): F[A] =
    F.map(run)(_._2)
}

舉個例子

我有四個動作:
1. 用名字查出User的Id
2. 用Id查這個User的State
3. 用Id再查User的Age
4. 最後做個Foo的Check

def searchUserId(name:String)=123.set(Vector(s"search user with name:[$name] found id:[123]"))

def getState(id:Int)="alive".set(Vector("state is alive"))

def getAge(id:Int)=18.set(Vector("age is 18"))

def checkFoo(state:String,age:Int)=(age>=18)?"Old Foo".set(Vector(s"age older then 18 is Old Foo."))|"Young Foo".set(Vector(s"age younger than 18 is Young Foo."))

val foo = for{
            uid <- searchUserId("Joe")
            state <- getState(uid)
            age <- getAge(uid)
            foo <- checkFoo(state,age)
          } yield foo
foo: scalaz.WriterT[scalaz.Id.Id,scala.collection.immutable.Vector[String],String] = WriterT((Vector(search user with name:[Joe] found id:[123], state is alive, age is 18, age older then 18 is Old Foo.),Old Foo))

foo.value
scalaz.Id.Id[String] = Old Foo

foo.written
scalaz.Id.Id[scala.collection.immutable.Vector[String]] = Vector(search user with name:[Joe] found id:[123], state is alive, age is 18, age older then 18 is Old Foo.)

可以看到,因為是monad,所以我這邊透過for comprehension來把這些動作串起來,最後產生了foo,而他的value就是最後處理完成的結果,而written就是我每個過程中附加上來的log,是不是簡單好用呢?

有關效能的注意事項

當你要把一個Monoid(在我們的例子用的Monoid是Vector)放在Writer Monad裡的時候,要注意performance的考量。Learning-Scalaz的文章裡有做了個測試,若是用List的話,花的時間會是Vector的兩倍哦!

Reference
- Learning-Scalaz Writer
- Learning-Scalaz Id

張貼留言

My World