【Janet】特殊表达式

元组用来表示函数调用,宏和特殊语法。大多数功能通过函数表现,一部分通过宏,少部分通过特殊语法。特殊语法既不是函数也不是宏,它被编译器用来表示无法用函数或宏表示的底层结构。可以认为特殊语法构成了 Janet 语言的正真核心。在 Janet 种一共有13种特殊语法。

(def name meta... value)

它将值绑定到一个符号。我们可以使用这个符号来表示它后面的表达式的结果。 def 绑定是不可变的,但是同一个符号可以重新绑定到新的值。

lisp 复制代码
(def anumber (+ 1 2 3 4 5))

(print anumber) # prints 15

def 可以使用元组,数组,表或结构体对值进行解包。这让我们可以在一个 def 里面进行多个绑定。

lisp 复制代码
(def [a b c] (range 10))
(print a " " b " " c) # prints 0 1 2

(def {:x x} @{:x (+ 1 2)})
(print x) # prints 3

(def [y {:x x}] @[:hi @{:x (+ 1 2)}])
(print y x) # prints hi3

在全局作用域中, def 也可以给符号添加元数据和文档。在非全局作用域,元数据会被忽略。

lisp 复制代码
(def mydef :private 3) # Adds the :private key to the metadata table.
(def mydef2 :private "A docstring" 4) # Add a docstring

# The metadata will be ignored here because mydef is
# not accessible outside of the do form.
(do
 (def mydef :private 3)
 (+ mydef 1))

(var name meta... value)

def 类似,除了绑定可以修改,其他都与 def 一样。

lisp 复制代码
(var a 1)
(defn printa [] (print a))

(printa) # prints 1
(++ a)
(printa) # prints 2
(set a :hi)
(printa) # prints hi

💡

可变与不可变?它们的本质到底是什么?

defvar 的区别可以理解为指针与变量的区别。 def 就是指针,更换绑定就是让他指向不同的地址,但是每次指向的内存区域都不能修改。 var 就是变量,可以修改它对应的内存的内容。

内存就是个巨大的数组,数组只有两个信息:下标和下标对应的值。所谓变量就是数组的下标,而指针呢,就是这个下标对应的值是另一个下标。

(fn name? args body...)

函数字面量(闭包)。函数字面量由名称(可选),参数列表和函数体组成(其实就是匿名函数)。通过函数名可以更方便的实现递归。参数列表是一个参数名元组,函数体是0或多个表达式式。函数结果是最后一个表达式的值。其他表达式仅用于产生副作用。

函数也引入了一个新的词法作用域,函数内的 defvar 不会逃逸到函数外。

lisp 复制代码
(fn []) # The simplest function literal. Takes no arguments and returns nil.
(fn [x] x) # The identity function
(fn identity [x] x) # The name will make stacktraces nicer
(fn [] 1 2 3 4 5) # A function that returns 5
(fn [x y] (+ x y)) # A function that adds its two arguments.

(fn [& args] (length args)) # A variadic function that counts its arguments.

# A function that doesn't strictly check the number of arguments.
# Extra arguments are ignored.
(fn [w x y z &] (tuple w w x x y y z z))

# For improved debugging, name with symbol or keyword
(fn alice [] :smile)
(fn :bob [] :sit)

(do body...)

执行一系列表达式并返回最后一个表达式的结果。它也会引入一个新的词法作用域。

lisp 复制代码
(do 1 2 3 4) # Evaluates to 4

# Prints 1, 2 and 3, then evaluates to (print 3), which is nil
(do (print 1) (print 2) (print 3))

# Prints 1
(do
 (def a 1)
 (print a))

# a is not defined here, so fails
a

(quote x)

将第一个参数求值为字面量。参数不会被编译,而是在编译后的代码中做为常量使用。 (quote expression) 可以简写为在表达式前加上一个单引号。

lisp 复制代码
(quote 1) # evaluates to 1
(quote hi) # evaluates to the symbol hi
(quote quote) # evaluates to the symbol quote

'(1 2 3) # Evaluates to a tuple (1 2 3)
'(print 1 2 3) # Evaluates to a tuple (print 1 2 3)

(if condition when-true when-false?)

分支结构。第一个表达式是条件,第二个是条件为真时执行的表达式,第三个时条件为假时执行的表达式。如果不提供第三个表达式默认为 nil

if 语句只有在必要时才会求值 when-truewhen-false 表达式,他是惰性求值的,因此不能是函数或宏。

条件表达式只有 nilfalse 被认为是假,其他值都是真。

when-truewhen-false 表达式会在一个新的词法作用域下求值。

lisp 复制代码
(if true 10) # evaluates to 10
(if false 10) # evaluates to nil
(if true (print 1) (print 2)) # prints 1 but not 2
(if 0 (print 1) (print 2)) # prints 1
(if nil (print 1) (print 2)) # prints 2
(if @[] (print 1) (print 2)) # prints 1

(splice x)

splice 可以将数组或元组的内容提取到外面,从而脱去数据结构的包装(也就是把数据结构的衣服给脱了)。它只在两个地方有效,做为函数调用的参数或用于词法构造器,或者做为 unquote 表达式的参数。其他情况下直接返回参数 x 。它的语法糖是在表达式前加上一个分号。 splice 不能嵌套,除非做为 unquote 的参数。

在函数调用中, splice 会将 x 的内容插入函数列表。

lisp 复制代码
(+ 1 2 3) # evaluates to 6

(+ @[1 2 3]) # bad

(+ (splice @[1 2 3])) # also evaluates to 6

(+ ;@[1 2 3]) # Same as above

(+ ;(range 100)) # Sum the first 100 natural numbers

(+ ;(range 100) 1000) # Sum the first 100 natural numbers and 1000

[;(range 100)] # First 100 integers in a tuple instead of an array.

(def ;[a 10]) # this will not work as def is a special form.

注意,这意味着我们几乎不需要 apply 函数,因为 splice 更加灵活。

splice 表达式也可以做为 unquote 表达式的参数,类似于 Common Lisp 中的 unquote-splicing 。使用语法糖可以简写为 ,;some-array-expression

(while condition body...)

while 表达式类似于 C 的 while 循环。循环体会一直运行直到条件为 falsenil 。因此循环体需要包含一些副作用,否则循环将永远进行下去。 while 表达式的结果永远为 nil。它也会引入新的词法作用域。

lisp 复制代码
(var i 0)
(while (< i 10)
 (print i)
 (++ i))

(break value?)

退出 while 循环或函数。 break 表达式只能退出最内层的循环。因为 while 循环永远返回 nil ,可选的 value 参数对 while 循环没有影响,但是当从函数返回时, value 将作为函数的返回值。

break 表达式对于构造宏非常有用。你因该在手写代码中尽量避免使用它,尽管它对于提前退出的情况非常有用(还是优先尝试使用 cond 宏吧)。

lisp 复制代码
# Breaking from a while loop
(while true
 (def x (math/random))
 (if (> x 0.95) (break))
 (print x))
lisp 复制代码
# Early exit example using (break)
(fn myfn
 [x]
 (if (= x :one) (break))
 (if (= x :three) (break x))
 (if (and (number? x) (even? x)) (break))
 (print "x = " x)
 x)

# Example using (cond)
(fn myfn
 [x]
 (cond
  (= x :one) nil
  (= x :three) x
  (and (number? x) (even? x)) nil
  (do
   (print "x = " x)
   x)))

(set l-value r-value)

l-value 的值更新为 r-valueset 表达式的结果是 r-value

r-value 可以是任何表达式, l-value 应该是已绑定的变量或一个数据结构和键。这让它可以表现的像 Common Lisp 中的 setfsetq

lisp 复制代码
(var x 10)
(defn prx [] (print x))
(prx) # prints 10
(set x 11)
(prx) # prints 11
(set x nil)
(prx) # prints nil

(def tab @{})
(set (tab :property) "hello")
(pp tab) # prints @{:property "hello"}

(quasiquote x)

类似于 (quote x) ,但是允许 x 内有 unquote 。这让 quasiquote 对于写宏非常有用,因为宏定义常会生成大量具有自定义值的模板代码。它的语法糖是在表达式前加上 ~ 。有了它, (unquote x) 会求值并将 x 插入 unquote 表达式。 (unquote x) 的语法糖是 ,x

(unquote x)

Unquote quasiquote 中的表达式。quasiquote 之外的 unquote 是无效的。

(upscope & body)

类似于 doupscope 会求值多个表达式并以最后一个表达式的值做为结果。但是它不会创建新的词法作用域,也就是说 upscope 里面创建的绑定在它定义的作用域内是可见的。它可以在书写宏时同时定义多个 defvar

lisp 复制代码
(upscope
 (def a 1)
 (def b 2)
 (def c 3)) # -> 3

(+ a b c) # -> 6

通常这个宏应该做为最后的手段,我们有其他更好的方式,包括使用解包。

lisp 复制代码
(def [a b c] [1 2 3])