跳转到内容

newLISP/Apply 和 map 简介

来自维基教科书,开放的书籍,开放的世界

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 和数据列表“介绍”给彼此,然后退后一步,让它们自己完成任务。

有两个重要的函数可以做到这一点:applymap

apply 接受一个函数和一个列表,并使它们协同工作。

(apply add data)
;-> 5.3
(div (apply add data) (length data))
;-> 0.5888888889

这产生了所需的结果。这里我们将 add 函数视为任何其他 newLISP 列表、字符串或数字,将其用作另一个函数的参数。你不需要引用它(尽管你可以这样做),因为 apply 已经期望一个函数的名称。

另一个可以使函数和列表协同工作的函数是 map,它将一个函数一次一个地应用于列表中的每一项。例如,如果你想将 floor 函数应用于数据列表的每个元素(将其向下取整为最接近的整数),你可以将 mapfloor 和数据组合在一起,如下所示。

(map floor data)
;-> (0 3 -2 1 -3 0 1 2 0)

并将 floor 函数应用于数据的每个元素。结果被组合起来并返回一个新的列表中。

更详细的 apply 和 map

[编辑 | 编辑源代码]

applymap 都允许你将函数视为数据。它们具有相同的基本形式。

(apply f l)
(map f l)

其中 f 是一个函数的名称,l 是一个列表。其想法是你告诉 newLISP 使用你指定的函数来处理一个列表。

apply and map use functions on list elements

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 表达式逐个遍历元素就可以轻松转换列表所有元素的方法。

更多技巧

[编辑 | 编辑源代码]

mapapply 都还有更多技巧。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 函数返回的值是该对的第二个成员,这将成为下一对第一个元素的值。

Lisp 风格

[编辑 | 编辑源代码]

这种将函数名称作为数据片段传递的做法是 newLISP 的一大特点,也非常有用。你会发现它有很多用途,有时会使用你认为对 map 不太有用的函数。例如,以下是 setmap 的控制下运行的。

(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)
华夏公益教科书