我一直在努力學習函式式編程,但我仍然難以像函式式程式員那樣思考。一種這樣的掛斷是如何實作高度依賴回圈/執行順序的索引繁重的操作。
例如,考慮以下 Java 代碼
public class Main {
public static void main(String[] args) {
List<Integer> nums = Arrays.asList(1,2,3,4,5,6,7,8,9);
System.out.println("Nums:\t" nums);
System.out.println("Prefix:\t" prefixList(nums));
}
private static List<Integer> prefixList(List<Integer> nums){
List<Integer> prefix = new ArrayList<>(nums);
for(int i = 1; i < prefix.size(); i)
prefix.set(i, prefix.get(i) prefix.get(i-1));
return prefix;
}
}
/*
System.out:
Nums: [1, 2, 3, 4, 5, 6, 7, 8, 9]
Prefix: [1, 3, 6, 10, 15, 21, 28, 36, 45]
*/
這里,在prefixList函式中,首先克隆了nums串列,然后對其進行了迭代操作,其中索引i上的值依賴于索引i-1(即需要執行順序)。然后回傳這個值。
這在函式式語言(Haskell、Lisp 等)中會是什么樣子?我一直在學習單子,并認為它們在這里可能是相關的,但我的理解仍然不是很好。
uj5u.com熱心網友回復:
信不信由你,這個函式實際上是Haskell內置的。
> scanl ( ) 0 [1..9]
[0,1,3,6,10,15,21,28,36,45]
因此,廣泛的答案通常是:我們可以使用方便的與串列相關的原語來構建而不是手動撰寫回圈。人們喜歡說遞回是 FP 中最接近命令式編程中“for 回圈”的類比。雖然這可能是真的,但一般的函式式程式使用的顯式遞回比一般的命令式程式使用的回圈要少得多。我們所做的大部分作業(尤其是串列)都是由map、filter、和、、foldl中的所有其他(高度優化的)好東西以及.Data.ListData.FoldableData.Traversablebase
至于我們如何實作這些功能,可以看原始碼scanl。出于效率原因,GHC 上的寫法略有不同,但基本要點是
scanl :: (b -> a -> b) -> b -> [a] -> [b]
scanl _ q [] = [q]
scanl f a (b:xs) = a : scanl f (f a b) xs
你不需要索引。您需要在構建串列時跟蹤單個先前的值,我們可以使用函式引數來做到這一點。如果您確實有一個需要隨機訪問各種索引的演算法,我們可以Data.Vector做到這一點。但 99% 的情況下,答案是“停止考慮索引”。
uj5u.com熱心網友回復:
這不是一個索引繁重的操作,事實上你可以用一個單行來做到這一點scanl1 :: (a -> a -> a) -> [a] -> [a]:
prefixList = scanl1 ( )
實際上,對于 的串列Nums,我們得到:
Prelude> prefixList [1 .. 9]
[1,3,6,10,15,21,28,36,45]
scanl1將原始串列的第一項作為累加器的初始值,并產生它。然后每次它獲取累加器和給定串列的下一項,并將它們相加為新的累加器,并產生新的累加器值。
通常不需要索引,但列舉串列就足夠了。命令式編程語言通常使用for帶有索引的回圈,但在許多情況下,這些可以被foreach不考慮索引的回圈替換。在 Haskell 中,這通常也有助于使演算法更懶惰。
如果你真的需要隨機訪問array查找,你可以使用和vector包中定義的資料結構。
uj5u.com熱心網友回復:
這不是 Haskell 的答案,但您標記了這個問題lisp,所以這里是 Racket 中的答案:這既完全實用,也表明您不需要索引來解決這個問題。
這個函式所做的是獲取一個數字流并為其回傳前綴流。這都是偷懶的。
(define (prefix-stream s)
(let ps-loop ([st s]
[p 0])
(if (stream-empty? st)
empty-stream
(let ([np ( p (stream-first st))])
(stream-cons
np
(ps-loop
(stream-rest st)
np))))))
所以現在
> (stream->list (prefix-stream (in-range 1 10)))
'(1 3 6 10 15 21 28 36 45)
但當然你也可以這樣做:
> (prefix-stream (in-naturals))
#<stream>
這顯然不是您可以轉換為串列的流,但您可以查看其中的一部分:
(stream->list (stream-take (prefix-stream (in-naturals)) 10))
'(0 1 3 6 10 15 21 28 36 45)
> (stream->list (stream-take (stream-tail (prefix-stream (in-naturals)) 1000) 10))
'(500500 501501 502503 503506 504510 505515 506521 507528 508536 509545)
(請注意,in-naturals認為自然是從 0 開始的,這是正確和正確的。)
uj5u.com熱心網友回復:
在 Clojure 中,這可以寫成:
(defn prefix-list [nums]
(loop [i 1
prefix nums]
(if (= i (count nums))
prefix
(recur (inc i) (assoc prefix i ( (get prefix i) (get prefix (dec i))))))))
(prefix-list [1 2 3 4 5 6 7 8 9])回傳[1 3 6 10 15 21 28 36 45]
在 Clojure 中,資料通常是不可變的(不能修改)。在這種情況下,該函式assoc接受一個向量,并回傳一個與原始向量一樣的新向量,但i第 th 個元素已更改。這聽起來可能效率低下,但底層資料結構允許在接近恒定的時間內完成更新(O (log 32 (n)))。
正如其他人所指出的那樣,可以在不使用索引向量的情況下對這個特定問題進行編碼,但我正在努力提供一個在使用索引陣列時對您的原始 Java 代碼有效的解決方案。
uj5u.com熱心網友回復:
我知道你問過函式式語言,但我只是想順便提一下 Python,作為一種多范式語言,也有這個很好的高階itertools.accumulate函式。
Accumulate接受一個集合并回傳其部分和的迭代器,或任何自定義二進制函式。與您的示例等效的功能 Python 代碼將是:
from itertools import accumulate
print(list(accumulate(range(1, 10))))
通常,Python itertools和functools標準庫模塊為函式式編程提供了出色的工具。
uj5u.com熱心網友回復:
其他人指出,您的特定示例可以使用類似的函式很好地處理scanl,所以讓我們看看“強烈依賴回圈/執行順序的索引繁重的操作”這個更廣泛的問題。我們可以將這個問題分解為三個問題:
- 索引
- 回圈播放
- 突變
只要索引的概念有意義,就支持對資料結構進行索引。例如,List兩者Vector都支持索引。正如其他人指出的那樣,Vector如果您的索引是隨機的,則性能會更好,但這種情況非常罕見。
盡管在 Prelude 和標準庫中實作了如此多的“高階函式”(將函式作為引數的函式),但命令式回圈可以直接替換為遞回函式(參見Wikipedia ),因此您很少需要顯式遞回。scanl就是一個例子。它允許您通過將其分配給預先撰寫的函式來避免顯式遞回呼叫。然而,該函式是遞回定義的。編譯器在生成機器代碼時可能會將該遞回優化為回圈。
最后,您可能有一個數字陣列,并且真的非常想通過一系列步驟來改變陣列中的值。這個執行緒中的每個人,包括我在內,都會非常努力地說服你擺脫這種情況,讓你以更“實用”的方式思考。但是如果我們失敗了,那么你可以使用狀態執行緒 monad。這為您提供了一種在函式內本地(可怕)改變資料結構的方法,同時僅使用不可變(不可怕)的資料結構與函式外部的事物進行互動,從而確保函式是參考透明的。
uj5u.com熱心網友回復:
此功能也稱為“累積和”,另一種 Python 方法是使用numpy(它是用 C 撰寫的,速度超快):
nums = np.array([1, 2, 3, 4, 5, 6, 7, 8, 9]) # or np.arange(1, 10) or np.arange(9) 1
print(nums.cumsum())
輸出:
[1, 3, 6, 10, 15, 21, 28, 36, 45]
uj5u.com熱心網友回復:
Elixir 中的內置函式類似于 Haskell 的:Enum.scan/2和Enum.scan/3:
Enum.scan(1..9, 0, fn element, acc -> element acc end) |> IO.inspect()
# yields:
# [1, 3, 6, 10, 15, 21, 28, 36, 45]
當您第一次從 OO 切換到函式式思維方式時,索引可能很難放手,但我發現幾乎所有我以前會遇到的索引解決方案(例如for next回圈)的問題都有一個優雅的對應物在maporreduce函式中,或以某種形式的尾遞回。
例如,您可以使用尾遞回和手動累加器構建自己的函式來處理此操作:
defmodule Foo do
def bar(nums, acc \\ [])
# We've reached the end of input: return the accumulator (reversed)
def bar([], acc), do: Enum.reverse(acc)
# The very first call is special because we do not have a previous value
def bar([x | tail], []) do
bar(tail, [x])
end
# All other calls land here
def bar([x | tail], [prev | _] = acc) do
bar(tail, [x prev | acc])
end
end
nums = [1, 2, 3, 4, 5, 6, 7, 8, 9]
prefix = Foo.bar(nums)
IO.inspect(prefix)
# Yields the same result:
# [1, 3, 6, 10, 15, 21, 28, 36, 45]
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/440930.html
上一篇:將來自多個抓取的scrapeURL結果連接到一個串列中
下一篇:使用高階函式重寫
