微信公众号搜"智元新知"关注
微信扫一扫可直接关注哦!

“映射”是否一定会产生额外的嵌套级别?

如何解决“映射”是否一定会产生额外的嵌套级别?

使用嵌套的 map 是否会自动创建另一层嵌套?这是我使用的一个基本示例:

; One level
(map (lambda (x1)
   "Hi")
 '(1))

; Two levels
(map (lambda (x1)
   (map (lambda (x2)
          "Hi")
        '(1)))
     '(1))

; Three levels
(map (lambda (x1)
   (map (lambda (x2)
      (map (lambda (x3)
          "Hi")
           '(1)))
        '(1)))
     '(1))

; Four levels
(map (lambda (x1)
   (map (lambda (x2)
      (map (lambda (x3)
         (map (lambda (x4)
          "Hi")
           '(1)))
        '(1)))
     '(1)))
   '(1))
("Hi")
(("Hi"))
((("Hi")))
(((("Hi"))))

或者,是否存在添加一个 map 不会产生额外嵌套的反例?我无法“理解”map 如何添加一个级别。例如,对于第一级:

(map (lambda (x1)
   "Hi")
 '(1))

我知道列表中有一个元素,并且“对于列表中的每个元素”——我们将返回列表中那个位置的元素 "Hi",因此对于第一级我们得到 { {1}}。

但是,例如,我们如何从列表 ("Hi") 中获得第二级的嵌套列表?我知道我已经问了很多与 ("Hi") 和嵌套有关的问题,但希望如果我能理解从 map 级别到 1 的过程,我可以弄清楚其余的.. .

解决方法

map 收集所有函数调用的返回值,并将它们包装在另一个列表中。

如果函数返回一个列表,您将获得一个列表列表。

所以如果你有嵌套的地图,你总是得到嵌套列表作为最终结果。并且每一级映射都增加了另一级嵌套。

请注意,只有当您在每个级别实际返回 map 的值时,这才是正确的。你可以用它做其他事情,例如

;; Two levels
(map (lambda (x1)
       (length (map (lambda (x2)
                      "Hi")
                    '(1))))
     '(1))

这将返回 (1)

,

map 将为该列表中的每个元素返回一个列表。让我们通过一个多地图示例来展示嵌套是如何发挥作用的。

首先,让我们对一系列数字执行单个 map 并且对它们“不做任何事情”。也就是说,只需从输入中返回元素:

(map (lambda (x) x)
     '(1 2 3))
; (1 2 3)

此时,如果我们添加另一个 map,可能很容易认为不应该发生另一层嵌套,因为这里我们没有“嵌套”输入列表。它只是“停留”在 (1 2 3)

但是如果我们添加一个外层,我们可以看到这是如何发生的:

(map (lambda (ignore-me)
       (map (lambda (x) x)
            '(1 2 3)))
     '(1))
; (( 1 2 3 ))

只有一个“外部元素”很难看到这一点,但是,让我们添加 5 个外部元素并使其更容易发现:

(map (lambda (ignore-me)
       (map (lambda (x) x)
            '(1 2 3)))
     '(5 5 5 5 5))
;(
;  (1 2 3) 
;  (1 2 3) 
;  (1 2 3) 
;  (1 2 3) 
;  (1 2 3)
;)

在这里我们可以看到对于每个外部元素——其中有五个——我们正在返回“内部结果”——在上面的例子中是(1 2 3) .如果我们再次做同样的事情,这次产生两个额外的外循环,我们可以看到类似的结果发生:

(map (lambda (ignore-me1)
(map (lambda (ignore-me2)
       (map (lambda (x) x)
            '(1 2 3)))
     '(5 5 5 5 5)))
     '(2 2))
; (
;   ((1 2 3) (1 2 3) (1 2 3) (1 2 3) (1 2 3)) 
;   ((1 2 3) (1 2 3) (1 2 3) (1 2 3) (1 2 3))
; )
,

一对一

您的示例包括 (map (lambda (x) x) ...)

(map (lambda (ignore-me)
       (map (lambda (x) x) ; <- no-op
            '(1 2 3)))
     '(1))
((1 2 3))

“很难看到”,因为内部的 map 本质上是一个空操作。 map 在输入和输出之间创建 1:1 关系,对映射过程的作用没有意见。如果映射过程返回单个元素、列表或嵌套非常多的列表,则无关紧要 -

(map (lambda (ignore-me)
       '(1 2 3))
     '(foo))
((1 2 3))
(map (lambda (_)
       '(((((really nested))))))
     '(1 2 3))
((((((really nested)))))
 (((((really nested)))))
 (((((really nested))))))

或者使用一些 ascii 可视化 -

(define (fn x) (+ x x))

(map fn '(1 2 3))
;          \ \ \
;           \ \ (fn 3)
;            \ \      \
;             \ (fn 2) \
;              \      \ \
;               (fn 1) \ \
;                     \ \ \
;  output:           '(2 4 6) 

您是否应该在映射过程中选择 map 到另一个 map 调用 -

(map (lambda (...)
       (map ...)) ; <- nested map
     ...)

同样的规则适用。每个 map 输出与其输入列表具有 1:1 关系。我们可以想象 map 的类型以获得更多见解 -

map : ('a -> 'b,'a list) -> 'b list
       --------  -------     -------
         \        \           \_ returns a list of type 'b
          \        \
           \        \__ input list of type 'a
            \
             \__ mapping procedure,maps
                 one element of type 'a
                 to one element of type 'b

实现地图

作为一项学习练习,实施 map 以深入了解其工作原理非常有用 -

(define (map fn lst)
  (if (null? lst)
      '()
      (cons (fn (car lst))
            (map fn (cdr lst)))))

(map (lambda (x)
       (list x x x x x))
     '(1 2 3))
((1 1 1 1 1)
 (2 2 2 2 2)
 (3 3 3 3 3))

如您所见,maplst 中的元素类型或 fn 的返回值是什么没有意见。 map 简单地将列表的 car 传递给 fn 并将它cons 放到列表的 cdr 的递归结果上。


附加地图

我们应该查看的另一个相关过程是 append-map -

append-map : ('a -> 'b list,'a list) -> 'b list
              -------------  -------     -------
                \             \           \_ returns a list of type 'b
                 \             \_ input list of type 'a
                  \
                   \_ mapping function,maps
                      one element of type 'a
                      to a LIST of elements of type 'b

这里唯一的区别是映射过程预期返回元素的列表,而不仅仅是单个元素。这样,append-map 在输入列表和输出列表之间创建了 1:many 关系。

(append-map (lambda (x)
              (list x x x x x))
            '(1 2 3))
(1 1 1 1 1 2 2 2 2 2 3 3 3 3 3)

append-map 的这个特性就是为什么它有时被称为 flatmap,因为它“压平”了一层嵌套 -

(append-map (lambda (x)
              (map (lambda (y)
                     (list x y))
                   '(spade heart club diamond)))
            '(J Q K A))
((J spade)
 (J heart)
 (J club)
 (J diamond)
 (Q spade)
 (Q heart)
 (Q club)
 (Q diamond)
 (K spade)
 (K heart)
 (K club)
 (K diamond)
 (A spade)
 (A heart)
 (A club)
 (A diamond))

读者的后续练习:

  • 如果我们在上面的例子中用 append-map 交易 map,输出会是什么?
  • 以另一种或两种方式定义 map。验证正确的行为
  • 根据 append-map 定义 map。不使用 map 重新定义它。

打字松散

Scheme 是一种无类型语言,因此列表的内容不需要是同质的。尽管如此,以这种方式考虑 mapappend-map 还是很有用的,因为类型有助于传达函数的行为方式。以下是 Racket 提供的更准确的类型定义 -

(map proc lst ...+) → list?
  proc : procedure?
  lst : list?
(append-map proc lst ...+) → list?
  proc : procedure?
  lst : list?

这些松散得多,并且确实反映了您实际可以编写的松散程序。例如 -

(append-map (lambda (x)
              (list 'a-symbol "hello" 'float (* x 1.5) 'bool (> x 1)))
            '(1 2 3))
(a-symbol "hello" float 1.5 bool #f a-symbol "hello" float 3.0 bool #t a-symbol "hello" float 4.5 bool #t)

可变映射和附加映射

您在上面的类型中看到那些 ...+ 了吗?为简单起见,到目前为止,我一直在隐藏一个重要的细节。 map 的可变参数接口意味着它可以接受 1 个或更多输入列表 -

(map (lambda (x y z)
       (list x y z))
     '(1 2 3)
     '(4 5 6)
     '(7 8 9))
((1 4 7) (2 5 8) (3 6 9))
(append-map (lambda (x y z)
              (list x y z))
            '(1 2 3)
            '(4 5 6)
            '(7 8 9))
(1 4 7 2 5 8 3 6 9)

读者的后续练习:

  • 定义可变参数 map
  • 定义可变参数 append-map

一点点 lambda 演算

您了解 lambda 演算。您知道 (lambda (x y z) (list x y z)list 相同吗?这称为 Eta reduction -

(map list '(1 2 3) '(4 5 6) '(7 8 9))
((1 4 7) (2 5 8) (3 6 9))
(append-map list '(1 2 3) '(4 5 6) '(7 8 9))
(1 4 7 2 5 8 3 6 9)

分隔的延续

Remember when 我们谈到了 delimited continuations 和运算符 shiftreset?了解了 append-map 后,让我们看看 ambambiguous choice operator -

(define (amb . lst)
  (shift k (append-map k lst)))

我们可以像这样使用 amb -

(reset (list (amb 'J 'Q 'K 'A) (amb '♡ '♢ '♤ '♧)))
(J ♡ J ♢ J ♤ J ♧ Q ♡ Q ♢ Q ♤ Q ♧ K ♡ K ♢ K ♤ K ♧ A ♡ A ♢ A ♤ A ♧)
(reset (list (list (amb 'J 'Q 'K 'A) (amb '♡ '♢ '♤ '♧))))
((J ♡) (J ♢) (J ♤) (J ♧) (Q ♡) (Q ♢) (Q ♤) (Q ♧) (K ♡) (K ♢) (K ♤) (K ♧) (A ♡) (A ♢) (A ♤) (A ♧))

在一个更有用的例子中,我们使用 pythagorean theorem to find-right-triangles -

(define (pythag a b c)
  (= (+ (* a a) (* b b)) (* c c))) ; a² + b² = c²

(define (find-right-triangles . side-lengths)
  (filter ((curry apply) pythag)
          (reset (list
                   (list (apply amb side-lengths)
                         (apply amb side-lengths)
                         (apply amb side-lengths))))))

(find-right-triangles 1 2 3 4 5 6 7 8 9 10 11 12)
((3 4 5) (4 3 5) (6 8 10) (8 6 10))

分隔的延续有效地将您的程序从里到外,让您专注于声明式结构 -

(define (bit)
  (amb #\0 #\1))

(define (4-bit)
  (reset (list (string (bit) (bit) (bit) (bit)))))

(4-bit)
("0000" "0001" "0010" "0011" "0100" "0101" "0110" "0111" "1000" "1001" "1010" "1011" "1100" "1101" "1110" "1111")

amb 可用于任何表达式中的任何位置,为每个提供的值计算一次表达式。这种无限使用可以产生非凡的效果 -

(reset
  (list
    (if (> 5 (amb 6 3))
        (string-append "you have "
                       (amb "dark" "long" "flowing")
                       " "
                       (amb "hair" "sentences"))
        (string-append "please do not "
                       (amb "steal" "hurt")
                       " "
                       (amb "any" "other")
                       " "
                       (amb "people" "animals")))))
("please do not steal any people"
 "please do not steal any animals"
 "please do not steal other people"
 "please do not steal other animals"
 "please do not hurt any people"
 "please do not hurt any animals"
 "please do not hurt other people"
 "please do not hurt other animals"
 "you have dark hair"
 "you have dark sentences"
 "you have long hair"
 "you have long sentences"
 "you have flowing hair"
 "you have flowing sentences")

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。