摘要:在本文中,我們一起來揭開迭代循環的面紗,並介紹一些能夠完全取代迭代的程式設計概念。
連結:https://medium.com/codex/ive-almost-stopped-using-iteration-entirely-ee34f208d7ad
作者 | Emmett Boudreau
譯者 | 彎月 責編 | 鄭麗媛
在編寫軟體時,我們經常需要使用包含不同類型的資料,這種資料通常被稱為「結構」。更具體地說,有些特定類型的結構又稱為「可迭代對象」。
可迭代對象是一種類型,其中包含名叫「元素」的資料類型,而不是欄位或屬性。舉個例子,Julia 中的可迭代對象包括 Dict 和 Vector,就相當於 Python 中的 list 和 dict。Dict 包含 Pair 類型的元素,而 Vector 包含其參數指定的任意類型。這些類型的結構被稱為「可迭代對象」,因為它們是可以迭代。
迭代是程式設計中的一個非常關鍵的概念。事實上,迭代非常重要,就連低級彙程式設計式碼中都包含類似於循環的實現。迭代,更具體地說,for 循環,是我們在學習第一門程式語言時學習的第一個知識點。然而,迭代也有其自身的一些問題。
許多進程的執行速度減慢,究其原因最大的障礙往往就是循環。因此,我們應該最大限度地提高循環的性能,避免嵌套,並儘可能避免循環。那麼,為什麼我們要將循環作為默認的方式之一呢?為什麼不試試看其他有助於提升性能的方式呢?


避免使用循環
有幾種技巧可以避免使用循環。首先是遞迴,不過遞迴的應用範圍更窄,而且性能遠遠比不上循環。不過,第二種方法就比較實用了。這種方法叫做「廣播」(broadcasting),它將一個函數應用到給定結構中的每個元素上,因此可以很容易地一次性改變多個元素。還有一個類似的概念「對映」(mapping),從某種意義上來說,它和廣播非常相似。第三種方法與它們相似,叫做「解析式」(comprehension)。這幾個概念的基本思想都是將一個函數應用到所有元素上,從而在多個值上同時進行某種運算。
在編寫 Python 時,最常用的方法就是 map。為了對映一個函數,我們只需用 lambda 或 def 定義它:
myfunc = lambda x: x + 1
然後將這個函數傳遞給 map 函數,後者將返回一個 map 對象。該對象可以轉換成一個 list,從而獲取其值。map 函數的第一個參數是上述函數,第二個參數是要對映的 list。
map(myfunc, [1, 2, 3, 4, 5])
list(map(myfunc, [1, 2, 3, 4, 5]))
[2, 3, 4, 5, 6]
雖然 Python 中的解析式也很強大,但與 Julia 等語言相比,其功能還是太受限了。因此,大部分情況下,解析式是快速操作多個元素的最佳選擇。但是,由於解析式非常善於進行小型操作(如上例),我們應該在這裡演示一下。
vals = [x + 1 for x in [1, 2, 3, 4, 5]]
print(vals)
[2, 3, 4, 5, 6]
Julia 也有 map 函數,而且像 Python 一樣,該函數非常易於使用。不過我必須承認,我在 Julia 中使用 map 的次數遠遠少於廣播以及更強大的解析式。Julia 的 map 的語法也和 Python 非常相似,最主要的區別就是它會返回一個 Vector:
julia> map(x -> x + 1, [1, 2, 3, 4, 5, 6])
6-element Vector{Int64}:
2
3
4
5
6
7
Julia 也支持廣播。與 map 很相似,我們可以利用廣播在陣列內的每個元素上應用任意函數。Python 的 NumPy 也提供了此功能,但是本文主要想討論 Julia。在 Julia 中,只需在給定函數前寫一個點(.), 即可將其應用到每個元素。Julia 的基本運運算元中經常被引用的一個示例如下:
x = [5, 10, 15]
y = [5, 10, 15]
x .* y
3-element Vector{Int64}:
25
100
225
對於其他通用的應用,該操作也可以使用 @. 宏完成。如果你想了解有關 Julia 中廣播的更多資訊,可以閱讀這篇文章:https://medium.com/chifi-media/broadcasting-power-in-julia-beginner-friendly-overview-11cdd099623a。
像 Python 一樣,Julia 也有解析式。雖然 Python 的解析式很有侷限性,特別是其可讀性並非太好,但 Julia 則截然不同。當然,沒有任何規定組禁止你在 Python 的解析式之外定義函數,但這樣做會導致一些問題,比如當需要在函數中引某個變數的時候就會很麻煩。儘管如此,雖然有時候 Python 的解析式需要藉助其他東西才能清晰易讀,但 Julia 的解析式可以像函數一樣書寫,可以寫在多行內,而且支持多種語句!下面是一個解析式,它很像 Python:
x = [x - 1 for x in 1:5]

替代循環
下面,我們來看看 Julia 的解析式究竟好在哪裡。首先,我們可以利用 begin/end 語法,以函數的形式編寫解析式。
x = [begin x - 1 end for x in 1:5]
在解析式中添加 begin/end 敞開了一扇大門,可以讓解析式完成更多的工作。我喜歡將解析式寫成這樣:
x = [begin
x - 1
end for x in 1:5]
在這個新函數中,我們可以做任何循環能做的事情,除了一些例外。例如,下面是個條件語句:
julia> x = [begin
if x > 1
5
else
3
end
end for x in 1:10]
10-element Vector{Int64}:
3
5
5
5
5
5
5
5
5
5
雖然 Python 也能實現這一點,但別忘了,我們還能添加更多的語句,甚至可以使用 try/catch 和閉包!
julia> x = [begin
if x > 1
5
else
try
"hello" * 5
catch
3
end
end
end for x in 1:10]
10-element Vector{Int64}:
3
5
5
5
5
5
5
5
5
5

需要循環的情況
雖然用函數替換循環能節省不少時間,但循環在程式語言和軟體中依然有著重要的作用。最值得一提的是,循環的關鍵字 break 和 continue 是其他方法都沒有的。這可以節省很多不必要的循環。break 關鍵字能結束循環,而 continue 關鍵字可以直接跳轉到下一次循環。下面是選擇循環的一個例子,主要目的是在找到適當的值時,利用 break 跳出循環:
function display(d::OliveDisplay, m::MIME{<:Any}, o::Any)
T::Type = typeof(o)
mymimes = [MIME"text/html", MIME"text/svg", MIME"text/plain"]
mmimes = [m.sig.parameters[3] for m in methods(show, [IO, Any, T])]
correctm = nothing
for m in mymimes
if m in mmimes
correctm = m
break
end
end
show(d.io, correctm(), o)
end
在這個例子中,我們依次遍歷 MIME,找到最有可能的值。在已有的 MIME(即 mmimes)中找到所需的 mime 後,設置 correctm,然後將其傳遞給 show 函數。這個例子中,相較於對映或解析式,選擇迭代更合適,因為它擁有獨到的優勢。

總結
一直以來,迭代式循環都是程式設計中的一個重要的概念,在高級語言中得到了廣泛的應用。循環是從硬體層面上實現的,其重要性不言而喻。但是,許多現代程式語言完全可以不使用循環,我們可以利用函數、對映和解析式來實現相同的功能。但是,在有一些情況下,選擇循環依然比解析式或對映更好。一種情況就是可以利用 break 和 continue 關鍵字跳過元素、加快循環。