Functional programming 什么时候可以用函数式语言修改变量?
所以,我正在自学使用racketscheme的函数式编程,到目前为止我很喜欢它。作为我自己的练习,我一直在尝试以一种纯功能的方式实现一些简单的任务。我知道不变性是函数式风格的一个重要组成部分,但我想知道是否有这样的情况 我想到了一种有趣的方法,当与过滤器一起使用时,函数可以从字符串列表中去除非唯一字符串,如下所示:Functional programming 什么时候可以用函数式语言修改变量?,functional-programming,scheme,lisp,purely-functional,mutability,Functional Programming,Scheme,Lisp,Purely Functional,Mutability,所以,我正在自学使用racketscheme的函数式编程,到目前为止我很喜欢它。作为我自己的练习,我一直在尝试以一种纯功能的方式实现一些简单的任务。我知道不变性是函数式风格的一个重要组成部分,但我想知道是否有这样的情况 我想到了一种有趣的方法,当与过滤器一起使用时,函数可以从字符串列表中去除非唯一字符串,如下所示: (define (make-uniquer) (let ([uniques '()]) (lambda (x) (if (not (member x uniq
(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
在字符串列表上返回一个闭包以进行唯一性比较,这样它就可以充当过滤器的简单谓词。但我正在破坏性地更新关闭列表。这是一种糟糕的形式,还是可以用这种方式改变局部封闭变量?纯函数编程和非纯函数编程
s是固有的,这允许记忆(缓存结果)。缺少可变状态允许重入,允许不同版本的链接数据结构共享内存,并使自动并行成为可能。关键是,通过限制自己的变异状态,您不再需要考虑命令式编程的许多复杂问题
然而,这种限制也有缺点。一个是性能:一些算法和数据结构(如构建哈希表)不能简单地表示为纯函数,而不必复制大量数据。另一个:与Haskell相比,Haskell是一种纯函数式语言。由于变异(概念上)不存在,所以必须使用s来表示状态变化。(虽然Haskell提供了一种相当简洁的do
-符号语法糖,但在状态单子中编程与“常规”Haskell是完全不同的语言!)如果您的算法最容易用几个改变状态的互锁循环来表示,Haskell实现将比不纯正语言中的实现更加笨拙
例如,更改XML文档中深度嵌套的单个节点。这是可能的,但如果没有状态突变,使用。伪代码示例(纯):
old_xml=。
(此外,通常可以编写一个只处理内部状态的过程函数,这样就可以对其进行包装,使其对调用方来说实际上是一个纯函数。这在不纯净的语言中更容易一些,因为您不必在状态单子中编写它并将其传递给runST
)
虽然以不纯正的风格编写会失去这些好处,但函数编程的其他一些便利(如高阶函数)仍然适用
利用突变
Lisp是一种函数语言,这意味着它允许状态变异这是经过设计的,因此,如果您需要变异,您可以使用它,而实际上不必使用其他语言
通常情况下,是的,在
- 出于性能原因需要,或者
- 使用变异可以更清楚地表达您的算法
当您这样做时:
- 清楚地记录您的
uniquify
函数将改变您传递给它的列表。调用者给你的函数传递一个变量,然后让它变回来,这将是一件令人讨厌的事情李>
- 如果您的应用程序是多线程的,请分析、注意并记录您的不纯函数是否是线程安全的
在这种情况下,我会避免可变实现,因为功能实现在性能方面可以很好地竞争。以下是该函数的三个版本(包括内置的删除重复项
):
#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
,因为我的第一个版本比较慢。我还通过一个哈希表改进了可变版本,但是可能还有其他的优化可以应用。此外,内置的删除重复项
针对性能进行了优化,并使用了可变的数据结构(尽管它避免了设置!
)
你也可能对这本书感兴趣。指出set
可能会损害性能,因此请小心使用。如果两个线程同时调用该函数,它是否会中断?@Bill如果两个线程从make uniquer
使用自己的闭包,则为否。但如果它们都使用相同的闭包,则为是。这是安全的主要测试吗?作为一个工程决策,这取决于你。好问题。如果make uniquer
仅从(由)(内部)调用uniquify
,则没有问题。确保它被很好地隐藏、封装。
#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