從 iterator 及 generator 到 iterable
今天來跟大家介紹 iterator、generator 及 iterable 這三個東西。基本上,大家應該都有用過 for 迴圈,然而很多東西可以放在迴圈的 in
後方,這樣就可以將裡頭的元素一個一個取出來。但是這是怎做到的呢?
很基本的作法是去實作一個設計模式(design pattern)中的 iterator,假設我們要迭代一個陣列裡的元素,iterator 有別於陣列本身,是另外一個物件。Iterator 可以提供你一個介面,例如 next()
,讓你可以取得陣列裡的下一個元素。
像這樣的方式很常見也很簡單可以實作,在 Julia 裡就像這樣:
1 | mutable struct ArrayIterator |
1 | julia> v = collect(1:2:30); |
各位可以看到 iterator 一般都會實作 has_next()
及 next()
函式,是用來確認是否有下一個元素存在及取得下一個元素。Iterator 中會包含陣列本身,也就是想要迭代的集合本身,還有一個 index
會指向目前的元素,並在取得元素之後移往下一個。
Iterator 是一個物件,它需要一個已經存在的集合來作為它迭代的集合。這代表這個集合必須事先存在,如果這個集合很大,那就會佔去不少記憶體空間。有沒有什麼方法可以不需要集合就可以迭代的呢?Generator 是一個方式。
Generator,顧名思義,它會在你取得下一個元素的時候產生出來,並不會事先計算,所以集合也不會存在。Julia 的 generator 很好撰寫,如下:
1 | x = (i^2 for i in 1:10) |
這就是一個會產生 1 到 10 數字的 generator。Generator 也可以被放到 for 迴圈裡被迭代,但唯一不同的是,它並不佔記憶體空間,只需要即時的計算即可。
Julia 目前提供的方式稱為 Iterable,只要有實作 iterate()
這個 API 的就稱為 Iterable。像以下例子:
1 | struct Character |
iterate()
的輸入是「狀態」,輸出是下一個狀態,而狀態包含兩個物件:要迭代的元素以及順序。因此,會看起來像是這個樣子:
1 | (el1, (el2, i+1)) = iterate(iter, (el1, i)) |
特別要說明的是,在輸出的狀態中,需要帶有目前元素的資訊 (el1, (el2, i+1))
。如果要結束迴圈,那就回傳 nothing
。我們就可以來世試看這個可以迭代輸出 ASCII 碼的 Iterable。
1 | julia> c = Character() |
這樣可以有 generator 的好處,而且本身也是一個 iterator。它不需要是一個物件,只要有實作 iterate()
這個 API 的物件就可以使用,所以我們還可以拿來這樣用。
1 | struct IterableNumber |
這是一個可以自定義起點的可迭代數字,數字可以持續到無限大。你得手動把它停下來!
1 | julia> iter = IterableNumber(0) |
或是你想做個 sliding window:
1 | struct SlidingWindow |
這邊用 view(iter.vec, i:e)
做取值的方式,比起用 iter.vec[i:e]
取值來的快很多。iter.vec[i:e]
這種方式會造出一個新的陣列,而 view 不會。
1 | julia> v = collect(1:50); |
這邊介紹了三種迭代的方法,而 iterable 則是目前由 Julia 官方支援的主流方式。除了可以迭代數字以外還可以有其他用途,甚至可以迭代直到無限大的數字。Sliding window 這樣的東西在計算上或是深度學習上是很實用的結構!是不是覺得 Julia 越來越像 functional language 了XD