2013-10-30 56 views
22

我最近看了一個關於如何提出IO monad的視頻,談話是在scala中。我實際上想知道具有函數返回IO [A]的點是什麼。包裝在IO對象中的lambda表達式就是突變,在某種程度上它們必須被觀察到更高的變化,我的意思是執行,以便發生某些事情。你不只是把問題推到別的地方嗎?Scala IO monad:有什麼意義?

我可以看到的唯一好處是它允許延遲評估,因爲如果您不調用unsafePerformIO操作,則不會發生副作用。此外,我猜想該程序的其他部分可以使用/共享代碼並在需要發生副作用時分離。

我想知道這是否全部?可測試性有沒有優勢?我假設不是你必須觀察到否定這種否定的效果。如果你使用特質/接口,你可以控制依賴關係,但不能在這些依賴關係發生效果時進行控制。

我把下面的例子放在代碼中。

case class IO[+A](val ra:() => A){ 
    def unsafePerformIO() : A = ra(); 
    def map[B](f: A => B) : IO[B] = IO[B](() => f(unsafePerformIO())) 
    def flatMap[B](f: A => IO[B]) : IO[B] = { 
    IO(() => f(ra()).unsafePerformIO()) 
    } 
} 



case class Person(age: Int, name: String) 

object Runner { 

    def getOlderPerson(p1: Person,p2:Person) : Person = 
    if(p1.age > p2.age) 
     p1 
     else 
     p2 

    def printOlder(p1: Person, p2: Person): IO[Unit] = { 
    IO(() => println(getOlderPerson(p1,p2))).map(x => println("Next")) 
    } 

    def printPerson(p:Person) = IO(() => { 
    println(p) 
    p 
    }) 

    def main(args: Array[String]): Unit = { 

    val result = printPerson(Person(31,"Blair")).flatMap(a => printPerson(Person(23,"Tom")) 
            .flatMap(b => printOlder(a,b))) 

    result.unsafePerformIO() 
    } 

} 

你可以看到效果是如何延遲到主要我認爲是很酷的。我從視頻中瞭解到這一點後就提出了這個問題。

我的執行是否正確,我的理解是否正確。

我也想知道是否應該將它與ValidationMonad結合起來,就像在ValidationMonad [IO [Person]]中一樣,這樣當異常發生時我們可以短路。請思考。

布萊爾

回答

27

函數的類型簽名記錄它是否有副作用是很有價值的。您的IO實現具有價值,因爲它確實實現了很多。它使您的代碼更好地記錄在案;如果您重構代碼以儘可能多地分離涉及IO的邏輯,那麼您已經使得非IO涉及的函數更易於組合並且更易於測試。你可以在沒有顯式IO類型的情況下進行相同的重構。但使用明確的類型意味着編譯器可以幫助你做分離。

但這只是一個開始。在你的問題的代碼中,IO操作被編碼爲lambdas,因此是不透明的;除了運行IO操作之外,IO操作無能爲力,運行時的影響是硬編碼的。

這不是實現IO monad的唯一可能方式。

例如,我可能會讓我的IO操作案例類擴展一個共同的特徵。然後,我可以編寫一個運行函數的測試,並查看它是否返回IO操作的正確種類

在那些代表不同類型的IO動作的類中,我可能不會包含運行時所做動作的硬編碼實現。相反,我可以使用類型類型來解耦。這將允許交換IO操作執行的不同實現。例如,我可能有一組實現與生產數據庫進行通信,另一組與數據庫模擬對話以進行測試。

有一個在第13章中的Scala比亞爾納鬆& Chiusano的書函數式編程的(「外部效應和I/O」)一個好的處理這些問題。特別參見13.2.2「簡單IO類型的優點和缺點」。

更新(2015年12月):「交換IO操作的不同實現方式」,現在越來越多的人正在使用「自由單體」來實現這種功能。見例如John De Goes的博客文章「A Modern Architecture for FP」。

+0

謝謝。很好的答案我今晚會看看這些想法。 –

+0

你有沒有帶子類和類型類的代碼片段? –

+2

查看Drexin鏈接到的Runar的幻燈片,特別是'ConsoleIO'的東西。它演示瞭如果您運行這些操作('隱式對象ConsoleEffect ..')定義可能發生什麼IO動作('case object GetLine ...','case class PutLine ...')的聲明的分離。 .')。但請注意,其中還有其他內容。它不是最基本的代碼,只能說明我所說的。 –

18

使用IO單子的好處是具有純程序。你不要把這些副作用推得更高,但是消除它們。如果你有一個像下面的非純函數:

def greet { 
    println("What is your name?") 
    val name = readLine 
    println(s"Hello, $name!") 
} 

您可以通過它改寫刪除副作用:

def greet: IO[Unit] = for { 
    _ <- putStrLn("What is your name?") 
    name <- readLn 
    _ <- putStrLn(s"Hello, $name!") 
} yield() 

第二個功能是引用透明。

一個很好的解釋,爲什麼使用IO單子導致純程序可以從scala.io找到in Rúnar Bjarnason's slides(視頻可以找到here)。

+0

這裏是非FP程序員。爲什麼第二個是透明的?打印將取決於包含在問候功能中的用戶輸入,否? – nawfal

+0

@nawfal它是透明的,因爲它對外界沒有影響,並且不依賴於外部狀態。每次運行它時,都會得到相同的結果:運行時會打印一些文本並要求輸入。如果你想做兩次,你可以調用兩次函數來獲得兩個相同的動作,或者你可以調用一次函數,重用它返回的動作,並且你的程序在語義上是相同的。你也可以調用它,從不使用語義上相同的結果,永遠不要調用函數。 – puhlen

+0

@puhlen我明白了,但是當你調用返回的動作時,你有副作用,對吧?基本上你正在將副作用的部分移動到別的地方? – nawfal