Lisp 函数定义:defun 与参数
Lisp 是一种历史悠久的函数式编程语言,它的函数定义方式与其他语言有很大不同。本文将详细介绍 defun 的用法以及各种参数类型的特点,帮助你快速掌握 Lisp 函数定义的核心技能。
defun 的基本语法
defun 是 Lisp 中定义函数的核心宏,它的语法结构简洁明了。理解基本结构是后续学习的基础:
(defun 函数名 (参数列表)
"函数文档字符串(可选)"
函数体)
这个结构包含四个部分:
- 函数名:一个符号,用于后续调用这个函数
- 参数列表:定义函数接受哪些输入
- 文档字符串:描述函数用途,便于代码阅读和理解
- 函数体:包含实际的计算逻辑,可以有多条表达式
下面是一个简单的函数示例,定义一个加法函数:
;; 定义一个加法函数
(defun add (a b)
"计算两个数的和"
(+ a b))
调用这个函数非常简单:
(add 3 5) ; 返回 8
函数定义完成后,返回的 add 符号可以直接当作函数使用。这是 Lisp 作为一个「同像性」语言的体现——代码和数据具有相同的结构。
必选参数
必选参数是最基本的参数类型,调用时必须提供所有参数,顺序也不能改变。这种参数类型适用于函数的核心输入。
(defun greet (name message)
"向用户问好"
(format t "你好,~A!~%" message name))
调用方式如下:
(greet "张三" "今天过得怎么样?")
如果调用时参数数量不匹配,Lisp 会立即报错。例如:
(greet "李四") ; 错误:参数不足
(greet "王五" "你好" "extra") ; 错误:参数过多
必选参数的优点是调用接口简单清晰,缺点是缺乏灵活性。对于有默认值的参数,需要使用可选参数。
可选参数
使用 &optional 可以定义可选参数。这些参数有默认值,如果不提供则使用默认值,这让函数的调用更加灵活。
(defun greet-with-title (name &optional title)
"带职称的问候函数"
(if title
(format t "你好,~A ~A!~%" title name)
(format t "你好,~A!~%" name)))
调用示例:
(greet-with-title "李四) ; 输出:你好,李四!
(greet-with-title "李四" "博士) ; 输出:你好,博士 李四!
如果需要指定默认值,语法略有不同,需要用括号包裹默认值:
(defun power (base &optional (exponent 2))
"计算幂次,默认二次方"
(expt base exponent))
调用这个函数时:
(power 3) ; 返回 9,相当于 3 的 2 次方
(power 3 3) ; 返回 27,相当于 3 的 3 次方
值得注意的是,如果需要根据前面的参数计算默认值,可以在括号中引用前面的参数:
(defun rectangle-area (width &optional (height width))
"计算矩形面积,高默认等于宽"
(* width height))
关键字参数
使用 &key 可以定义关键字参数。调用时通过参数名指定值,顺序可以任意,这大大增强了调用时的灵活性。
(defun configure-server (&key port host debug)
"配置服务器参数"
(format t "主机: ~A~%" (or host "localhost"))
(format t "端口: ~A~%" (or port 8080))
(format t "调试模式: ~A~%" debug))
关键字参数的调用方式非常自由:
(configure-server :port 3000 :host "example.com" :debug t)
(configure-server :debug t) ; 只指定调试模式
(configure-server :host "localhost") ; 只指定主机
(configure-server) ; 全部使用默认值
关键字参数的优势非常明显:
- 调用时可读性强,
:port、:host等标签直接表明参数含义 - 不必记忆参数顺序,可以按任意顺序排列
- 便于以后扩展函数接口,新增关键字参数不会影响现有调用
如果希望关键字参数也有默认值,可以这样定义:
(defun send-email (to &key subject body (priority "normal"))
"发送邮件函数"
(list :to to :subject subject :body body :priority priority))
剩余参数
使用 &rest 可以收集所有剩余参数到列表中。这在需要定义可变参数函数时非常有用。
(defun sum-all (&rest numbers)
"计算所有参数的和"
(apply #'+ numbers))
调用示例:
(sum-all 1 2 3 4 5) ; 返回 15
(sum-all 10 20) ; 返回 30
(sum-all) ; 返回 0
剩余参数通常与其他参数类型组合使用。常见的应用场景包括日志函数、消息格式化等:
(defun log-info (message &rest args)
"记录信息日志"
(apply #'format (append (list t message) args)))
组合使用多种参数类型
在复杂的函数中,经常需要组合使用多种参数类型。Lisp 对参数类型的顺序有严格要求,必须遵循以下顺序:
必选参数 → &optional → &key → &rest
违反这个顺序会导致语法错误。以下是一个综合示例:
(defun create-user (name &optional age &key email city)
"创建用户信息"
(list :name name
:age age
:email email
:city city))
调用方式:
(create-user "王五" 30 :city "北京" :email "wang@example.com")
(create-user "赵六" :city "上海") ; age 使用默认值 nil
(create-user "钱七") ; 使用所有默认值
再看一个更复杂的例子,组合所有参数类型:
(defun process-data (dataset &optional (verbose nil) &key transform filter &rest options)
"处理数据集的通用函数"
(let ((result dataset))
(when transform
(setf result (funcall transform result)))
(when filter
(setf result (remove-if-not filter result)))
(format t "处理完成,数据条数: ~A~%" (length result))
(format t "额外选项: ~A~%" options)
result))
实战示例
以下三个实用函数展示了参数类型的实际应用场景:
示例一:数学统计函数
(defun statistics (&rest numbers)
"返回一组数字的统计信息"
(let ((n (length numbers)))
(when (> n 0)
(list :count n
:sum (apply #'+ numbers)
:average (/ (apply #'+ numbers) n)
:min (apply #'min numbers)
:max (apply #'max numbers)))))
调用结果:
(statistics 10 20 30 40 50)
; 返回: (:COUNT 5 :SUM 150 :AVERAGE 30 :MIN 10 :MAX 50)
示例二:配置管理函数
(defun make-config (&key (env "development") (log-level "info") (timeout 30) (retries 3))
"创建应用配置"
(lambda (key)
(case key
(:env env)
(:log-level log-level)
(:timeout timeout)
(:retries retries)
(otherwise (error "未知配置项: ~A" key)))))
调用方式:
(defvar config (make-config :env "production" :timeout 60))
(funcall config :env) ; 返回 "production"
(funcall config :timeout) ; 返回 60
示例三:构建器模式函数
(defun build-query (&key table columns where &rest extra)
"构建 SQL 查询语句"
(let ((sql (format nil "SELECT ~{~A~^, ~} FROM ~A"
(or columns '(*))
table)))
(when where
(setf sql (format nil "~A WHERE ~A" sql where)))
(dolist (opt extra)
(setf sql (format nil "~A ~A" sql opt)))
sql))
调用示例:
(build-query :table "users" :columns '("id" "name" "email") :where "age > 18")
; 返回: "SELECT id, name, email FROM users WHERE age > 18"
常见错误与调试
在实际开发中,定义函数时容易出现以下错误:
错误一:参数顺序错误
最常见的错误是参数类型顺序颠倒。下面的定义是错误的:
;; 错误写法
(defun bad-example (&key a &optional b c)
...)
正确的顺序应该是:
;; 正确写法
(defun good-example (&optional b c &key a)
...)
错误二:忘记默认值语法
为可选参数指定默认值时,必须使用括号包裹。如果写成这样是错误的:
;; 错误写法
(defun wrong (&optional default-value nil)
...)
应该这样写:
;; 正确写法
(defun correct (&optional (default-value nil))
...)
错误三:关键字参数调用错误
调用关键字参数时,必须使用冒号 : 作为前缀。新手容易忘记:
;; 错误写法
(configure-server port 3000 host "example.com")
;; 正确写法
(configure-server :port 3000 :host "example.com")
调试技巧
- 使用
describe函数:查看函数的参数列表定义 - 使用
arglist宏:获取函数的参数签名 - 添加断言:在函数体开头验证参数类型和值
进阶技巧:参数解构
除了基本的参数类型,Lisp 还支持在参数列表中进行解构,这在处理列表或嵌套结构时非常有用:
(defun process-point (&key ((x px) 0) ((y py) 0))
"处理二维坐标点"
(list :x px :y py :distance (sqrt (+ (* px px) (* py py)))))
调用方式:
(process-point) ; 返回 (:X 0 :Y 0 :DISTANCE 0)
(process-point :x 3 :y 4) ; 返回 (:X 3 :Y 4 :DISTANCE 5)
另一个解构示例,处理坐标点列表:
(defun total-distance (&rest points)
"计算经过所有点的总距离"
(labels ((distance (p1 p2)
(sqrt (+ (expt (- (car p2) (car p1)) 2)
(expt (- (cadr p2) (cadr p1)) 2))))
(calc (pts total)
(if (< (length pts) 2)
total
(calc (cdr pts)
(+ total (distance (car pts) (cadr pts)))))))
(calc points 0)))
总结
defun 是 Lisp 中最常用的函数定义方式。通过灵活组合必选参数、可选参数、关键字参数和剩余参数,可以创建出接口清晰、功能强大的函数。掌握这些参数类型,能够让你的 Lisp 代码更加灵活和易用。

暂无评论,快来抢沙发吧!