什么时候可以在函数式语言中修改变量?

时间:2012-08-21 00:22:13

标签: functional-programming scheme lisp purely-functional mutability

所以我正在使用Racket Scheme教我自己的函数式编程,到目前为止我喜欢它。作为我自己的练习,我一直在尝试以纯粹的功能方式实现一些简单的任务。我知道不变性是功能风格的重要组成部分,但我想知道是否有任何时候它是可以的。

我想到一种函数在与filter一起使用时从字符串列表中删除非唯一字符串的有趣方式,如下所示:

(define (make-uniquer)
  (let ([uniques '()])
    (lambda (x)
      (if (not (member x uniques))
          (set! uniques (cons x uniques))
          #f))))

(define (uniquify x)
  (let ([uniquer (make-uniquer)])
    (filter uniquer x)))

正如您所看到的,make-uniquer返回一个字符串列表上的闭包,以便与唯一性进行比较,这样它就可以作为过滤器的简单谓词。但我破坏性地更新了封闭式清单。这是不好的形式,还是可以用这种方式改变本地封闭变量?

2 个答案:

答案 0 :(得分:10)

纯粹且不纯的函数式编程

Pure function本身就是referentially transparent,它允许记忆(缓存结果)。缺乏可变状态允许重入,允许不同版本的链接数据结构共享内存,并使自动并行化成为可能。关键在于,通过限制自己改变状态,你不再需要考虑许多复杂的命令式编程问题。

然而,这种限制有缺点。一个是性能:一些算法和数据结构(如构建哈希表)根本无法表示为纯函数而无需复制大量数据。另一个:与Haskell相比,一种纯函数式语言。由于变异不存在(概念上),您必须使用monad来表示状态变化。 (虽然Haskell提供了一个相当简洁的do - 符号语法糖,但是状态monad中的编程与“常规”Haskell完全不同!)如果你的算法最容易使用几个改变状态的互锁循环来表达, Haskell的实现将比不纯的语言更加笨拙。

一个例子是更改嵌套在XML文档中的单个节点。使用zipper data structures,没有状态变异就可能更困难但更困难。示例伪代码(纯):

old_xml = <a><b><c><d><e><f><g><h attrib="oldvalue"/></g></f></e></d></c></b></a>
// '\' is the XML selection operator
node_to_change = orig_xml \ "a" \ "b" \ "c" \ "d" \ "e" \ "f" \ "g" \ "h"
node_changed = node_to_change.copy("attrib" -> "newvalue")
new_xml = node_changed.unselect().unselect().unselect().unselect()
                      .unselect().unselect().unselect().unselect()
return new_xml

示例(不纯):

xml = <a><b><c><d><e><f><g><h attrib="oldvalue"/></g></f></e></d></c></b></a>
node_to_change = orig_xml.select_by_xpath("/a/b/c/d/e/f/g/h")
node_to_change.set("attrib" -> "newvalue")
return xml    // xml has already been updated

有关纯功能数据结构的更多信息,请参阅https://cstheory.stackexchange.com/questions/1539/whats-new-in-purely-functional-data-structures-since-okasaki。 (此外,通常可以编写一个只能操作内部状态的过程函数,这样它就可以被包装起来,使它对调用者来说实际上是一个纯粹的函数。这在一个不纯的语言中有点容易,因为你没有必须在状态monad中写入并将其传递给runST。)

虽然以不纯的风格写作会失去这些好处,但功能编程的其他一些便利(如高阶函数)仍然适用。

使用变异

Lisp是一种impure函数式语言,意味着它允许状态变异。 这是设计,因此如果您需要变异,您可以使用它而无需使用其他语言。

一般来说,,在

时使用状态变异是好的
  • 出于性能原因需要它,或
  • 使用变异可以更清楚地表达您的算法。

当你这样做时:

  • 清楚地记录您的uniquify函数会改变您传递给它的列表。如果调用者将函数传递给变量并让它返回更改,那将是令人讨厌的!
  • 如果您的应用程序是多线程的,请分析,注意并记录您的不纯函数是否是线程安全的。

答案 1 :(得分:10)

在这种情况下,我会避免可变实现,因为功能实现在性能方面可以很好地竞争。以下是函数的三个版本(包括内置remove-duplicates):

#lang racket

(define (make-uniquer)
  (let ([uniques (make-hash)])
    (lambda (x)
      (if (not (hash-ref uniques x #f))
          (hash-set! uniques x #t)
          #f))))

(define (uniquify x)
  (let ([uniquer (make-uniquer)])
    (filter uniquer x)))

(define (uniquify-2 lst)
  (define-values (_ r)
   (for/fold ([found (hash)] [result '()])
             ([elem (in-list lst)])
     (cond [(hash-ref found elem #f)
            (values found result)]
           [else (values (hash-set found elem #t)
                         (cons elem result))])))
  (reverse r))

(define randoms (build-list 100000 (λ (n) (random 10))))
(time (for ([i 100]) (uniquify randoms)))
(time (for ([i 100]) (uniquify-2 randoms)))
(time (for ([i 100]) (remove-duplicates randoms)))

;; sanity check
(require rackunit)
(check-equal? (uniquify randoms) (uniquify-2 randoms))
(check-equal? (uniquify randoms) (remove-duplicates randoms))

我得到的时间是

cpu time: 1348 real time: 1351 gc time: 0
cpu time: 1016 real time: 1019 gc time: 32
cpu time: 760 real time: 760 gc time: 0

不是科学数字,所以拿一粒盐。公平地说,我调整了uniquify-2,因为我的第一个版本速度较慢。我还使用哈希表改进了可变版本,但也许还有其他优化可以应用。此外,内置remove-duplicates针对性能进行了调整,并使用了可变数据结构(尽管它避免了set!)。

您可能也对Guide entry on performance感兴趣。它指出使用set!可能会损害性能,因此请谨慎使用。