2017-04-19 13 views
4

下面是FPIS如何在遞歸上下文中進行惰性解釋?

object test2 { 

    //a naive IO monad 
    sealed trait IO[A] { self => 
    def run: A 
    def map[B](f: A => B): IO[B] = new IO[B] { def run = f(self.run) } 
    def flatMap[B](f: A => IO[B]): IO[B] = { 
     println("calling IO.flatMap") 
     new IO[B] { 
     def run = { 
      println("calling run from flatMap result") 
      f(self.run).run 
     } 
    } 
    } 
    } 

    object IO { 
    def unit[A](a: => A): IO[A] = new IO[A] { def run = a } 
    def apply[A](a: => A): IO[A] = unit(a) // syntax for IO { .. } 
    } 

    //composer in question 
    def forever[A,B](a: IO[A]): IO[B] = { 
    lazy val t: IO[B] = a flatMap (_ => t) 
    t 
    } 

    def PrintLine(msg: String) = IO { println(msg) } 

    def say = forever(PrintLine("Still Going..")).run 
} 

test2.say代碼將堆棧溢出之前打印數千「仍在繼續」。但我不知道到底發生了什麼。

輸出看起來是這樣的:
斯卡拉> test2.say
調用IO.flatMap //只有一次
從flatMap結果調用運行
仍在繼續..
從flatMap結果
調用運行 還在..

... //重複直到堆棧溢出

當樂趣ction 永遠返回,是完全計算(緩存)的懶惰值? 而且,flatMap方法似乎只被調用一次(我添加了打印語句),這反映了永遠的遞歸定義。爲什麼?

===========
我覺得有趣的另一件事是永遠的B型可以是任何東西。實際上斯卡拉可以運行它不透明。

我手動嘗試永遠[單位,雙],永遠[單位,字符串]等,這一切工作。這感覺很聰明。

+0

'IO'在哪裏定義?請發佈你的問題[MCVE]。 –

+0

這是合理的 – user1206899

回答

2

我想知道當函數永遠返回,是完全計算(緩存)的懶惰值?

如果是這樣,那麼爲什麼需要懶惰的關鍵字?

你的情況沒用。它可以像情況是有用的:

def repeat(n: Int): Seq[Int] { 
    lazy val expensive = "some expensive computation" 
    Seq.fill(n)(expensive) 
    // when n == 0, the 'expensive' computation will be skipped 
    // when n > 1, the 'expensive' computation will only be computed once 
} 

其他的事情,我不明白的是,flatMap方法似乎 被調用一次哪些計數器的 遞歸定義(我添加打印語句)永遠。爲什麼?

無法發表評論,直到你可以提供一個最小的,完整的,並且可驗證的例子,像@Yuval Itzchakov說

更新19/04/2017

好吧,我需要糾正自己:-)在你的情況下,lazy val是必需的,因爲遞歸引用本身。

來解釋你的觀察,讓我們嘗試了forever(a).run呼叫擴大:

  1. forever(a)擴展到

  2. { lazy val t = a flatMap(_ => t) }擴展到

  3. { lazy val t = new IO[B] { def run() = { ... t.run } }

因爲t是懶惰的,所以2和3中的flatMapnew IO[B]只被調用一次,然後'緩存'以供重用。

在3上調用run()時,您開始在t.run上進行遞歸,從而得到您觀察到的結果。

不完全確定自己的需求,但forever非堆棧吹版本可以等來實現:

def forever[A, B](a: IO[A]): IO[B] = { 
    new IO[B] { 
     @tailrec 
     override def run: B = { 
     a.run 
     run 
     } 
    } 
    } 
+0

感謝您的建議。 – user1206899

+0

這看起來更清晰,謝謝 – user1206899

3

什麼forever方法確實是,顧名思義,使單子實例a運行永遠。更確切地說,它給了我們無限的一元操作鏈。

其值t遞歸定義爲:

t = a flatMap (_ => t) 

它將擴展爲

t = a flatMap (_ => a flatMap (_ => t)) 

它將擴展爲

t = a flatMap (_ => a flatMap (_ => a flatMap (_ => t))) 

等。

Lazy使我們能夠定義像這樣的東西。如果我們刪除了懶惰的部分,我們會得到一個「前向引用」錯誤(如果遞歸值包含在某個方法中),或者它將被初始化爲一個默認值,而不是遞歸地使用(如果包含在一個類中,使其成爲一個帶有幕後獲取者和二傳手的職業場)。

演示:

val rec: Int = 1 + rec 
println(rec) // prints 1, "rec" in the body is initialized to default value 0 


def foo() = { 
    val rec: Int = 1 + rec // ERROR: forward reference extends over definition of value rec 
    println(rec) 
} 

然而,僅此是爲什麼整個堆棧溢出的事情發生的原因。有另一個遞歸部分,而這一個實際上是負責堆棧溢出。這是隱藏在這裏:

def run = { 
    println("calling run from flatMap result") 
    f(self.run).run 
} 

方法run自稱是(看到self.run)。當我們像這樣定義它時,我們不會當場評估self.run,因爲f尚未被調用;我們只是說它會在調用run()後被調用。

但是,當我們在forever中創建值t時,我們正在創建一個flatmaps到自身的IO monad(它提供給flatMap的函數是「評估自己」)。這將觸發run,因此會觸發f的評估和調用。我們從來沒有真正離開flatMap上下文(因此只有一個打印語句是爲flatMap部分),因爲一旦我們嘗試flatMap,run開始評估函數f,該函數返回我們調用run的IO,調用函數f返回IO上,我們稱之爲運行其調用函數f返回上我們稱之爲運行IO ...

+0

根據你的擴展模式,它應該是flatMap溢出堆棧的權利?然而,溢出函數運行並且flatMap甚至被稱爲不超過一次,這是混淆了我的部分。 – user1206899

+1

我現在有點纏住我的頭。擴展的「t」都指向內存中的相同結果IO實例,對吧?一個flatMap調用就是創建它所需要的。 – user1206899

+0

感謝您的闡述。一旦我掌握了它,這真的很有趣。 – user1206899

2
new IO[B] { 
    def run = { 
     println("calling run from flatMap result") 
     f(self.run).run 
    } 
} 

我明白了爲什麼現在四溢在運行方法時發生:在外運行調用運行高清實際上指向def run本身。

調用堆棧看起來像這樣:

 
f(self.run).run 
     |-----|--- println 
      |--- f(self.run).run 
         |-----|------println 
           |------f(self.run).run 
              |------ (repeating) 

F(self.run)總是指向同一評估/高速緩存懶惰VAL噸對象
因爲F:_ =>噸簡單地返回t IS UNIQUE新創建的 IO [B]託管我們正在調用的run方法,並將立即遞歸調用。

這就是我們在堆棧溢出之前可以看到打印語句的方式。

但是仍然不清楚在這種情況下,懶惰的val如何做到正確。