【Janet】函数
Janet 是函数式语言,这意味着构成程序的成分之一就是定义函数(另一个是数据机构)。因为 Janet 是类 Lisp 语言,函数也是值,和数字或字符串一样,它们可以被传递和按需创建。
函数由 defn 宏定义,如下:
(defn triangle-area"Calculates the area of a triangle."[base height](print "calculating area of a triangle...")(* base height 0.5))
通过 defn 定义的函数包括一个名称,一些可选的标志,最后是一个函数体。上例中函数名叫 triangle-area,接受两个参数,分别叫做 base 和 height。函数体会打印一个消息并计算三角形的面积。
函数一旦定义,就可以像 print 或 + 一样被使用。
# Prints "calculating area of a triangle..." and then "25"
(print (triangle-area 5 10))
当函数嵌套调用时,内层函数首先求值。此外,函数参数从第一个到最后一个按顺序求值。因为函数是一等公民,就像数字或字符串一样,它也可以做为函数参数传递。
(print triangle-area)
这会打印 triangle-area 函数在内存中地址。
函数也可以是匿名的, fn 关键字用来定义函数字面量,它可以不绑定到任何符号。
# Evaluates to 40
((fn [x y] (+ x x y)) 10 20)
# Also evaluates to 40
((fn [x y &] (+ x x y)) 10 20)# Will throw an error about the wrong arity
((fn [x] x) 1 2)
# Will not throw an error about the wrong arity
((fn [x &] x) 1 2)
第一个表达式创建了一个匿名函数,将第一个参数的二倍加上第二个参数,然后使用 10 和 20 调用该函数。它返回 (10 + 10 + 20) = 40。
另一种创建匿名函数的方式是使用 | 读取器宏(等价于 short-fn 宏)。
# These are functionally equivalent
((fn [x] (+ x x)) 1) # -> 2
(|(+ $ $) 1) # -> 2# Multiple arguments may be referenced
(|(+ $0 $1 $2) 1 2 3) # -> 6# All arguments can be accessed as a tuple
(|(map inc $&) -1 0 1 2) # -> @[0 1 2 3]# | is shorthand for (short-fn ...)
((short-fn (+ $ $)) 1) # -> 2# Other constructs can be used after |
(|[:x $] :y) # -> [:x :y]
(|{:a $0 :b $1} 1 2) # -> {:a 1 :b 2}# Will throw an error about the wrong arity
(|(inc $) 1 2)
# Will not throw an error about the wrong arity
(|(get $& 0) 1 2)
在上例中 $ 符号表示第一个参数。同样 $0, $1 等分别表示对应位置的参数。 $& 表示剩余参数,以元组的形式。
还有一个常用的宏 defn 用来创建函数并立刻绑定到一个名字。它可以用于顶层和其他表达式中。类似的还有 defmacro 宏用来定义宏。
(defn myfun [x y](+ x x y))# You can think of defn as a shorthand for def and fn together
(def myfun-same (fn [x y](+ x x y)))(myfun 3 4) # -> 10
Janet 为你提供了很多宏(也可以自定义)。宏是接受源码做为参数并转化成其他源码的函数,常用来自动生成一些重复的模式。
可选参数
大多数 Janet 函数在没有传递正确数量的参数时都会报错。如果你想定义一个有可选参数的函数,当参数没有传递时取默认值。Janet 通过在参数列表中使用 &opt 符号来支持可选参数, &opt 之后的参数如果没有提供默认为 nil 。
(defn my-opt-function"A dumb function with optional arguments."[a b c &opt d e f](default d 10)(default e 11)(default f 12)(+ a b c d e f))
default 宏用来给参数设置默认值。如果参数是 nil, default 会将它重新定义为默认值。
可变参数
Janet 原生支持可变参数函数。可变参数函数可以接受任意数量的函数,并将它们放入一个元组。在参数列表中使用 & 符号定义可变参数函数, & 之前的参数不是可选的,除非使用 &opt 符号指定。
(defn my-adder"Adds numbers in a dubious way."[& xs](var accum 0)(each x xs(+= accum (- (* 2 x) x)))accum)(my-adder 1 2 3) # -> 6
忽略额外参数
如果你需要一个函数可以接受额外的参数但是不使用它们,这种情况可能发生在函数实现了某个接口但是它本身并不需要这些参数。在参数列表的最后加上 & 可以让函数丢弃多余的参数。
(defn ignore-extra[x &](+ x 1))(ignore-extra 1 2 3 4 5) # -> 2
关键字风格参数
当一个函数有很多参数,调用时有不给参数名称,就容易让人迷惑。一个解决方案时使用表或者结构体来传递所有参数。通常这是一个好办法,因为现在你可以通过结构体的键来区分每个参数了。
(defn make-recipe"Create some kind of cake recipe..."[args](def dry [(args :flour) (args :sugar) (args :baking-soda)])(def wet [(args :water) (args :eggs) (args :vanilla-extract)]){:name "underspecified-cake" :wet wet :dry dry})# Call with an argument struct
(make-recipe{:flour 1:sugar 1:baking-soda 0.5:water 2:eggs 2:vanilla-extract 0.5})
这通常很不错,但是也有一些不好的地方。首先是具体的参数没有文档,文档中只有一个帮助不大的 “args” 参数。文档中应该指示出结构体中期望的键。其次在调用函数时需要写一对括号来创建结构体,虽然这不是什么大事,但没有括号的话会更好。
第一个问题我们可以通过在参数中使用模式匹配(destructuring,我觉得就是模式匹配)来解决。
(defn make-recipe-2"Create some kind of cake recipe with destructuring..."[{:flour flour:sugar sugar:baking-soda soda:water water:eggs eggs:vanilla-extract vanilla}](def dry [flour sugar soda])(def wet [water eggs vanilla]){:name "underspecified-cake" :wet wet :dry dry})# We can call the function in the same manner as before.
(make-recipe-2{:flour 1:sugar 1:baking-soda 0.5:water 2:eggs 2:vanilla-extract 0.5})
优化后的函数文档中会包含函数需要的参数列表。要解决第二个问题我们可以在函数的参数列表中使用 &keys 符号来自动在调用时创建结构体。
(defn make-recipe-3"Create some kind of recipe using &keys..."[&keys {:flour flour:sugar sugar:baking-soda soda:water water:eggs eggs:vanilla-extract vanilla}](def dry [flour sugar soda])(def wet [water eggs vanilla]){:name "underspecified-cake" :wet wet :dry dry})# Calling this function is a bit different now - no struct
(make-recipe-3:flour 1:sugar 1:baking-soda 0.5:water 2:eggs 2:vanilla-extract 0.5)
现在这个版本要比之前的两个版本看起来清晰多了,但是有个需要注意的地方。将所有参数打包成一个结构体通常更有用,因为结构体可以被高效的传递并贯穿整个应用。需要很多参数的函数通常会将这些参数传递给子程序,因此在实践中这是很常见的需求。 &keys 语法常用于更注重简洁和可读性的场景。
如果需要,两种调用风格也可以相互转换。将结构体传递给 &keys 风格的参数可以使用 kvs 函数。反过来,只需要将参数包裹进大括号中即可。
(def args{:flour 1:sugar 1:baking-soda 0.5:water 2:eggs 2:vanilla-extract 0.5})# Turn struct into an array key, value, key, value, ... and splice into a call
(make-recipe-3 ;(kvs args))
注意 &keys 后面的不一定要是一个结构体,也可以是一个单独的符号,在函数内可以通过这个符号做为结构体的名称访问参数传递的键值对。
(defn feline-counter[&keys animals](if-let [n-furries (get animals :cat)](printf "Awww, I see %d cat(s)!" n-furries)(print "Shucks, where are my friends?")))(feline-counter :ant 1 :bee 2 :cat 3)
可选标志
做为一种风格,一些 Janet 函数允许你通过单个参数传递多个可选的标志。
例如,有个 foo 函数接受一个字符串参数,还可以传递 :a, :b 和 :c 中的一个或多个标志,你可以使用以下形式调用 foo 函数:
(foo "hi") # No flags passed.
(foo "hi" :a) # Just the `:a` flag.
(foo "hi" :ab) # The `:a` and `:b` flags.
(foo "hi" :ba) # Same.
(foo "hi" :cab) # All three flags specified.
这个模式类似于在 shell 中给命令传递多个标志:
$ my-command -abc # is like
$ my-command -a -b -c
只不过在 Janet 中使用的是 : 而不是 -。下面是一个使用可选标志的示例:
#!/usr/bin/env janet(defn my-cool-string``Decorates `some-str` with stars around it.`flags` is set of flags; it is a keyword where eachcharacter indicates a flag from this set:* `:u` --- uppercase the string too.* `:b` --- put braces around it as well.Returns the decorated string.``[some-str &opt flags](var u-flag-set false)(var b-flag-set false)(when flags(set u-flag-set (string/check-set flags :u))(set b-flag-set (string/check-set flags :b)))(let [s (string "*" some-str "*")s (if u-flag-set (string/ascii-upper s) s)s (if b-flag-set (string "{" s "}") s)]s))(print (my-cool-string "I ♥ Janet")) # *I ♥ Janet*
(print (my-cool-string "I ♥ Janet" :u)) # *I ♥ JANET*
(print (my-cool-string "I ♥ Janet" :b)) # {*I ♥ Janet*}
(print (my-cool-string "I ♥ Janet" :ub)) # {*I ♥ JANET*}
使用这种风格的内置函数包括 file/open, fiber/new 和 os/execute。
命名参数
从 1.23.0 开始,函数支持命名参数。这是一个比关键字参数更方便的语法。有命名参数的函数也是可变参数的,函数不会拒绝命名错误的参数。所有 &named 符号之后的参数在传递时需要在参数前加上同名关键字。
(defn named-fn[x &named person1 person2](print "giving " x " to " person1)(print "giving " x " to " person2))(named-fn "apple" :person1 "bob" :person2 "sally")
