文章目录

Lisp 函数定义:defun 与参数

发布于 2026-04-05 12:33:05 · 浏览 14 次 · 评论 0 条

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)                    ; 全部使用默认值

关键字参数的优势非常明显:

  1. 调用时可读性强,:port:host 等标签直接表明参数含义
  2. 不必记忆参数顺序,可以按任意顺序排列
  3. 便于以后扩展函数接口,新增关键字参数不会影响现有调用

如果希望关键字参数也有默认值,可以这样定义:

(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")

调试技巧

  1. 使用 describe 函数:查看函数的参数列表定义
  2. 使用 arglist:获取函数的参数签名
  3. 添加断言:在函数体开头验证参数类型和值

进阶技巧:参数解构

除了基本的参数类型,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 代码更加灵活和易用。

评论 (0)

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

扫一扫,手机查看

扫描上方二维码,在手机上查看本文