所以我正在使用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
返回一个字符串列表上的闭包,以便与唯一性进行比较,这样它就可以作为过滤器的简单谓词。但我破坏性地更新了封闭式清单。这是不好的形式,还是可以用这种方式改变本地封闭变量?
答案 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!
可能会损害性能,因此请谨慎使用。