Lisp 中的顺序过程
Sequential procedures in Lisp
当我尝试使用不可变对象以函数式风格编程时,顺序操作最终被从里到外编写,如下所示:
(thing-operation3
(thing-operation2
(thing-operation1 thing extra-arg1)
extra-arg2)
extra-arg3)
我开始看到这种模式在我的代码中不断重复,而且我发现它很难阅读。使用 curry 和 compose 等高阶过程可以略微改善这一点:
((compose1
(curryr thing-operation3 extra-arg3)
(curryr thing-operation2 extra-arg2)
(curryr thing-operation1 extra-arg1))
thing)
也许更好,但它仍然是颠倒的,并且需要一些额外的认知负担才能弄清楚到底发生了什么。我不确定这是否是 ideomatic Lisp 代码。
面向对象的风格更容易阅读:
thing.operation1(extra-arg1).operation2(extra-arg2)
.operation3(extra-arg3)
它以自然顺序读取,也可以用不可变对象来实现。
在 Lisp 中以什么ideomatic方式编写这样的顺序操作以便于阅读?
您可以使用 PROGN Common Lisp 特殊形式。
或者您可以根据自己的喜好定义自己的 Lisp 宏。
Clojure 有一个线程运算符 ->
,它可以满足您的期望:
(-> thing
(thing-operation1 extra-arg1)
(thing-operation2 extra-arg2)
(thing-operation3 extra-arg3))
您可以在其他 Lisp 方言中将其作为宏轻松实现。例如,Greg Hendershott 的 rackjure 库有一个 ~>
形式,它在 Racket 中做同样的事情。
->
(或rackjure中的~>
)宏将结果拼接为每个子窗体的第一个参数。如果您想将结果拼接为最后一个参数,则有一个 ->>
宏(rackjure 中的 ~>>
)。
Common Lisp 中的常用方法是使用 LET*
(let* ((thing1 (thing-operation0 thing0 extra-arg0))
(thing2 (thing-operation1 thing1 extra-arg1))
(thing3 (thing-operation2 thing2 extra-arg2)))
(thing-operation3 thing3 extra-arg3))
这样可以命名 return 值,这提高了可读性并且可以为这些值编写声明。
也可以编写一个宏,可以像下面这样使用:
(pipe
(thing-operation1 thing extra-arg1)
(thing-operation2 _2 extra-arg2)
(thing-operation3 _3 extra-arg3)
(thing-operation4 _4 extra-arg4))
一些语言提供类似的宏,而 Lisp 库可能提供它的变体。让我们写一个简单的版本:
(defmacro pipe (expression &rest expressions)
(if (null expressions)
expression
(destructuring-bind ((fn arg &rest args) &rest more-expressions)
expressions
(declare (ignorable arg))
`(pipe
(,fn ,expression ,@args)
,@more-expressions))))
对于上面的 pipe
表达式,生成以下代码:
(THING-OPERATION4
(THING-OPERATION3
(THING-OPERATION2
(THING-OPERATION1 THING EXTRA-ARG1)
EXTRA-ARG2)
EXTRA-ARG3)
EXTRA-ARG4)
变体:
(defmacro pipe (expression &rest expressions)
(if (null expressions)
expression
(destructuring-bind ((fn arg &rest args) &rest more-expressions)
expressions
`(pipe
(let ((,arg ,expression))
(,fn ,arg ,@args))
,@more-expressions))))
这会让你写:
(pipe (+ 1000 pi)
(+ arg1 arg1) ; use the previous result multiple times
(+ arg2 (sqrt arg2))) ; use the previous result multiple times
怎么样
(reduce (lambda (a b) (funcall b a))
(list thing
(partial-apply op1 arg1)
(partial-apply op2 arg2)
...
(partial-apply opn argn) ))
(在Common Lisp). In Racket,
(foldl (lambda (a b) (a b))
thing (list
(partial-apply op1 arg1)
(partial-apply op2 arg2)
...
(partial-apply opn argn) ))
关于术语,它是 ((curry fun) arg)
或 (partial-apply fun arg)
。
当我尝试使用不可变对象以函数式风格编程时,顺序操作最终被从里到外编写,如下所示:
(thing-operation3
(thing-operation2
(thing-operation1 thing extra-arg1)
extra-arg2)
extra-arg3)
我开始看到这种模式在我的代码中不断重复,而且我发现它很难阅读。使用 curry 和 compose 等高阶过程可以略微改善这一点:
((compose1
(curryr thing-operation3 extra-arg3)
(curryr thing-operation2 extra-arg2)
(curryr thing-operation1 extra-arg1))
thing)
也许更好,但它仍然是颠倒的,并且需要一些额外的认知负担才能弄清楚到底发生了什么。我不确定这是否是 ideomatic Lisp 代码。
面向对象的风格更容易阅读:
thing.operation1(extra-arg1).operation2(extra-arg2)
.operation3(extra-arg3)
它以自然顺序读取,也可以用不可变对象来实现。
在 Lisp 中以什么ideomatic方式编写这样的顺序操作以便于阅读?
您可以使用 PROGN Common Lisp 特殊形式。
或者您可以根据自己的喜好定义自己的 Lisp 宏。
Clojure 有一个线程运算符 ->
,它可以满足您的期望:
(-> thing
(thing-operation1 extra-arg1)
(thing-operation2 extra-arg2)
(thing-operation3 extra-arg3))
您可以在其他 Lisp 方言中将其作为宏轻松实现。例如,Greg Hendershott 的 rackjure 库有一个 ~>
形式,它在 Racket 中做同样的事情。
->
(或rackjure中的~>
)宏将结果拼接为每个子窗体的第一个参数。如果您想将结果拼接为最后一个参数,则有一个 ->>
宏(rackjure 中的 ~>>
)。
Common Lisp 中的常用方法是使用 LET*
(let* ((thing1 (thing-operation0 thing0 extra-arg0))
(thing2 (thing-operation1 thing1 extra-arg1))
(thing3 (thing-operation2 thing2 extra-arg2)))
(thing-operation3 thing3 extra-arg3))
这样可以命名 return 值,这提高了可读性并且可以为这些值编写声明。
也可以编写一个宏,可以像下面这样使用:
(pipe
(thing-operation1 thing extra-arg1)
(thing-operation2 _2 extra-arg2)
(thing-operation3 _3 extra-arg3)
(thing-operation4 _4 extra-arg4))
一些语言提供类似的宏,而 Lisp 库可能提供它的变体。让我们写一个简单的版本:
(defmacro pipe (expression &rest expressions)
(if (null expressions)
expression
(destructuring-bind ((fn arg &rest args) &rest more-expressions)
expressions
(declare (ignorable arg))
`(pipe
(,fn ,expression ,@args)
,@more-expressions))))
对于上面的 pipe
表达式,生成以下代码:
(THING-OPERATION4
(THING-OPERATION3
(THING-OPERATION2
(THING-OPERATION1 THING EXTRA-ARG1)
EXTRA-ARG2)
EXTRA-ARG3)
EXTRA-ARG4)
变体:
(defmacro pipe (expression &rest expressions)
(if (null expressions)
expression
(destructuring-bind ((fn arg &rest args) &rest more-expressions)
expressions
`(pipe
(let ((,arg ,expression))
(,fn ,arg ,@args))
,@more-expressions))))
这会让你写:
(pipe (+ 1000 pi)
(+ arg1 arg1) ; use the previous result multiple times
(+ arg2 (sqrt arg2))) ; use the previous result multiple times
怎么样
(reduce (lambda (a b) (funcall b a))
(list thing
(partial-apply op1 arg1)
(partial-apply op2 arg2)
...
(partial-apply opn argn) ))
(在Common Lisp). In Racket,
(foldl (lambda (a b) (a b))
thing (list
(partial-apply op1 arg1)
(partial-apply op2 arg2)
...
(partial-apply opn argn) ))
关于术语,它是 ((curry fun) arg)
或 (partial-apply fun arg)
。