【问题标题】:Avoid using set! into functional programming style algorithm避免使用集合!进入函数式编程风格算法
【发布时间】:2019-05-05 16:16:05
【问题描述】:

我不确定我是否可以在这里问这个问题。如果这不是正确的地方,请告诉我,我会删除它。

我正在学习 Racket,有人告诉我如何避免在函数式编程风格中使用 set!。但是我很困惑,我不明白“函数式编程风格”的含义。只是为了学习,我想问这个问题。

我有以下代码:

 (define lst1 '())

 (do ([i n (- i 1)])
   ((zero? i))
   ; Get an item.
   (set! item (random-chooser original-list))
   ; Insert it into an auxiliary list, lst1
   (set! lst1 (cons item lst1))
   ; Remove the item from the original list
   (set! original-list (remove item original-list)))

 (append (list lst1) (list original-list))))))

此代码运行良好。我必须从original-list 列表中随机选择n 项目,不重复。然后创建一个包含两个子列表的列表,其中在lst1 中选择了n 项目,并将original-list 中的其余项目作为第二个子列表。

在不使用set! 的情况下,有没有更好的方法来做到这一点?

【问题讨论】:

  • 您的编辑实际上使三个答案中的两个无效。据我了解规则,我们不应该在 SO 上这样做。我觉得有义务回滚您的编辑。 :) 你总是可以问一个新问题,改变要求。 :)
  • @WillNess 如果我问另一个问题,类似于这个问题,我会得到很多反对票,因为人们会认为它是重复的。
  • 但它不是重复的。在这里您要求 k 随机选取的值,您将指定必须使用用户提供的函数。还包括此问题的链接,作为背景。
  • 这个问题已经结束(因为我有答案),但我在问如何避免使用set!。感谢您的 cmets。

标签: list random functional-programming scheme racket


【解决方案1】:

在函数式编程中,我们尽可能通过组合现有过程来编写代码。你所做的所有操作都可以用list procedures来表达:

(define n 5)
(define lst '(0 1 2 3 4 5 6 7 8 9))

(let ((shuffled (take (shuffle lst) n)))
  (list shuffled (remove* shuffled lst)))

=> '((5 8 9 6 2) (0 1 3 4 7))

最后你会得到两个列表,第一个包含从原始列表中随机选择的 n 元素,第二个包含其余元素。

在函数式编程中,我们非常努力地避免显式循环(我们使用递归代替它)和set!。我们使用现有程序、组合并创建新数据(例如列表),而不是修改现有数据。

我可以看出你来自程序背景,放弃循环和赋值有点困难,但这就是函数式编程的美妙之处:它让你以不同的方式思考如何解决问题。变异数据是许多困难的根源,尤其是在进行并发编程时,这就是 FP 不惜一切代价避免它的原因。

请注意,尽可能避免改变数据,但 Scheme 作为一种不纯 函数式编程语言允许它。例如,shuffle 过程在选取随机值时必须在某个时刻改变状态。但这一切都被封装了,在正常的日常编程中,您可以完成大部分工作,而无需使用 set!

【讨论】:

  • 感谢您的回答,让我明白了很多!但是我不知道如何使用您的示例使 mime 更具功能性,因为 random-chooser 这是我自己的函数,它不适合 (take (shuffle lst) n)
  • 在进行函数式编程时要记住的第一件事就是重用现有的过程,random-chooser 可能没有必要。话虽如此,我需要了解它在做什么。
  • random-chooser真的必要的。不解释它在内部做什么,它从我作为参数传递的列表中返回一个项目。
  • 使用remove* 是不正确的,以防输入中有重复项。简单的drop n 就足够了,或者如果将take+drop 融合到一个函数中,一次返回列表的两个部分(如Haskell 的splitAt),则更好。然后,使用 "shuffle" 执行 extraneous 工作(在 strict 语言中),随机选择输入中的 all 元素而不是只是n。在重复的情况下也是不正确的,因为 Q 是关于“不重复”选择 n 元素,但“shuffle”显然是纯粹的位置。 /对不起/ :)
  • @ÓscarLópez 啊,这实际上更有意义。也许这确实是他们的意思,不要多次选择相同的位置。我说得太直白了。 :/ 那么使用 shuffle 是正确的,甚至看起来很巧妙;但它仍然给(random) 打了太多电话(大概在内部)。 :)
【解决方案2】:

为了消除set! 和显式循环的使用,您需要使用递归,将“更新”的值作为参数传入。

Scheme 在letrec 周围有一些很好的语法糖(递归let,用于可以引用自身的绑定),它允许您一次性创建和调用函数:named let。这通常用于循环,因此该函数通常称为loop,但它可以称为任何其他名称。重要的是在循环的时候用修改过的参数调用它,而不是在循环结束的时候调用它。

(define (take-random n lst)
  ; Syntactic sugar that creates a 3-parameter function called `loop` and calls it
  ; with the values `n`, `'()` and `lst`
  (let loop ((n n)
             (picked '())
             (lst lst))
    ; If the loop counter reaches zero or if there are no more items to take
    (if (or (zero? n) (null? lst))
        ; Then combine the picked items and the remaining unpicked items
        (list picked lst)
        ; Otherwise, pick a random item from the list
        (let ((item (random-chooser lst)))
          ; And loop again, decreasing the loop counter to be nearer zero,
          ; adding the picked item to the list of picked items, and removing
          ; it from the list of remaining items to pick from
          (loop (- n 1)
                (cons item picked)
                (remove item lst))))))

;; Examples
(take-random 3 '(1 2 3 4 5 6))
; => ((4 1 6) (2 3 5))
(take-random 3 '(1 2))
; => ((2 1) ())

Óscar López's answer 很好地展示了执行此操作的更实用的方式。

【讨论】:

  • 感谢您的回答。我问了另一个问题,关于何时使用define 以及何时使用letstackoverflow.com/questions/53637079/…。我在你的回答中看到你只使用define 来定义一个函数,而在函数的主体中,你使用let
【解决方案3】:

解决方案

shuffle 确实是一个很好的解决方案,而且非常实用。功能样式禁止对现有/定义的变量值进行突变/更改(因此没有set!)。而是创建/复制现有对象并对其进行变异或以变异形式创建它。 shuffle 以变异的顺序创建/复制原始列表。

您可以使用takedrop 获取或离开任何列表的第一个n 位置。或者使用split-at 函数。

但是,由于这会将结果作为值返回,但任务是返回一个列表,因此您使用let-values 绑定两个返回结果并将它们绑定到一个列表中。

(define (choose-n-and-rest lst n)
  (let-values ([(chosen rest) (split-at (shuffle lst) n)])
    (list chosen rest)))

或:

(define (choose-n-and-rest lst n)
  (let ((new-lst (shuffle lst)) ; so that `take` & `drop` use the same shuffled list
    (list (take new-list n) (drop new-list n))))

但正如您所看到的heresplit-at 可能比takedrop 的组合更有效。

试试看:

(choose-n-and-rest '(a b c d e 1 2 3 4 5) 3) 
;; e.g. '((a 4 2) (1 b 3 5 c e d))

顺便说一句,set! 或更好:在 lisp 风格的函数式编程中,变异函数是/不是完全禁止的。 原因是性能。 复制每一个冗长的列表是昂贵的。 一个例子是 common-lisp 的nreverse,它改变了原始列表的顺序(颠倒了顺序)。非变异 reverse 创建一个新列表,其中的元素以相反的顺序排列。但为此它必须复制。 如果你改变局部变量(由let 定义),它可能会导致性能提升 - 当然,非常谨慎,因为突变是一件危险的事情。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2018-07-17
    • 1970-01-01
    • 1970-01-01
    • 2022-07-17
    • 2022-11-30
    • 1970-01-01
    • 2021-01-11
    • 1970-01-01
    相关资源
    最近更新 更多