今天來跟大家介紹 iterator、generator 及 iterable 這三個東西。基本上,大家應該都有用過 for 迴圈,然而很多東西可以放在迴圈的 in 後方,這樣就可以將裡頭的元素一個一個取出來。但是這是怎做到的呢?

很基本的作法是去實作一個設計模式(design pattern)中的 iterator,假設我們要迭代一個陣列裡的元素,iterator 有別於陣列本身,是另外一個物件。Iterator 可以提供你一個介面,例如 next(),讓你可以取得陣列裡的下一個元素。

像這樣的方式很常見也很簡單可以實作,在 Julia 裡就像這樣:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
mutable struct ArrayIterator
array::Array
index::Int64

ArrayIterator(array::Array) = new(array, 1)
end

has_next(iter::ArrayIterator) = (iter.index < length(iter.array))

function next(iter::ArrayIterator)
x = iter.array[iter.index]
iter.index += 1
return x
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
julia> v = collect(1:2:30);

julia> iter = ArrayIterator(v);

julia> has_next(iter)
true

julia> next(iter)
1

julia> next(iter)
3

julia> has_next(iter)
true

各位可以看到 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct Character
start::Char
length::Int
Character() = new('A', 62)
end

function Base.iterate(char::Character, (el, i)=(char.start, 0))
if i < char.length
return (el, (el+1, i+1))
else
return nothing
end
end

Base.length(char::Character) = char.length
Base.eltype(::Character) = Char

iterate() 的輸入是「狀態」,輸出是下一個狀態,而狀態包含兩個物件:要迭代的元素以及順序。因此,會看起來像是這個樣子:

1
(el1, (el2, i+1)) = iterate(iter, (el1, i))

特別要說明的是,在輸出的狀態中,需要帶有目前元素的資訊 (el1, (el2, i+1))。如果要結束迴圈,那就回傳 nothing。我們就可以來世試看這個可以迭代輸出 ASCII 碼的 Iterable。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
julia> c = Character()
Character('A', 62)

julia> iterate(c)
('B', 1)

julia> iterate(c, ('B', 1))
('C', 2)

julia> for x in c
println(x)
end
A
B
C
D
E
F
G
H
...

這樣可以有 generator 的好處,而且本身也是一個 iterator。它不需要是一個物件,只要有實作 iterate() 這個 API 的物件就可以使用,所以我們還可以拿來這樣用。

1
2
3
4
5
struct IterableNumber
start::Int
end

Base.iterate(iter::IterableNumber, (el, i)=(iter.start, 0)) = i < Inf ? (el, (el+1, i+1)) : nothing

這是一個可以自定義起點的可迭代數字,數字可以持續到無限大。你得手動把它停下來!

1
2
3
4
5
6
7
8
9
10
11
12
13
julia> iter = IterableNumber(0)
IterableNumber(0)

julia> for i in iter
println(i)
end
0
1
2
3
4
5
...

或是你想做個 sliding window:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
struct SlidingWindow
vec::Vector
n::Int
start::SubArray

SlidingWindow(vec::Vector, n) = new(vec, n, view(vec, 1:n))
end

function Base.iterate(iter::SlidingWindow, (el, i)=(iter.start, 1))
e = i + iter.n - 1
if e <= length(iter.vec)
return (el, (view(iter.vec, i:e), i+1))
else
return nothing
end
end

這邊用 view(iter.vec, i:e) 做取值的方式,比起用 iter.vec[i:e] 取值來的快很多。iter.vec[i:e] 這種方式會造出一個新的陣列,而 view 不會。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
julia> v = collect(1:50);

julia> sw = SlidingWindow(v, 5)
SlidingWindow([1, 2, 3, 4, 5, 6, 7, 8, 9, 1041, 42, 43, 44, 45, 46, 47, 48, 49, 50], 5, [1, 2, 3, 4, 5])

julia> for x in sw
println(x)
end
[1, 2, 3, 4, 5]
[1, 2, 3, 4, 5]
[2, 3, 4, 5, 6]
[3, 4, 5, 6, 7]
[4, 5, 6, 7, 8]
[5, 6, 7, 8, 9]
[6, 7, 8, 9, 10]
[7, 8, 9, 10, 11]
[8, 9, 10, 11, 12]
[9, 10, 11, 12, 13]
...

這邊介紹了三種迭代的方法,而 iterable 則是目前由 Julia 官方支援的主流方式。除了可以迭代數字以外還可以有其他用途,甚至可以迭代直到無限大的數字。Sliding window 這樣的東西在計算上或是深度學習上是很實用的結構!是不是覺得 Julia 越來越像 functional language 了XD