前言
要問我為什么不從頭開始翻譯而偏偏從第四部分開始的話递雀,是因為前三部分已經(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的應用來向你們展示一下到底它有多重要。