簡體   English   中英

Streams 遞歸算法背后的直覺是什么?

[英]What is the intuition behind recursive algorithms with Streams?

就像標題所說的那樣,遞歸算法背后的直覺是什么:

val fibs: LazyList[Int] = (0 #:: fibs).scanLeft(1)(_ + _)

val fibs: LazyList[Int] = 0 #:: 1 #:: (fibs.zip(fibs.tail).map{ t => t._1 + t._2 })

它們是如何展開的? 這種算法的基本情況是什么(如果是Nil ,為什么會這樣?)以及它們如何向fibs.take(5)發展,例如?

編輯。

正如下面幾個人指出的那樣,我確實理解延遲定義的 Stream 沒有基本案例。 相反,我的問題涉及在fibs.take(5)中評估無限流時的基本情況是什么(我相信答案是Nil ,如果我錯了,請糾正我)以及評估fibs.take(5)的計算步驟是什么fibs.take(5)

據說這里有兩件事在起作用:

  • 使用LazyList API 的遞歸語法
  • 展開背后的核心數學

所以,讓我們從關於 API 和語法的幾句話開始:

  • #::接受惰性值並將其添加到LazyList定義之前,這里是fibs使其定義在代碼級別上遞歸
  • LazyList懶惰地評估它的參數,然后緩存/記憶它們以供將來使用,讓我們立即訪問已經計算的值

然而,下面的機制實際上是核心遞歸的。

讓我們以List為例來看看什么是數據遞歸:

List(1,2,3,4)

這也可以寫成

1 :: 2 :: 3 :: 4 :: Nil

這與

( ( ( Nil.::(4) ).::(3) ).::(2) ).::(1)

你可以看到我們:

  • Nil
  • 創建我們使用的::(4, Nil)
  • 創建::(3, ::(4, Nil))
  • 等等

換句話說,我們必須從一些基本案例開始,自下而上構建整個事物。 根據定義,這些值必須是有限的,並且不能用於表示(可能)無限計算的系列。

但是有一種替代方法可以讓您表達這樣的計算 - corecursion 和 codata。

使用 corecursion 你有一個元組:

  • 最后計算的值
  • 一個可以取值並返回下一個元組的函數(下一個值 + 下一個函數!)
  • 沒有什么能阻止您使用與元組的第二個元素相同的功能,但有一個選擇是件好事

例如,您可以定義無限系列的 LazyList(1, 2, 3, 4, 5, 6, ...) ,例如:

// I use case class since
//   type Pair = (Int, Int => Pair)
// would be illegal in Scala 
final case class Pair(value: Int, f: Int => Pair)
val f: Int => Pair = n => Pair(n + 1, f)
Pair(1, f)

然后您將獲取Pair ,從中獲取value (最初為1 )並使用它來生成新的Pair s( Pair(2, f)Pair(3, f) ,...)。

使用 corecursion 生成其值的結構將被稱為 codata(因此LazyList可以被認為是 codata)。

與斐波那契數列相同的故事,您可以使用核心遞歸地定義它

  • (Int, Int)作為值(初始化為(0, 1)
  • val f: (Int, Int) => Pair = { case (n, m) => Pair((m, n + m), f }作為函數
  • 最后,您必須從每個生成的(Int, Int)對中選擇_1

但是, LazyList的 API 為您提供了一些不錯的工具,因此您不必手動執行此操作:

  • 它記憶(緩存)計算值,因此您可以訪問list(0)list(1)等,它們不會在使用后立即被遺忘
  • 它為您提供.map.flatMap .scanLeft等方法,因此雖然在內部它可能有更復雜的類型用於 corecursion,但您只會看到您需要的最終結果

顯然,根據 codata 的定義,所有這些都是懶惰地完成的:在每一步中,您只能知道到目前為止定義的值,以及如何生成下一步。

這將我們引向您的示例:

val fibs: LazyList[Int] = (0 #:: fibs).scanLeft(1)(_ + _)

您可以將其視為:

  • 以一對(0, f)開頭
  • 其中f接受這個0參數,並將其與1組合以創建(0, 1)元組
  • 然后構造 next f s 跟蹤前一個值,並將其沿當前值傳遞給傳遞給scanLeft的函數
  • 所有具有中間值和函數以及記憶的惡作劇都由 API 在內部處理

所以如果你問我,這種算法的“基本情況”是一對值和函數返回對,一遍又一遍地運行。

它們是如何展開的?

他們沒有。 #::函數接受一個別名參數,這意味着它是惰性求值的。

這種算法的基本情況是什么(如果是 Nil,為什么會這樣?)。

沒有“基本情況”,這些遞歸定義產生無限流:

scala> val fibs: LazyList[Int] = (0 #:: fibs).scanLeft(1)(_ + _)
val fibs: LazyList[Int] = LazyList(<not computed>)

scala> fibs.size
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space

(注意"<not computed>"標記,它暗示了懶惰)

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM