Lisp与其后代语言(CoffeeScript为例)之函数对比

小弟在此首先申明,此文撰写的内容为经验之谈,请Lisp专业人士勿喷。

Lisp由约翰·麦卡锡博士在1958年基于λ演算创造,演化至今,是历史第二悠久的高级语言,仅次于Fortran,也是第一个函数式编程语言,同时也是人工智能第一语言。

Lisp创造的很多概念被大多数现代语言传承并发扬。比如我们常见的Perl、Python、Ruby等语言,在它们的很多特性中,都有Lisp的身影。而这些共同特性中比较显著的可能就是函数了。(注意:Lisp函数非C语言函数)。

下面就以CoffeeScript为对照,对比Lisp与CoffeeScript中的函数特性。之所以选择CoffeeScript,是因为CoffeeScript作为JavaScript的转译语言,除了具有JavaScript的很多特性,也继承了RubyPythondLisp的很多特点。在此没有选择诸如Clojure(Lisp方言)Haskell,一方面是因为它们这些语言也是纯函数式语言,不利于比较;另一方面也因为不太了解它们。下面从最一般的声明方式记起。

声明与返回

一般而言,Lisp中的函数与Lisp普通变量无异,所以其命名规则与Lisp普通变量类似,可以包含除双引号、括号等符号外的其他ASCII字符(双引号等保留符号经过转义后也可以作函数名,虽然比较古怪),一般习惯采用“xxx-xxx-xxx…”的命名方式。

Lisp函数实则是lambda列表,而Lisp中的列表就如同C中的表达式。众所周知,表达式在执行后一定会返回一个值。Lisp中,一切皆列表,所以诸如IF判断操作也是列表操作。这就很方便了,执行的函数无论何时都会返回一个值,函数可以与任意列表结合使用。

下面的对比都是以同一功能的不同实现来记录的。

必要形参

比较见以下两例:

(注:Lisp运行环境,GNU CLISP 2.49+;Coffee运行环境,Node.js 9.1.0 + CoffeeScript 2.0.2)

Lisp

#!/usr/bin/env clisp

(defun introduce-myself (name age)
  (format t "My name is ~d, I am ~d years old." name age))

(introduce-myself "YanWen" 20)

Coffee

#!/usr/bin/env coffee

introduceMyself = (name, age) ->
  console.log "My name is #{name}, I am #{age} years old."

introduceMyself 'YanWen', 20

输出:

My name is YanWen, I am 20 years old.

可选形参

Lisp

#!/usr/bin/env clisp

(defun introduce-myself (&optional name (age 20))
  (format t "My name is ~d, I am ~d years old.~%" name age))

(introduce-myself)
(introduce-myself "YanWen")
(introduce-myself "YanWen" 20)

Coffee

#!/usr/bin/env coffee

introduceMyself = (name = undefined, age = 20) ->
  console.log "My name is #{name}, I am #{age} years old."

introduceMyself()
introduceMyself 'YanWen'
introduceMyself 'YanWen', 20

输出:

My name is NIL, I am 20 years old.  (My name is undefined, I am 20 years old.)
My name is YanWen, I am 20 years old.
My name is YanWen, I am 20 years old.

剩余形参

Lisp

#!/usr/bin/env clisp

(defun introduce-myself (&rest rest)
  (format t "My name is ~d, I am ~d years old. (all args: ~d)~%"
    (first rest) (second rest) rest))

(introduce-myself)
(introduce-myself "YanWen")
(introduce-myself "YanWen" 20)
(introduce-myself "YanWen" 20 "useless")

Coffee

#!/usr/bin/env coffee

introduceMyself = () ->
  process.stdout.write "My name is #{arguments[0]}, I am #{arguments[1]} 
    years old. -> all args: "; console.dir(arguments)

introduceMyself()
introduceMyself 'YanWen'
introduceMyself 'YanWen', 20
introduceMyself 'YanWen', 20, 'useless'

输出:

Lisp

My name is NIL, I am NIL years old. (all args: NIL)
My name is YanWen, I am NIL years old. (all args: (YanWen))
My name is YanWen, I am 20 years old. (all args: (YanWen 20))
My name is YanWen, I am 20 years old. (all args: (YanWen 20 useless))

Coffee

My name is undefined, I am undefined years old. -> all args: {}
My name is YanWen, I am undefined years old. -> all args: { '0': 'YanWen' }
My name is YanWen, I am 20 years old. -> all args: { '0': 'YanWen', '1': 20 }
My name is YanWen, I am 20 years old. -> all args: { '0': 'YanWen', '1': 20, '2': 'useless' }

关键字形参

Lisp

#!/usr/bin/env clisp

(defun introduce-myself (&key name age)
  (format t "My name is ~d, I am ~d years old.~%" name age))

(introduce-myself :name "YanWen" :age 20)

Coffee

#!/usr/bin/env coffee

introduceMyself = ({name, age}) ->
  console.log "My name is #{name}, I am #{age} years old."

introduceMyself name: 'YanWen', age: 20

(注:两者输出一致。)

四种参数类型在Lisp中可以组合使用,创造混合类型的参数,在此不作深入记录。而Coffee就不行,参数类型只能是以上四种类型中的一种,不能是混合类型。

返回值

Lisp / Coffee中的函数都会默认将最后一个执行的列表 / 表达式的值返回,什么都没有时返回。比如:

clisp repl环境

[1]> (defun return-value () )
RETURN-VALUE
[2]> (return-value)
NIL

coffee-2 repl环境

coffee> returnValue = ->
[Function: returnValue]
coffee> do returnValue
undefined

显然return-value / returnValue函数是空函数,默认返回NIL / undefined

当然,也可用return / return-from(lisp)返回,在此就不记录了,可以去查阅相关资料。

高阶函数与匿名函数(Lambda)

高阶函数

作为参数的函数叫高阶函数。Lisp中调用函数可以用操作符funcall或者applyJavaScript的类似实现是callapply以下是摘自《Lisp实用教程(Peter Serbel)》一书中的例子:

Lisp

#!/usr/bin/env clisp

;; plot image
(defun plot (fn min max step)
  (loop for i from min to max by step do
        (loop repeat (funcall fn i) do (format t "*"))
        (format t "~%")))

(plot #'exp 0 4 1/2)

用CoffeeScript实现类似功能:

Coffee

#!/usr/bin/env coffee

# plot image
plot = (fn, min, max, step) ->
  for i in [min..max] by step
    for repeat in [0..fn.call this, i]
      process.stdout.write "*"
    process.stdout.write "n"

plot Math.exp, 0, 4, 1/2

在Lisp实现中,内建函数exp被传入plot内;Coffee中,Math.exp传入plot内。

输出为:

*
**
***
*****
********
*************
*********************
**********************************
*******************************************************

匿名函数(Lambda)

无论是Lisp还是Coffee,函数与匿名函数没有显著区别,都是函数。一般把没有名字的函数叫匿名函数,这不有点废话吗。。。

一般用匿名函数还用于构造闭包,以保存上下文变量

同样以plot函数为例,不过这次使用匿名函数:

Lisp

(plot #'(lambda (x) (* x x)) 0 4 1/2)

Coffee

plot ((x) -> x ** 2), 0, 4, 1/2

输出为:

*
*
**
***
*****
*******
**********
*************
*****************

更多请参阅专业文献。

参考:

  1. 《实用Common Lisp编程. Peter Serbel(作者). 田春(译者)》 人民邮电出版社, 2011.10
  2. Wikipedia – Lisp Programming Lang

作者: YanWen

Web 开发者

发表评论

Fill in your details below or click an icon to log in:

WordPress.com 徽标

You are commenting using your WordPress.com account. Log Out /  更改 )

Google photo

You are commenting using your Google account. Log Out /  更改 )

Twitter picture

You are commenting using your Twitter account. Log Out /  更改 )

Facebook photo

You are commenting using your Facebook account. Log Out /  更改 )

Connecting to %s