Monads are Elephants(4) 翻譯

前言

要問我為什么不從頭開始翻譯而偏偏從第四部分開始的話递雀,是因為前三部分已經(jīng)有大神翻過了。而原文我也是從那里提供的鏈接跳過去的瞬矩,所以就接著開始讀第四部分了徒欣。原文和前三部分鏈接在這里攒磨。

原文  
http://james-iry.blogspot.com/search/label/monads
前面部分的翻譯
http://hongjiang.info/monads-are-elephants-part1-chinese
http://hongjiang.info/monads-are-elephants-part2-chinese
http://hongjiang.info/monads-are-elephants-part3-chinese

渣翻泳桦,輕噴。

Monads are Elephants Part 4

在你第一次摸到成年大象之前娩缰,你可不知道大象到底能有多大灸撰。我在這系列文章中把Monads比作大象,可是從開始直到現(xiàn)在我僅僅展示了一些諸如List, Option之類的嬰兒級別的大象呢拼坎。不過現(xiàn)在是時候了浮毯,讓我們來一起看下到底這頭成年巨獸是什么樣子的。有趣的是泰鸡,他甚至會表演一點雜技給你看亲轨。

Functional Programming and IO

函數(shù)式編程中有一個常見的概念,叫做引用透明鸟顺。引用透明意味著不論你什么時候在哪里調(diào)用一個特定的函數(shù)惦蚊,只要調(diào)用的參數(shù)一致,返回的結(jié)果就一定會一樣讯嫂。顯然的蹦锋,引用透明的程序相比擁有一大堆狀態(tài)的代碼來說更加容易使用和調(diào)試。

但是有一件事情貌似對引用透明來說是不可能的:IO欧芽。想象下莉掂,當用戶把自己吃的早餐作為字符串輸入到命令行的時候,每個readLine函數(shù)顯然會返回不同的字符串千扔。類似的道理憎妙,發(fā)送網(wǎng)絡包的函數(shù)也同時有成功和失敗的可能。

但我們并不能為了程序的引用透明性而放棄使用IO曲楚。一個沒有IO交互的程序充其量也只是別出心裁的消耗CPU的計算資源而已厘唾。

你可能猜到了,既然這個系列文章都在談論Monads龙誊,那他應該能夠提供一個解決方案抚垃。的確是這樣,我接下來會從一些基本的概念開始介紹趟大。雖然我只會用命令行讀取和打印的例子來演示如何解決這個問題鹤树。但你顯然可以舉一反三的把這個方法用于任何其他的例如網(wǎng)絡通信和文件讀寫的IO。

當然啦逊朽,你可能不覺得引用透明的IO在Scala中是不可缺少的罕伯。我也并不是在這里試圖鼓吹任何純凈函數(shù)式引用透明的真理。我在這里僅僅是希望討論Monads而事實上恰巧IO Monads能夠非常形象深刻的幫助你理解其他Monads是怎么工作的叽讳。

The World In a Cup

從命令行讀取字符串之所以不是引用透明的追他,是因為readLine函數(shù)的返回值是由用戶的狀態(tài)決定的熊昌,而用戶卻并不是調(diào)用readLine函數(shù)的參數(shù)。同樣一個讀取文件的函數(shù)讀到的內(nèi)容取決于文件系統(tǒng)的狀態(tài)湿酸。讀取指定web頁面的函數(shù)則會收到目標web服務器婿屹,因特網(wǎng),甚至本地網(wǎng)絡的狀態(tài)推溃。同樣昂利,輸出型的IO函數(shù)也會有類似的依賴關系。

所有這些都可以被歸囊括在一個我們創(chuàng)建的叫做WorldState的類然后把它作為所有IO函數(shù)的輸入和返回參數(shù)铁坎。不幸的是蜂奸,這個世界貌似太大啦。我的第一次嘗試最終以因內(nèi)存耗盡而導致的編譯器崩潰而失敗告終硬萍。因此我決定選擇一個和模擬整個世界相比不那么龐大的方法扩所。在這里我們會需要引入一點馬戲團魔術。

這個技巧就是僅僅用我們需要的那部分構造出整個世界朴乖,就是然后假裝世界會對其他部分了若指掌祖屏。以下有一些我們需要考慮到的。

  • 世界的狀態(tài)在IO函數(shù)之間變化买羞。
  • 整個世界的狀態(tài)是一個單例袁勺,你不能因為需要就隨時隨地的使用new關鍵詞來創(chuàng)建新的世界。
  • 整個世界在任何一個時刻都只會處在一個特定的狀態(tài)畜普。

第三點有一點微妙所以我們現(xiàn)在考慮前兩點來看看期丰。

針對第一點,這里有一個粗糙的版本吃挑。

//file RTConsole.scala  
object RTConsole_v1 {  
  def getString(state: WorldState) =   
    (state.nextState, Console.readLine)  
  def putString(state: WorldState, s: String) =   
    (state.nextState, Console.print(s) )  
}  

getString和putString函數(shù)直接使用了在scala.Console中定義的原生函數(shù)钝荡。并且他們將世界的狀態(tài)傳入,然后將一個包含了世界狀態(tài)和原生IO函數(shù)返回結(jié)果的元組傳出舶衬。

接著我們來看第二點怎么實現(xiàn)埠通。

//file RTIO.scala  
sealed trait WorldState{def nextState:WorldState}  
  
abstract class IOApplication_v1 {  
  private class WorldStateImpl(id:BigInt)   
      extends WorldState {  
    def nextState = new WorldStateImpl(id + 1)  
  }  
  final def main(args:Array[String]):Unit = {  
    iomain(args, new WorldStateImpl(0))  
  }  
  def iomain(  
      args:Array[String],   
      startState:WorldState):(WorldState, _)  
}  

我們將WorldState定義成一個密封的特質(zhì),因此它只能夠在同一個源代碼文件中被繼承约炎。然后我們在IOApplication中植阴,以private的方式蟹瘾,定義了WorldState得唯一實現(xiàn)所以現(xiàn)在沒有其他人可以繼承它了圾浅。IOApplication還定義了一個不可被覆蓋的main方法并且在里面調(diào)用了另一個需要在子類中實現(xiàn)的抽象方法iomain。注意我們在這里做的一切都是為了阻止那些使用IO庫的程序員們直接訪問到WorldState憾朴。

有了上面的鋪墊狸捕,我們來一個HelloWorld的例子。

// file HelloWorld.scala  
class HelloWorld_v1 extends IOApplication_v1 {  
  import RTConsole_v1._  
  def iomain(  
        args:Array[String],   
        startState:WorldState) =   
    putString(startState, "Hello world")  
}  

That Darn Property 3

之前我們曾經(jīng)提出過要求众雷,整個世界在任何一個時刻都只會處在一個特定的狀態(tài)灸拍∽鲎#可惜,這點至今我尚未能解決鸡岗,我們來看看這是為什么呢混槐。

class Evil_v1 extends IOApplication_v1 {  
  import RTConsole_v1._  
  def iomain(  
      args:Array[String],   
      startState:WorldState) = {  
    val (stateA, a) = getString(startState)  
    val (stateB, b) = getString(startState)  
    assert(a == b)  
    (startState, b)  
  }  
}  

在這里,我連續(xù)兩次調(diào)用getString函數(shù)并且他們的輸入?yún)?shù)一致轩性。如果我們的代碼是引用透明的話声登,那么兩次函數(shù)的返回值a和b,肯定是一致的揣苏。然后這顯然并不可能悯嗓,因為這兩個返回值完全取決于用戶分別兩次輸入命令行的內(nèi)容。這里的問題是卸察,在程序的執(zhí)行的過程中的任一時刻脯厨,startState和其他的狀態(tài)stateA,stateB一樣坑质,對程序員來說都是可見的合武。

Inside Out

作為解決這個問題的第一步,我決定把整個解決方案徹底推翻涡扼。與之前通過iomain函數(shù)來將WorldState進行轉(zhuǎn)化的方式不同眯杏,這次的iomain將會直接返回這樣一個供main方法調(diào)用的函數(shù)。代碼是這樣的壳澳。

//file RTConsole.scala  
object RTConsole_v2 {  
  def getString = {state:WorldState =>   
    (state.nextState, Console.readLine)}  
  def putString(s: String) = {state: WorldState =>   
    (state.nextState, Console.print(s))}  
}  

getString和putString函數(shù)不再直接對參數(shù)進行操作 - 這次他們將會返回一個新的等待WorldState傳入然后才被執(zhí)行的函數(shù)岂贩。

//file RTIO.scala  
sealed trait WorldState{def nextState:WorldState}  
  
abstract class IOApplication_v2 {  
  private class WorldStateImpl(id:BigInt)   
      extends WorldState {  
    def nextState = new WorldStateImpl(id + 1)  
  }  
  final def main(args:Array[String]):Unit = {  
    val ioAction = iomain(args)  
    ioAction(new WorldStateImpl(0));  
  }  
  def iomain(args:Array[String]):  
    WorldState => (WorldState, _)  
}  

現(xiàn)在IOApplication的main方法會調(diào)用iomain函數(shù)來獲得它接下來將會執(zhí)行的函數(shù),接下來它將一個最初的世界狀態(tài)傳遞給這個函數(shù)從執(zhí)行巷波。我們的HelloWorld看上去并沒什么改變萎津,除了他不再需要WorldState對象了。

//file HelloWorld.scala  
class HelloWorld_v2 extends IOApplication_v2 {  
  import RTConsole_v2._  
  def iomain(args:Array[String]) =   
    putString("Hello world")  
}  

起初看來抹镊,我們好像已經(jīng)解決這個問題了锉屈,因為WorldState不在出現(xiàn)在我們的程序中了。但事實是怎樣的呢垮耳,讓我們接著看下去颈渊。

Oh That Darn Property 3

class Evil_v2 extends IOApplication_v2 {  
  import RTConsole_v2._  
  def iomain(args:Array[String]) = {          
    {startState:WorldState =>  
      val (statea, a) = getString(startState)  
      val (stateb, b) = getString(startState)  
      assert(a == b)  
      (startState, b)  
    }  
  }  
}  

但是只要我們?nèi)绶ㄅ谥疲湍芡ㄟ^一個貌似完全符合規(guī)范的函數(shù)讓我們之前建立的規(guī)則再度悲劇了终佛】∷裕看來只要程序員不受到限制而能夠隨意創(chuàng)建IO函數(shù),那么他就能夠看穿這個WorldState把戲铃彰。

Property 3 Squashed For Good

看上去我們需要防止程序員隨意的創(chuàng)建簽名符合要求的函數(shù)绍豁。恩。牙捉。竹揍。那現(xiàn)在該怎么做呢敬飒?

顯而易見,在處理WorldState時我們已經(jīng)能夠做到防止程序員來實現(xiàn)它的子類了芬位。所以接下來讓我們把我們的整個函數(shù)也變成特質(zhì)的形式无拗。

sealed trait IOAction[+A] extends   
  Function1[WorldState, (WorldState, A)]   
  
private class SimpleAction[+A](  
   expression: => A) extends IOAction[A]...  

與WorldState不同,我們是需要創(chuàng)建IOAction的實例的昧碉。舉例來說蓝纲,我們接下來在另一個文件中將會定義的getString和putString函數(shù)就需要創(chuàng)建新的IOAction。我們這樣做僅僅是讓他們更加安全晌纫。這會讓你有點摸不著頭腦税迷,直到意識到我們現(xiàn)在的getString和putString函數(shù)都具有兩個不相干的部分組成:一部分調(diào)用原生IO,另一部分則將世界轉(zhuǎn)化到下一個狀態(tài)锹漱。讓我們借助一點工廠方法來讓代碼變得更清楚箭养。

//file RTIO.scala  
sealed trait IOAction_v3[+A] extends   
  Function1[WorldState, (WorldState, A)]   
  
object IOAction_v3 {  
  def apply[A](expression: => A):IOAction_v3[A] =   
    new SimpleAction(expression)  
  
  private class SimpleAction [+A](  
      expression: => A) extends IOAction_v3[A] {  
    def apply(state:WorldState) =   
      (state.nextState, expression)  
  }  
}  
  
sealed trait WorldState{def nextState:WorldState}  
  
abstract class IOApplication_v3 {  
  private class WorldStateImpl(id:BigInt)   
      extends WorldState {  
    def nextState = new WorldStateImpl(id + 1)  
  }  
  final def main(args:Array[String]):Unit = {  
    val ioAction = iomain(args)  
    ioAction(new WorldStateImpl(0));  
  }  
  def iomain(args:Array[String]):IOAction_v3[_]  
}  

IOAction對象是一個創(chuàng)建SimpleAction的工廠而已,通過使用“=> A”的注解哥牍,我們讓它的構造函數(shù)需要傳入一個會被延遲計算的表達式作為參數(shù)毕泌。這個表達式不會被計算直到SimpleAction的apply方法被調(diào)用。而為了調(diào)用這個apply方法嗅辣,一個WorldState需要被傳入撼泛。而返回的結(jié)果將是一個包含了新的WorldState和表達式結(jié)果的元組。

現(xiàn)在我們的IO方法看起來是這樣子的澡谭。

//file RTConsole.scala  
object RTConsole_v3 {  
  def getString = IOAction_v3(Console.readLine)  
  def putString(s: String) =   
    IOAction_v3(Console.print(s))  
}  

最終我們的HelloWorld還和之前保持一樣愿题。

class HelloWorld_v3 extends IOApplication_v3 {  
  import RTConsole_v3._  
  def iomain(args:Array[String]) =   
    putString("Hello world")  
}  

現(xiàn)在我們似乎可以保證悲劇不再發(fā)生了。程序員不再能夠接觸到WorldState了蛙奖。它被完全的密封起來了潘酗,main方法現(xiàn)在僅僅是傳第一個WorldState給IOAction的apply方法,而我們沒法通過定制IOAction子類的apply方法的方式來進行隨意的IO操作了雁仲。

不幸的是仔夺,我們遇到了一點組合的問題。我們沒法將多個IO操作進行組合了攒砖。因此我們不再能夠簡單的進行這樣的對話了缸兔。“你叫什么名字吹艇?”惰蜜,Bob, “你好Bob∑海”

額蝎抽。IO操作是一個表達式的容器而Monads是容器。IO操作需要被組合而Monads是能夠被組合的路克。也許樟结。。精算。讓我們來看下瓢宦。

Ladies and Gentleman I Present the Mighty IO Monad

我們給IOAction的apply方法傳入一個類型為A的參數(shù),然后返回一個IOAction[A]的對象灰羽。這看起來很像“unit”驮履。雖然它不是,但是對于現(xiàn)在來說廉嚼,它已經(jīng)足夠像了玫镐。接下來只要我們知道對于這樣的Monad我們的flatMap是什么,Monad法則就能告訴我們它對應的map函數(shù)應該是怎么樣的怠噪。但我們需要怎樣的flatMap呢恐似。它的函數(shù)簽名應該是這樣的

def flatMap[B](f: A=>IOAction[B]):IOAction[B]。

所以這有什么用傍念?

在這里我們希望它能夠?qū)⒁粋€操作鏈接到另一個返回IOAction的函數(shù)矫夷,并且在這個函數(shù)激活的時候能夠順序的調(diào)用這兩個操作。換句話說憋槐,getString.flatMap{y => putString(y)}應該會返回一個IOAction的Monad双藕,在執(zhí)行的時候能夠首先調(diào)用getString操作然后執(zhí)行putString函數(shù)所返回的操作。試試看阳仔。

//file RTIO.scala  
sealed abstract class IOAction_v4[+A] extends   
    Function1[WorldState, (WorldState, A)] {  
  def map[B](f:A => B):IOAction_v4[B] =   
    flatMap {x => IOAction_v4(f(x))}    
  def flatMap[B](f:A => IOAction_v4[B]):IOAction_v4[B]=   
    new ChainedAction(this, f)  
    
  private class ChainedAction[+A, B](  
      action1: IOAction_v4[B],   
      f: B => IOAction_v4[A]) extends IOAction_v4[A] {  
    def apply(state1:WorldState) = {  
      val (state2, intermediateResult) =   
        action1(state1);  
      val action2 = f(intermediateResult)  
      action2(state2)  
    }  
  }    
}  
  
object IOAction_v4 {  
  def apply[A](expression: => A):IOAction_v4[A] =   
    new SimpleAction(expression)  
  
  private class SimpleAction[+A](expression: => A)   
      extends IOAction_v4[A] {  
    def apply(state:WorldState) =   
      (state.nextState, expression)  
  }      
}  
  
// the rest remains the same  
sealed trait WorldState{def nextState:WorldState}  
  
abstract class IOApplication_v4 {  
  private class WorldStateImpl(id:BigInt) ...  

IOAction的工廠和SimpleAction依然保持不變忧陪。IOAction類現(xiàn)在擁有了我們的Monad方法。按照Monad法則近范,map的行為由flatMap和我們選取的unit決定赤嚼。看樣子顺又,對于我們的新IO行為ChainedAction來說更卒,flatMap的實現(xiàn)是我們新方案的關鍵。

我們來看下ChainedAction的apply方法稚照。首先它將最初的世界狀態(tài)傳遞給action1來執(zhí)行蹂空,得到了第二個世界狀態(tài)以及一個中間的執(zhí)行結(jié)果。它鏈接到的函數(shù)會需要這個執(zhí)行結(jié)果并且由這個函數(shù)產(chǎn)生另一個操作:action2果录。action2通過第二個世界狀態(tài)調(diào)用并且最終講包含了所有結(jié)果的元組返回出來上枕。記住,所有這一切都是延遲執(zhí)行的弱恒,直到我們的main方法將最初的世界狀態(tài)傳遞進來才會執(zhí)行辨萍。

A Test Drive

你也許會有疑問,getString和putString函數(shù)既然僅僅是返回對應的IO動作而不是真正執(zhí)行他們的話,為什么不干脆就直接叫做createGetStringAction和createPutStringAction呢锈玉。想要知道原因的話爪飘,我們來接著看看當把它們和我們的老朋友“for”糅雜之后會發(fā)生些什么呢。

object HelloWorld_v4 extends IOApplication_v4 {  
  import RTConsole_v4._  
  def iomain(args:Array[String]) = {  
    for{  
        _ <- putString(  
            "This is an example of the IO monad.");  
        _ <- putString("What's your name?");  
        name <- getString;  
        _ <- putString("Hello " + name)  
    } yield ()  
  }  
}  

看上去好像“for”和getString/putString一起組成了一種新的表達復雜IO行為的迷你語言拉背。

Take a Deep Breath

現(xiàn)在讓我們來總結(jié)一下师崎。IOApplication建立了一個純粹的體系。用戶創(chuàng)建它的子類并且實現(xiàn)一個會被main方法調(diào)用的叫做iomain的方法椅棺。而這個方法會返回一個IOAction犁罩,它可能是一個單獨的IO操作,也可能是一組鏈接起來的操作两疚。這個操作不會立刻執(zhí)行直到有人傳遞給它一個WorldState對象床估。這里ChainedAction類保證了WorldState在每個動作之間的變化和傳遞。

getString和putString并不像它們名字宣稱的那樣能夠傳入或輸出字符串诱渤。事實上丐巫,它們返回IOActions。但是作為Monad源哩,IOAction能夠被嵌入for表達式鞋吉,從而讓它們看起來像是真的做了它們號稱做的事情。

這是一個好的開始励烦,我們已經(jīng)幾乎完成了一個完美的Monad谓着。但這里還有兩個問題。首先坛掠,因為unit會改變整個世界的狀態(tài)赊锚,所以我們似乎稍微打破了一點Monad法則(e.g. m flatMap unit === m)。不過這點問題不大屉栓,因為在這里這個變化是不可見的舷蒲。不過我們最好還是想辦法修復它。

第二個問題則是友多。眾所周知牲平,IO是有失敗的可能的,而我們目前還沒有考慮怎么樣去處理它們域滥。

IO Errors

對Monad來說纵柿,失敗是用Zero來表示的。因此我們只要將這里的失敗的定義(異常)對應到我們Monad的概念中就可以了启绰。這里我要換一個套路:我會給出一個最終版的程序昂儒,我會在期間對它們一一說明來幫助你理解。

IOAction對象保持了帶有若干個工廠方法和私有實現(xiàn)類的方便的模塊的形式(也許把它們寫成匿名類更好委可,不過帶有名字的話比較便于我進行說明)渊跋。SimpleAction保持原樣而IOAction的apply方法是它的工廠方法。

//file RTIO.scala  
object IOAction {  
  private class SimpleAction[+A](expression: => A)   
      extends IOAction[A] {  
    def apply(state:WorldState) =   
      (state.nextState, expression)  
  }  
  
  def apply[A](expression: => A):IOAction[A] =   
    new SimpleAction(expression)  

UnitAction是“unit”的操作 - 一個返回特定值但不改變世界狀態(tài)的操作。unit方法是它的一個工廠方法拾酝。把它從SimpleAction那里分離開來看上去有點奇怪燕少,不過我們最好還是養(yǎng)成良好的習慣,按照Monad的規(guī)則來處理它們微宝。

private class UnitAction[+A](value: A)   
    extends IOAction[A] {  
  def apply(state:WorldState) =   
    (state, value)  
}  
  
def unit[A](value:A):IOAction[A] =   
  new UnitAction(value)  

FailureAction是我們用來表示Zero的類棺亭。這是一個總是會拋出異常的IO操作虎眨。我們自定義的UserException是從其中拋出的異常之一蟋软。這里的fail和ioError方法都是我們用來創(chuàng)建Zero的工廠方法。fail方法讀入一個字符串然后返回一個會拋出UserException的action嗽桩,ioError則將一個任意異常作為參數(shù)岳守,返回一個會拋出該異常的action。

  private class FailureAction(e:Exception)   
      extends IOAction[Nothing] {  
    def apply(state:WorldState) = throw e  
  }  
    
  private class UserException(msg:String)   
    extends Exception(msg)  
  
  def fail(msg:String) =   
    ioError(new UserException(msg))      
  def ioError[A](e:Exception):IOAction[A] =   
    new FailureAction(e)  
}  

IOAction的flatMap方法和ChainedAction保持原樣碌冶。map函數(shù)現(xiàn)在會直接調(diào)用unit方法所以它也是符合Monad法則的湿痢。除此之外,我還另外添加了兩個操作符>>和<<扑庞。如同flatMap會將一個返回另一個action的函數(shù)拼接到這個action之后譬重,>>和<<將另一個action直接拼接到這個action之后。這僅僅是一個返回值的問題罐氨,>>讀作“then”臀规,有了它我們就可以做到創(chuàng)建一個返回第二個操作結(jié)果的動作。因此putString "What's your name" >> getString 就能創(chuàng)建一個首先輸出提示符然后讀入用戶輸入的操作栅隐。而相反塔嬉,<<讀作“before”,則會創(chuàng)建一個會返回第一個操作結(jié)果的動作租悄。

sealed abstract class IOAction[+A]   
    extends Function1[WorldState, (WorldState, A)] {  
  def map[B](f:A => B):IOAction[B] =   
    flatMap {x => IOAction.unit(f(x))}    
  def flatMap[B](f:A => IOAction[B]):IOAction[B]=  
    new ChainedAction(this, f)  
  
  private class ChainedAction[+A, B](  
      action1: IOAction[B],   
      f: B => IOAction[A]) extends IOAction[A] {  
    def apply(state1:WorldState) = {  
      val (state2, intermediateResult) =   
        action1(state1);  
      val action2 = f(intermediateResult)  
      action2(state2)  
    }  
  }    
  
  def >>[B](next: => IOAction[B]):IOAction[B] =  
    for {  
      _ <- this;  
      second <- next  
    } yield second  
      
  def <<[B](next: => IOAction[B]):IOAction[A] =  
    for {  
      first <- this;  
      _ <- next  
    } yield first  

由于我們現(xiàn)在定義好Zero了谨究,我們很容易就能夠遵循Monad法則來添加一個filter方法。這里我創(chuàng)建了兩種形式的filter方法泣棋。第一種允許傳入一個用戶自定義的消息來提示為什么filter不兼容而第二種則延續(xù)Scala的標準規(guī)范胶哲,使用通用的錯誤消息。

def filter(  
    p: A => Boolean,   
    msg:String):IOAction[A] =  
  flatMap{x =>   
    if (p(x)) IOAction.unit(x)   
    else IOAction.fail(msg)}  
def filter(p: A => Boolean):IOAction[A] =  
  filter(p, "Filter mismatch")  

Zero還意味著我們能夠?qū)崿F(xiàn)Monad的加法了潭辈。為了實現(xiàn)他鸯屿,我們需要做一點準備工作。HandlingAction能夠包裹住另一個action類萎胰,并且在那個action拋出異常的時候?qū)惓鬟f給一個特定的處理函數(shù)碾盟。onError則是一個專門創(chuàng)建HandlingAction的工廠方法。最后技竟,“or”是我們的Monad加法冰肴。簡單說,它會在一個action失敗的時候嘗試運行另外那個可選的action。

private class HandlingAction[+A](  
    action:IOAction[A],  
    handler: Exception => IOAction[A])   
    extends IOAction[A] {  
  def apply(state:WorldState) = {  
    try {  
      action(state)  
    } catch {  
      case e:Exception => handler(e)(state)  
    }  
  }      
}  
  
def onError[B >: A](  
    handler: Exception => IOAction[B]):  
    IOAction[B] =   
  new HandlingAction(this, handler)        
  
def or[B >: A](  
    alternative:IOAction[B]):IOAction[B] =  
  this onError {ex => alternative}  

最終版本的IOApplication保持原樣

sealed trait WorldState{def nextState:WorldState}  
  
abstract class IOApplication {  
  private class WorldStateImpl(id:BigInt)   
      extends WorldState {  
    def nextState = new WorldStateImpl(id + 1)  
  }  
  final def main(args:Array[String]):Unit = {  
    val ioaction = iomain(args)  
    ioaction(new WorldStateImpl(0));  
  }  
  def iomain(args:Array[String]):IOAction[_]  
}  

RTConsole同樣也幾乎沒變熙尉,不過我給它添加了一個類似于println的putLine?方法联逻。我還把getString方法設置為了val變量。為什么不呢检痰?它并不會改變包归。

//file RTConsole.scala  
object RTConsole {  
  val getString = IOAction(Console.readLine)  
  def putString(s: String) =   
    IOAction(Console.print(s))  
  def putLine(s: String) =   
    IOAction(Console.println(s))  
}    

現(xiàn)在是時候給我們的HelloWorld程序添加點新功能了。sayHello方法通過一個字符串返回了一個action铅歼,如果它認得出這個名字那么就會給他打個招呼公壤,不然就會返回一個會導致失敗的action。

Ask是一個方便的創(chuàng)建提示符然后讀入字符串的方法椎椰,我們的>>操作符保證了這個action的結(jié)果會是getString返回的字符串厦幅。

processsString方法讀入一個任意字符串,如果讀到的是“quit”的話慨飘,它就會返回一個會向你告別的action然后退出确憨。其他情況下,它會再次調(diào)用sayHello方法并將你輸入的字符串傳入瓤的,然后為了防止失敗還將sayHello返回的action和另一action用or進行了組合休弃。最后它會再次調(diào)用一個叫做loop的action。

Loop是很有趣的方法圈膏。它被定義成了一個val塔猾,當然用def也沒什么不好。它其實是一個遞歸的函數(shù)而并不是一個普通的循環(huán)本辐。它的定義中用到processString方法而恰巧processString方法也是基于loop來定義的桥帆。

最后是iomain方法,它僅僅是創(chuàng)建了一個會打印自我介紹語句然后會再次調(diào)用loop的action慎皱。

警告:由于這個庫中l(wèi)oop方法的實現(xiàn)問題老虫,最終這些代碼可能會導致棧溢出。不要在任何生產(chǎn)環(huán)境中用這段代碼茫多,你能從上面的注解里看到原因祈匙。

object HelloWorld extends IOApplication {  
  import IOAction._  
  import RTConsole._  
    
  def sayHello(n:String) = n match {  
    case "Bob" => putLine("Hello, Bob")  
    case "Chuck" => putLine("Hey, Chuck")  
    case "Sarah" => putLine("Helloooo, Sarah")  
    case _ => fail("match exception")  
  }  
    
  def ask(q:String) =  
    putString(q) >> getString  
  
  def processString(s:String) = s match {  
    case "quit" => putLine("Catch ya later")  
    case _ => (sayHello(s) or           
        putLine(s + ", I don't know you.")) >>  
  
        loop   
  }  
      
  val loop:IOAction[Unit] =   
    for {  
      name <- ask("What's your name? ");  
      _ <- processString(name)  
    } yield ()  
    
  def iomain(args:Array[String]) = {  
    putLine(  
        "This is an example of the IO monad.") >>  
    putLine("Enter a name or 'quit'") >>  
    loop  
  }  
}  

結(jié)論

在這篇文章中,我把IO Monad稱為IO Action來讓大家更好的理解它們是等待被執(zhí)行的動作天揖。也許有的人會認為在Scala中夺欲,IO Monad并沒有太大的實際意義。沒關系今膊,我的本意也并不是在這里鼓吹函數(shù)引用透明性些阅。IO Monad只是作為我們目前遇到的Monad中唯一從各種意義上來說都不是容器類型的簡單例子來介紹。

事實上斑唬,IO Monad依然可以被看做是容器市埋,只是與普通包含值的容器不同黎泣,它包含的是表達式。它通過flatMap和map函數(shù)依次將嵌套的表達式來組成更加復雜的表達式缤谎。

也許從更深層次的角度可以把IO Monad看做一個函數(shù)或是抽象的計算抒倚。而flatMap的作用可以看做是把函數(shù)應用于計算而創(chuàng)建更加復雜計算的。

在這個系列的最后部分坷澡,我會介紹一種將容器和計算模型統(tǒng)一的抽象方法托呕。但首先我要通過展示一個稍微復雜一點的運用了大量Monad的應用來向你們展示一下到底它有多重要。

最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末频敛,一起剝皮案震驚了整個濱河市项郊,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌姻政,老刑警劉巖呆抑,帶你破解...
    沈念sama閱讀 206,602評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件岂嗓,死亡現(xiàn)場離奇詭異汁展,居然都是意外死亡,警方通過查閱死者的電腦和手機厌殉,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,442評論 2 382
  • 文/潘曉璐 我一進店門食绿,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人公罕,你說我怎么就攤上這事器紧。” “怎么了楼眷?”我有些...
    開封第一講書人閱讀 152,878評論 0 344
  • 文/不壞的土叔 我叫張陵铲汪,是天一觀的道長。 經(jīng)常有香客問我罐柳,道長掌腰,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,306評論 1 279
  • 正文 為了忘掉前任张吉,我火速辦了婚禮齿梁,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘肮蛹。我一直安慰自己勺择,他們只是感情好,可當我...
    茶點故事閱讀 64,330評論 5 373
  • 文/花漫 我一把揭開白布伦忠。 她就那樣靜靜地躺著省核,像睡著了一般。 火紅的嫁衣襯著肌膚如雪昆码。 梳的紋絲不亂的頭發(fā)上气忠,一...
    開封第一講書人閱讀 49,071評論 1 285
  • 那天邓深,我揣著相機與錄音,去河邊找鬼笔刹。 笑死芥备,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的舌菜。 我是一名探鬼主播萌壳,決...
    沈念sama閱讀 38,382評論 3 400
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼日月!你這毒婦竟也來了袱瓮?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,006評論 0 259
  • 序言:老撾萬榮一對情侶失蹤爱咬,失蹤者是張志新(化名)和其女友劉穎尺借,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體精拟,經(jīng)...
    沈念sama閱讀 43,512評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡燎斩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,965評論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了蜂绎。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片栅表。...
    茶點故事閱讀 38,094評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖师枣,靈堂內(nèi)的尸體忽然破棺而出怪瓶,到底是詐尸還是另有隱情,我是刑警寧澤践美,帶...
    沈念sama閱讀 33,732評論 4 323
  • 正文 年R本政府宣布洗贰,位于F島的核電站,受9級特大地震影響陨倡,放射性物質(zhì)發(fā)生泄漏敛滋。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,283評論 3 307
  • 文/蒙蒙 一玫膀、第九天 我趴在偏房一處隱蔽的房頂上張望矛缨。 院中可真熱鬧,春花似錦帖旨、人聲如沸箕昭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,286評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽落竹。三九已至,卻和暖如春货抄,著一層夾襖步出監(jiān)牢的瞬間述召,已是汗流浹背朱转。 一陣腳步聲響...
    開封第一講書人閱讀 31,512評論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留积暖,地道東北人藤为。 一個月前我還...
    沈念sama閱讀 45,536評論 2 354
  • 正文 我出身青樓,卻偏偏與公主長得像夺刑,于是被迫代替她去往敵國和親缅疟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,828評論 2 345

推薦閱讀更多精彩內(nèi)容