函数应用由一个项后接一个或多个实参组成;也可以后接零个或多个实参,并以省略号结尾。
term ::= ... | term argument+ | term argument* ..
通常,函数应用以并置方式书写:实参放在函数之后,二者之间至少有一个空格。 在 Lean 的类型论中,所有函数都恰好接受一个实参并产生一个值。 每个函数应用都将一个函数与一个实参组合起来。 多个实参通过柯里化表示。
高层项语言将函数及其一个或多个实参视为一个整体,除普通位置实参外,还支持隐式实参、可选实参和具名实参等附加功能。 精译器会将这些转换为核心类型论中更简单的模型。
函数应用由一个项后接一个或多个实参组成;也可以后接零个或多个实参,并以省略号结尾。
term ::= ... | term argument+ | term argument* ..
函数实参可以是项,也可以是具名实参。
argument ::= ... | term | ((ident | _:ident) :=term)
函数的核心语言类型决定实参在最终表达式中的位置。 函数类型包含其预期参数的名称。 在 Lean 的核心语言中,非依赖函数类型编码为参数名称不出现在类型体中的依赖函数类型。 此外,这些名称由内部选取,无法写作具名实参的名称;这对于防止意外捕获十分重要。
函数预期的每个参数都有名称。 递归遍历函数的实参类型时,按以下方式从实参序列中选择实参:
若参数名称与某个具名实参提供的名称匹配,则选择该实参。
若参数是隐式参数,则创建并选择一个具有该参数类型的新元变量。
若参数是实例隐式参数,则创建并插入一个具有该参数类型的新实例元变量。实例元变量会被安排稍后合成。
若参数是严格隐式参数,且仍有尚未选择的具名或位置实参,则创建并选择一个具有该参数类型的新元变量。
若参数是显式参数,则选择并精译下一个位置实参。若没有位置实参:
有一种特殊情况:当函数应用出现在模式中且存在省略号时,可选实参和自动实参会变为通配模式(_),而不是被插入。
若类型不是函数类型但仍有实参剩余,则会报错。
插入所有实参后,若存在省略号,则所有缺失的实参都会设为新元变量,如同它们是隐式实参一样。
若为缺失的显式位置实参创建了新变量,则整个应用会包裹在绑定这些变量的 Lean.Parser.Term.fun : termfun 项中。
最后调用实例合成,并尽可能求解更多元变量:
推断整个函数应用的类型。类型推断期间发生的合一可能会求解某些元变量。
合成实例元变量。仅当推断类型是某个实例的输出参数元变量时,才使用默认实例。
若存在预期类型,则将其与推断类型合一;但会丢弃此次合一产生的错误。若预期类型与推断类型可能相等,合一就能求解剩余的隐式实参元变量。若二者不可能相等,也不会抛出错误,因为外围精译器或许能插入强制转换或单子提升。
可以使用 Lean.Parser.Command.check : command#check 命令查看为函数调用插入了哪些实参。
函数 sum3 接受三个显式的 Nat 参数,名称分别为 x、y 和 z。
def sum3 (x y z : Nat) : Nat := x + y + z
三个实参都可以按位置提供。
#check sum3 1 3 8
它们也可以按名称提供。
#check sum3 (x := 1) (y := 3) (z := 8)
按名称提供实参时,可以采用任意顺序。
#check sum3 (y := 3) (z := 8) (x := 1)
具名实参与位置实参可以自由混用。
#check sum3 1 (z := 8) (y := 3)
具名实参与位置实参可以自由混用。 若按名称提供了实参,就会使用该实参,即使它出现在本可使用的位置实参之后。
#check sum3 1 (x := 8) (y := 3)
若要在尚未提供的实参之后插入具名实参,则会创建一个已填入所提供实参的函数。
#check sum3 (z := 8)
在幕后,实参名称会保留在函数类型中。 这意味着其余实参仍可再次按名称传递。
#check (sum3 (z := 8)) (y := 1)
参数名称取自函数的类型,函数参数所用名称不必与类型中使用的名称匹配。 这意味着,与参数名称冲突的局部绑定不会妨碍具名参数的使用,因为 Lean 会重命名函数参数以避免冲突,同时保持类型中的名称不变。
#check let x := 15; sum3 (z := x)
这里,用于命名 sum3 第一个实参的 x 已被替换,以免与外围的 Parser.Term.letlet 冲突:
尽管 x 已被重命名,仍可按名称传递它:
#check (let x := 15; sum3 (z := x)) (x := 4)
这是因为类型中仍使用名称 x。
启用选项 pp.piBinderNames 可显示类型中的参数名称:
set_option pp.piBinderNames true in
#check let x := 15; sum3 (z := x)
可选参数和自动参数并非 Lean 核心类型论的一部分。
它们使用 optParam 和 autoParam 辅助机制进行编码。
用于支持可选参数的辅助类型。
声明中的绑定器 (x : α := default) 是 x : optParam α default 的语法糖;若调用处没有
提供该参数,精译器会尝试使用 default 作为实参。
关于结构字段的小节介绍了从类型为结构的项中投影字段的表示法。
广义字段表示法由一个项、一个点号(.)和一个标识符依次组成,三者之间不能有空格。
term ::= ...
| term.ident如果一个项的类型是应用于零个或多个参数的常量,那么无论该项是不是拥有字段的结构或类型类实例,都可以用字段表示法将一个函数应用于它。 使用字段表示法应用其他函数称为广义字段表示法。
点号后的标识符会在该项类型的命名空间中查找,也就是在这个常量名称所对应的命名空间中查找。
如果类型不是常量的应用(例如,它是一个元变量或宇宙),那么它就没有命名空间,因而不能使用广义字段表示法。
特别地,如果表达式是函数,广义字段表示法会在 Function 命名空间中查找。因此,Nat.add.uncurry 是广义字段表示法的一种用法,它等价于 Function.uncurry Nat.add。
如果找不到该字段,但可以展开这个常量,得到另一个常量或常量应用类型,那么就用新的常量重复这一过程。
找到函数后,点号前的项会成为该函数的一个参数。 具体而言,它会成为第一个不会导致类型错误的显式参数。 除此之外,该应用会照常精译。
类型 Username 是常量,因此可以用广义字段表示法,将 Username 命名空间中的函数应用于类型为 Username 的项。
def Username := String
Username.validate 就是这样的函数之一,它检查用户名是否没有前导空白,且是否只使用了少量允许的字符。
在其定义中,广义字段表示法用于调用函数 String.isPrefixOf、String.any、Char.isAlpha 和 Char.isDigit。
String.isPrefixOf 接受两个 String 参数;在这里," " 用作第一个参数,因为它是点号前的项。
虽然 name 的类型是 Username,但仍可用广义字段表示法对它调用 String.any,这是因为 Username.any 没有定义,而 Username 可展开为 String。
def Username.validate (name : Username) : Except String Unit := do
if " ".isPrefixOf name then
throw "Unexpected leading whitespace"
if name.any notOk then
throw "Unexpected character"
return ()
where
notOk (c : Char) : Bool :=
!c.isAlpha &&
!c.isDigit &&
!c ∈ ['_', ' ']
def adminUser : Username := "admin"
然而,不能用字段表示法对 "admin" 调用 Username.validate,因为 String 不会展开为 Username。
#eval "admin".validate
另一方面,adminUser 的类型是 Username,因此可以用广义字段表示法调用 Username.validate 函数:
#eval adminUser.validate
反过来,确实可以用广义字段表示法对 Username 值 adminUser 调用 String.any,因为类型 Username 可展开为 String。
#eval adminUser.any (· == 'm')
pp_nodot 属性使 Lean 的美化打印器在打印函数时不使用字段表示法。
attr ::= ... | pp_nodot
管道语法提供了函数应用的其他写法。 重复使用管道时,可借助解析优先级把函数依次应用于位置参数,而不必使用嵌套括号。
右管道表示法把管道右侧的项应用于左侧的项。
term ::= ...
| term |> term左管道表示法把管道左侧的项应用于右侧的项。
term ::= ...
| term <| term右管道表示法背后的直观理解是:左侧的值被送入第一个函数,其结果再送入第二个函数,以此类推。 在左管道表示法中,右侧的值向左传递。
右管道可以在一个项上依次调用一系列函数。 对读者而言,它往往更强调正在变换的数据。
#eval "Hello!" |> String.toList |> List.reverse |> List.head!
e |>.f arg 是 (e).f arg 的另一种语法。