Featured image of post Clojure 基础语法 —— 变量和函数

Clojure 基础语法 —— 变量和函数

本篇文章主要介绍 Clojure 的变量类型,变量定义,函数定义,函数重载和多参数处理以及宏定义的基本方法。

Clojure中的数据类型详解

Clojure作为一门函数式编程语言,提供了一组丰富的数据类型。下面我将详细介绍Clojure中的核心数据类型及其特性。

基本数据类型

数值(Number)类型

1
2
3
4
5
42      ; 整数
3.14    ; 浮点数
1/3     ; 分数(Ratio)
0xff    ; 十六进制
2r1010  ; 二进制

原子(Atom)类型

1
2
3
4
:keyword ; 关键字(常作为键使用)
'symbol  ; 符号(表示标识符)
true     ; 布尔值
nil      ; 空值

集合数据类型

字符串(String)

1
2
"Hello, Clojure!"  ; 双引号包裹
\A                 ; 单个字符(Java char)

Clojure字符串就是Java字符串,支持多行:

1
2
"第一行
第二行"

列表(List) - 不可变链表

1
2
'(1 2 3 4)       ; 使用单引号
(list 1 2 3 4)   ; 使用list函数
  • 链表结构,头部插入高效
  • 不可变
  • 常用作函数调用形式或者数据序列

向量(Vector) - 不可变数组

1
2
[1 2 3 4]        ; 方括号表示
(vector 1 2 3 4) ; 使用vector函数
  • 基于数组,随机访问高效(O(1))
  • 不可变
  • 常用作有序集合存储

集合(Set) - 不可变无序不重复集合

1
2
3
#{1 2 3 4}       ; 花括号前加#
(hash-set 1 2 3) ; 使用hash-set函数
(sorted-set 4 2) ; 排序集合→ #{2 4}
  • 保证元素唯一
  • 查找速度快(O(1))
  • 无序存储

字典(Map) - 键值对集合

1
2
3
{:name "Alice" :age 30}     ; 花括号表示
(hash-map :a 1 :b 2)       ; 使用hash-map函数
(sorted-map :b 2 :a 1)     ; 排序Map→ {:a 1 :b 2}
  • 键通常是关键字(keyword)
  • 键的唯一性保证
  • 支持嵌套结构

特殊数据类型

记录(Record)

1
2
3
(defrecord Person [name age])
(def alice (->Person "Alice" 30))
(:name alice) ; 访问字段→ "Alice"
  • 类似结构体
  • 固定字段集合
  • 实现Map接口

数组(Array)

1
2
(make-array Integer 3) ; Java数组
(int-array [1 2 3])    ; 基本类型数组
  • 实际上是Java数组的包装
  • 可变(mutable)
  • 主要用于Java互操作

Clojure强调不可变数据,所有内置集合类型默认都是不可变的。要使用可变状态,需要显式使用atomref等引用类型来包装这些不可变数据结构。

Clojure基础语法:变量定义

Clojure作为一门Lisp方言的函数式编程语言,其变量定义方式与主流命令式语言有很大不同。以下是Clojure中主要的变量定义方式:

def - 定义全局变量

def用于定义全局命名空间内的变量:

1
2
3
(def PI 3.14159)  ;; 定义一个常量
(def company-name "TechCorp") ;; 定义字符串变量
(def primes [2 3 5 7 11]) ;; 定义集合

注意:

  • def定义的变量是全局可访问的
  • 虽然技术上可以修改(使用alter-var-root),但惯例上应视为不可变
  • 命名通常使用连字符,如user-name而非userName

let - 定义局部绑定

let用于创建局部作用域的临时绑定:

1
2
3
(let [radius 5
      pi 3.14159]
  (* pi (* radius radius)))  ;; 计算圆面积

特点:

  • 绑定只在let块内有效
  • 绑定是并行赋值的(后面表达式不能依赖前面)
  • 常用在函数内部定义临时变量

defn - 定义函数

虽然主要是定义函数,但也是一种变量绑定方式:

1
2
3
4
5
(defn greet [name]
  (str "Hello, " name "!"))

;; 等价于:
(def greet (fn [name] (str "Hello, " name "!")))

动态变量 (带星号)

Clojure支持动态作用域变量:

1
2
3
4
(def ^:dynamic *debug* false)

(binding [*debug* true]
  (println "Debug mode:" *debug*))

惯例:

  • 动态变量名称用*包围,如*out*
  • 使用binding建立新的动态绑定

重要原则

  1. 不可变性:Clojure中的变量一旦定义,其值不应改变
  2. 避免全局状态:尽量使用局部绑定而非全局变量
  3. 线程安全:得益于不可变性,Clojure变量天生线程安全

这些是Clojure变量系统的基础部分,体现了其函数式编程的核心思想。

Clojure函数定义简介

Clojure作为一门函数式编程语言,函数是它的核心构建块。下面介绍Clojure中定义函数的主要方式:

基本函数定义(defn)

使用defn宏定义命名函数:

1
2
3
4
5
(defn function-name
  "可选的文档字符串"
  [param1 param2]  ; 参数向量
  (println param1 param2)  ; 函数体
  (+ param1 param2))  ; 隐式返回最后一个表达式

匿名函数(fn)

定义不绑定名称的函数:

1
2
(fn [x y] 
  (+ x y))

或使用简写形式:

1
#(+ %1 %2)  ; %1表示第一个参数,%2表示第二个

多参数函数

Clojure支持可变参数:

1
2
(defn sum [& nums]  ; nums是序列
  (apply + nums))

多分派函数(defmulti/defmethod)

基于参数值选择不同实现:

1
2
3
4
(defmulti greet :language)  ; 根据:language分派

(defmethod greet :english [_] "Hello!")
(defmethod greet :spanish [_] "¡Hola!")

高阶函数

函数可以作为参数或返回值:

1
2
3
4
(defn apply-twice [f x]
  (f (f x)))

(apply-twice #(* % 2) 3)  ; 返回12

Clojure的函数是不可变的一等公民,这种设计支持函数组合和复用,是函数式编程的基础。

Clojure 函数重载与多参数处理综合指南

Clojure 提供了多种方式来实现类似其他语言中的函数重载功能,包括基于参数数量、类型和任意条件的分派方法。以下是综合介绍:

基础函数多态(Multiple Arities)

最简单直接的"重载"方式是利用 Clojure 的函数多参数定义:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(defn process
  "基础多参数实现"
  ([] (println "无参数调用"))
  ([x] (println "单参数:" x))
  ([x y] (println "双参数:" x y))
  ([x y & z] (println "变长参数:" x y z)))

;; 使用示例
(process)            ; 无参数调用
(process 10)         ; 单参数: 10
(process 10 20)      ; 双参数: 10 20
(process 1 2 3 4)    ; 变长参数: 1 2 (3 4)

特点

  • 最简洁的实现方式
  • 编译时确定调用版本
  • 性能最优

多重方法(Multimethods)

基于参数数量的分派

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
(defmulti handle-request 
  "基于参数数量分派"
  (fn [& args] (count args)))

(defmethod handle-request 0 [_]
  "处理无参数请求")

(defmethod handle-request 1 [[x]]
  (str "处理单请求: " x))

(defmethod handle-request 2 [[x y]]
  (str "处理双请求: " x "和" y))

(defmethod handle-request :default [args]
  (str "处理复杂请求: " (pr-str args)))

基于参数类型的分派

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
(defmulti calculate
  "基于参数类型分派"
  (fn [x y] [(class x) (class y)]))

(defmethod calculate [Number Number] [x y]
  (+ x y))

(defmethod calculate [String String] [x y]
  (str x y))

(defmethod calculate [Number String] [x y]
  (str x y))

层级分派(Hierarchical Dispatch)

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
(defmulti render (fn [shape] (:type shape)))

;; 定义类型层次关系
(derive ::circle ::shape)
(derive ::square ::shape)

(defmethod render ::circle [_]
  "绘制圆形")

(defmethod render ::square [_]
  "绘制方形")

(defmethod render ::shape [_]
  "默认形状绘制")

协议与记录(Protocols & Records)

适用于面向对象风格的重载:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
(defprotocol DataProcessor
  (process-data [this])
  (process-data [this input])
  (process-data [this input options]))

(defrecord DefaultProcessor []
  DataProcessor
  (process-data [this] "无参数处理")
  (process-data [this input] (str "处理输入: " input))
  (process-data [this input options]
    (str "处理输入 " input " 带选项 " options)))

(let [p (DefaultProcessor.)]
  [(process-data p)
   (process-data p "data")
   (process-data p "data" {:format :json})])

综合使用示例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
;; 定义形状协议
(defprotocol Shape
  (area [this])
  (perimeter [this]))

;; 实现圆形
(defrecord Circle [radius]
  Shape
  (area [this] (* Math/PI radius radius))
  (perimeter [this] (* 2 Math/PI radius)))

;; 实现矩形 - 使用多参数构造
(defn make-rectangle
  ([side] (make-rectangle side side))  ; 正方形
  ([width height] (Rectangle. width height)))

(defrecord Rectangle [width height]
  Shape
  (area [this] (* width height))
  (perimeter [this] (* 2 (+ width height))))

;; 定义针对形状的多重方法
(defmulti draw :type)

(defmethod draw :circle [{r :radius}]
  (str "绘制圆形,半径 " r))

(defmethod draw :rectangle [{w :width h :height}]
  (str "绘制矩形 " w "x" h))

选择指南

特性 多参数函数 多重方法 协议
参数数量分派
参数类型分派
自定义条件分派
性能 最优 中等
扩展性 有限 中等
代码组织 简单 灵活 结构化

推荐选择

  • 优先使用简单的多参数函数实现基础重载
  • 需要复杂分派逻辑时使用多重方法
  • 定义数据类型行为时使用协议

Clojure 的这些特性共同构成了比传统面向对象语言更灵活的多态系统,开发者可以根据具体需求选择最合适的实现方式。

Clojure宏:代码的代码

Clojure宏(Macro)是一种元编程工具,允许你在编译时对代码进行转换,而不是在运行时处理数据。宏是Lisp家族语言最强大的特性之一,让语言具有真正的可扩展性。

宏的基本概念

宏与普通函数的关键区别:

  1. 执行时机:函数在运行时执行,宏在编译时展开
  2. 参数处理:函数会先对参数求值,宏接收未求值的表达式

定义宏

使用defmacro定义宏:

1
2
3
(defmacro macro-name [params]
  ; 返回可以求值的表达式
  )

简单宏示例

unless宏

1
2
3
4
5
6
7
8
(defmacro unless [condition & body]
  `(if (not ~condition)
     (do ~@body)))

; 使用
(unless (= 2 (+ 1 1))
  (println "Math is broken!")
  (println "This should never print"))

这个宏实现了与if not相反的逻辑,展开后变为:

1
2
3
4
(if (not (= 2 (+ 1 1)))
  (do 
    (println "Math is broken!")
    (println "This should never print")))

infix宏

1
2
3
4
5
6
7
(defmacro infix [form]
  (list (second form) 
        (first form) 
        (nth form 2)))

; 使用
(infix (2 + 3)) ; => 5

这个宏允许使用中缀表示法(如2 + 3)进行数学运算。

高级宏技术

语法引号与解引

在宏中常用反引号(`)创建模板,使用~解引用符号:

1
2
3
4
(defmacro debug [expr]
  `(let [result# ~expr]
     (println "Debug:" '~expr "=>" result#)
     result#))

result#会自动生成唯一的符号名,避免名称冲突。

使用macroexpand理解宏

1
2
(macroexpand '(unless true (println "No")))
; 输出:(if (clojure.core/not true) (do (println "No")))

实际应用案例

性能计时宏

1
2
3
4
5
6
7
8
(defmacro time-exec 
  "执行表达式并返回执行时间和结果"
  [expr]
  `(let [start# (System/currentTimeMillis)
         result# ~expr
         end# (System/currentTimeMillis)]
     {:result result#
      :time (- end# start#)}))

领域特定语言(DSL)

宏可以创建自定义语法:

1
2
3
4
5
6
7
(defmacro define-route [method path args & body]
  `(defroutes ~(symbol (str method "-" (name path)))
     (~method ~path ~args 
       (do ~@body))))

(define-route GET "/user/:id" [id]
  (str "User ID: " id))

宏的最佳实践

  1. 优先使用函数:能用函数解决的不要用宏
  2. 保持简单:宏应该聚焦在语法转换上
  3. 避免意外捕获变量:使用自动生成的符号名(如var#)
  4. 文档齐全:宏比函数更难理解,需要更好的文档

总结

Clojure宏提供了一种强大的代码生成机制,允许你扩展语言本身。虽然强大,但也需要谨慎使用。理解宏的关键是记住它们在编译阶段处理代码,而不是在运行时处理数据。

通过合理使用宏,可以消除重复代码、创建领域特定语言,以及实现超出核心语言语法的功能。

Licensed under CC BY-NC-SA 4.0
最后更新于 Mar 06, 2022 00:00 UTC
使用 Hugo 构建
主题 StackJimmy 设计