newLISP/Apply 和 map 簡介
通常,你會發現你有一些資料儲存在一個列表中,並且你想要將一個函式應用於它。例如,假設你從一個空間探測器中獲得了溫度讀數,並且它們儲存在一個名為 data 的列表中。
(println data)
(0.1 3.2 -1.2 1.2 -2.3 0.1 1.4 2.5 0.3)
你將如何將這些數字加起來,然後除以總數以找到平均值?也許你認為你可以使用 add,它對浮點數列表求和,但你不是在互動式地工作,所以你不能編輯程式碼使其像這樣讀取。
(add 0.1 3.2 -1.2 1.2 -2.3 0.1 1.4 2.5 0.3)
由於我們將資料儲存在一個名為 data 的符號中,我們可能嘗試以下方法。
(add data)
value expected in function add : data
但不行,這不起作用,因為 add 需要數字來加,而不是列表。當然,你可以用困難的方式來做,編寫一個迴圈,遍歷列表中的每一項,每次都增加一個執行總計。
(set 'total 0)
(dolist (i data)
(inc total i))
(println total)
5.3
這很好用。但是 newLISP 有一個更強大的解決方案,可以解決這個問題以及許多其他問題:你可以將函式視為資料,將資料視為函式,因此你可以像操作資料一樣輕鬆地操作函式。你只需要將 add 和資料列表“介紹”給彼此,然後退後一步,讓它們自己完成任務。
有兩個重要的函式可以做到這一點:apply 和 map。
apply 接受一個函式和一個列表,並使它們協同工作。
(apply add data)
;-> 5.3
(div (apply add data) (length data))
;-> 0.5888888889
這產生了所需的結果。這裡我們將 add 函式視為任何其他 newLISP 列表、字串或數字,將其用作另一個函式的引數。你不需要引用它(儘管你可以這樣做),因為 apply 已經期望一個函式的名稱。
另一個可以使函式和列表協同工作的函式是 map,它將一個函式一次一個地應用於列表中的每一項。例如,如果你想將 floor 函式應用於資料列表的每個元素(將其向下取整為最接近的整數),你可以將 map、floor 和資料組合在一起,如下所示。
(map floor data)
;-> (0 3 -2 1 -3 0 1 2 0)
並將 floor 函式應用於資料的每個元素。結果被組合起來並返回一個新的列表中。
apply 和 map 都允許你將函式視為資料。它們具有相同的基本形式。
(apply f l)
(map f l)
其中 f 是一個函式的名稱,l 是一個列表。其想法是你告訴 newLISP 使用你指定的函式來處理一個列表。
apply 函式將列表中的所有元素作為函式的引數使用,並計算結果。
(apply reverse '("this is a string"))
;-> "gnirts a si siht"
這裡,apply 看一下列表,在這種情況下它包含一個字串,並將元素作為引數提供給函式。字串被反轉。請注意,你不需要引用函式,但需要引用列表,因為你不希望 newLISP 在指定的函式有機會使用它之前就對其進行計算。
另一方面,map 函式會像一箇中士檢查一排士兵一樣,逐個遍歷列表,並將函式依次應用於每個元素,將元素作為引數使用。但是,map 會記住每個計算的結果,並將它們收集起來,並返回一個新的列表中。
因此,map 看起來像一個控制流詞,有點像 dolist,而 apply 是一種從程式內部控制 newLISP 列表計算過程的方法,在你想呼叫函式時和你想呼叫函式的地方呼叫它,而不僅僅是在正常的計算過程中。
如果我們調整 map 的前面示例,它會給出類似的結果,儘管結果是一個列表,而不是一個字串。
(map reverse '("this is a string"))
;-> ("gnirts a si siht")
因為我們使用了只有一個元素的列表,所以結果與 apply 示例幾乎相同,儘管請注意 map 返回一個列表,而在本示例中,apply 返回一個字串。
(apply reverse '("this is a string"))
;-> "gnirts a si siht"
該字串已被從列表中提取出來,反轉,然後儲存在 map 建立的另一個列表中。
在下面的示例中
(map reverse '("this" "is" "a" "list" "of" "strings"))
;-> ("siht" "si" "a" "tsil" "fo" "sgnirts")
你可以清楚地看到 map 已將 reverse 依次應用於列表的每個元素,並返回了一個包含反轉字串的列表。
為了說明這兩個函式之間的關係,這裡嘗試用 apply 來定義 map。
(define (my-map f l , r)
; declare a local variable r to hold the results
(dolist (e l)
(push (apply f (list e)) r -1)))
我們將將函式 f 應用於每個列表項的結果推送到臨時列表的末尾,然後依靠 push 在最後返回列表,就像 map 會做的那樣。這至少對於簡單的表示式有效。
(my-map explode '("this is a string"))
;-> ("t" "h" "i" "s" " " "i" "s" " " "a" " " "s" "t" "r" "i" "n" "g")
(map explode '("this is a string"))
;-> (("t" "h" "i" "s" " " "i" "s" " " "a" " " "s" "t" "r" "i" "n" "g"))
此示例說明了 map 為什麼如此有用。它是一種無需使用 dolist 表示式逐個遍歷元素就可以輕鬆轉換列表所有元素的方法。
map 和 apply 都還有更多技巧。map 可以同時遍歷多個列表。如果你提供兩個或多個列表,newLISP 會將每個列表的元素交織在一起,從每個列表的第一個元素開始,並將它們作為引數傳遞給函式。
(map append '("cats " "dogs " "birds ") '("miaow" "bark" "tweet"))
;-> ("cats miaow" "dogs bark" "birds tweet")
這裡,每個列表的第一個元素作為一對傳遞給 append,然後是每個列表的第二個元素,依此類推。
這種將多個字串編織在一起有點像用列表編織毛衣。或者像拉上拉鍊。
apply 也有一招。第三個引數表示函式應該使用前一個列表中的多少個引數。因此,如果一個函式接受兩個引數,而你提供了三個或更多引數,apply 會返回並進行另一次嘗試,使用第一次應用的結果和其他引數。它會繼續遍歷列表,直到所有引數都被用完。
為了實際操作,讓我們先定義一個接受兩個引數並比較其長度的函式。
(define (longest s1 s2)
(println s1 " is longest so far, is " s2 " longer?") ; feedback
(if (>= (length s1) (length s2)) ; compare
s1
s2))
現在你可以將此函式應用於字串列表,使用第三個引數告訴 apply 一次使用兩個字串的引數。
(apply longest '("green" "purple" "violet" "yellow" "orange"
"black" "white" "pink" "red" "turquoise" "cerise" "scarlet"
"lilac" "grey" "blue") 2)
green is longest so far, is purple longer? purple is longest so far, is violet longer? purple is longest so far, is yellow longer? purple is longest so far, is orange longer? purple is longest so far, is black longer? purple is longest so far, is white longer? purple is longest so far, is pink longer? purple is longest so far, is red longer? purple is longest so far, is turquoise longer? turquoise is longest so far, is cerise longer? turquoise is longest so far, is scarlet longer? turquoise is longest so far, is lilac longer? turquoise is longest so far, is grey longer? turquoise is longest so far, is blue longer? turquoise
這就像在海灘上行走,發現一顆鵝卵石,並一直拿著它,直到發現更好的鵝卵石。
apply 還為你提供了一種遍歷列表並將函式應用於每一對專案的方法。
(apply (fn (x y)
(println {x is } x {, y is } y)) (sequence 0 10) 2)
x is 0, y is 1 x is 1, y is 2 x is 2, y is 3 x is 3, y is 4 x is 4, y is 5 x is 5, y is 6 x is 6, y is 7 x is 7, y is 8 x is 8, y is 9 x is 9, y is 10
這裡發生的事情是,println 函式返回的值是該對的第二個成員,這將成為下一對第一個元素的值。
這種將函式名稱作為資料片段傳遞的做法是 newLISP 的一大特點,也非常有用。你會發現它有很多用途,有時會使用你認為對 map 不太有用的函式。例如,以下是 set 在 map 的控制下執行的。
(map set '(a b) '(1 2))
;-> a is 1, b is 2
(map set '(a b) (list b a))
;-> a is 2, b is 1
此構造提供了一種並行而不是順序地為符號賦值的方法。(你也可以使用 swap。)
map 的一些用法很簡單。
(map char (explode "hi there"))
;-> (104 105 32 116 104 101 114 101)
(map (fn (h) (format "%02x" h)) (sequence 0 15))
;-> ("00" "01" "02" "03" "04" "05" "06" "07" "08" "09" "0a" "0b" "0c" "0d" "0e" "0f")
其他的用法可能變得相當複雜。例如,給定一個以這種形式儲存在符號 image-data 中的資料字串。
("/Users/me/graphics/file1.jpg" " pixelHeight: 978" " pixelWidth: 1181")
這兩個數字可以用以下方法提取。
(map set '(height width) (map int (map last (map parse (rest image-data)))))
一些內建的 newLISP 函式可以與其他函式一起使用。例如,**curry** 函式可以複製一個雙引數函式,並建立一個單引數版本,該版本預先確定了第一個引數。所以,如果一個函式 *f1* 經常像這樣被呼叫:
(f1 arg1 arg2)
你可以使用 **curry** 函式建立一個新函式 *f2*,該函式有一個預先準備好的 *arg1* 引數:
(set 'f2 (curry f1 arg1))
現在你可以忘記第一個引數,只需為 *f2* 提供第二個引數即可:
(f2 arg2)
這有什麼用呢?考慮一下 **dup** 函式,該函式經常被用來插入多個空格:
(dup { } 10)
使用 **curry** 函式,你可以建立一個新函式,例如 *blank*,該函式是 **dup** 函式的一個特殊版本,它總是以一個空格作為字串引數呼叫:
(set 'blank (curry dup { }))
現在你可以使用 *(blank n)*:
(blank 10)
;-> ; 10 spaces
**curry** 函式可以用來建立臨時或匿名函式,並與 **map** 函式一起使用。
(map (curry pow 2) (sequence 1 10))
;-> (2 4 8 16 32 64 128 256 512 1024)
(map (fn (x) (pow 2 x)) (sequence 1 10)) ; equivalent
;-> (2 4 8 16 32 64 128 256 512 1024)
但避免在諸如 **inc** 等破壞性函式上使用 **curry** 函式,例如:
(setq a-list-of-pairs (sequence 2 10 2))
;-> (2 4 6 8 10)
(map (curry inc 3) a-list-of-pairs) ;-> you would expect (5 7 9 11 13), instead you get
;-> (5 9 15 23 33)
; one proper way to get every number incremented by 3 would be
(map (curry + 3) a-list-of-pairs)
;-> (5 7 9 11 13)
; or if you insist in using inc, then provide a copy of the increment so the reference inc gets doesn't mess up things
(map (curry inc (copy 3)) a-list-of-pairs)
;-> (5 7 9 11 13)
