语法扩展

Idris 支持以多种方式实现 嵌入式领域特定语言(Embedded Domain Specific Language, EDSL) [1] 。 我们见过的一种方式是扩展 do 记法。另一种重要的方式就是对核心语法进行扩展。 在本节中,我们描述了两种扩展语法的方式:syntax 规则与 dsl 记法。

syntax 规则

我们已经见过 if...then...else 表达式了,然而它并不是内建的。同样, 我们可以定义一个 Prelude 中的函数(它在 惰性 一节中出现过了):

ifThenElse : (x:Bool) -> Lazy a -> Lazy a -> a;
ifThenElse True  t e = t;
ifThenElse False t e = e;

接着用 syntax 声明来扩展核心语法:

syntax if [test] then [t] else [e] = ifThenElse test t e;

syntax 声明的左式描述了语法规则,而右式描述了其展开式。语法规则的构成为:

  • 关键字 — 在这里为 ifthenelse,它必须是有效的标识符。
  • 非终止符(Non-terminal) — 位于方括号内,此处为 [test][t][e],它们表示任意表达式。为避免解析歧义,这些表达式不能在顶层使用语法扩展 (也就是说你可以在括号中使用)。
  • 名称 — 位于大括号内,它表示可在右侧被绑定的名字。
  • 符号 — 位于引号内,例如 ":="。它也可在语法规则中包含保留字,例如 "let""in"

语法规则形式的限制在于它必须包含至少一个符号或关键字,且表示非终止符的变量不能重复。 任何表达式都可以使用,不过如果在一个规则的同一行内有两个非终止符,那么只有简单的表达式会被使用 (即,变量、常量或方括号括起的表达式)。规则可在其定义之前使用,但无法递归地使用。 因此以下语法扩展是有效的:

syntax [var] ":=" [val]                = Assign var val;
syntax [test] "?" [t] ":" [e]          = if test then t else e;
syntax select [x] from [t] "where" [w] = SelectWhere x t w;
syntax select [x] from [t]             = Select x t;

语法宏可被进一步限制为只能在模式(即,只能在模式匹配从句的左侧)中或只能在被标为 patternterm 语法规则的项(即除了模式匹配从句左侧的任何地方)中应用。 例如,假设我们定义了一个区间 Interval,用 So 静态检查以保证下界小于上界:

data Interval : Type where
   MkInterval : (lower : Double) -> (upper : Double) ->
                So (lower < upper) -> Interval

我们可以用 pattern 定义一个语法,它总是匹配 Oh 作为证明论据,用 term 请求提供一个证明项:

pattern syntax "[" [x] "..." [y] "]" = MkInterval x y Oh
term    syntax "[" [x] "..." [y] "]" = MkInterval x y ?bounds_lemma

term 中,语法 [x...y] 会生成一个证明义务 bounds_lemma (可能被重命名)。

最后,语法规则可引入另一种绑定形式。例如,for 循环在每次迭代中绑定一个参数:

syntax for {x} "in" [xs] ":" [body] = forLoop xs (\x => body)

main : IO ()
main = do for x in [1..10]:
              putStrLn ("Number " ++ show x)
          putStrLn "Done!"

注意,我们用形式 {x} 指明 x 表示一个已绑定的变量,它在右侧会被替换。 我们还把 in 放在了引号中,因为它是保留字。

dsl 记法

示例:良类型的解释器 一节中的良类型解释器是个依赖类型编程模式的简单例子。也就是说: 先用依赖类型描述一个 目标语言 及其类型系统,保证只有良类型的程序可被表示, 然后再通过这种方式表示程序。通过这种方式,我们可以编写序列化二进制数据 [2] 或安全运行并发过程 [3] 的程序。

然而,目标语言的形式使其难以在实践中编程。回想一下用 Expr 编写的阶乘程序:

fact : Expr G (TyFun TyInt TyInt)
fact = Lam (If (Op (==) (Var Stop) (Val 0))
               (Val 1) (Op (*) (App fact (Op (-) (Var Stop) (Val 1)))
                               (Var Stop)))

由于这是一种特别有用的模式,因此 Idris 提供了语法重载 [1] 使其在这种目标语言中更易于编程:

mkLam : TTName -> Expr (t::g) t' -> Expr g (TyFun t t')
mkLam _ body = Lam body

dsl expr
    variable    = Var
    index_first = Stop
    index_next  = Pop
    lambda      = mkLam

dsl 块描述了每个语法构造是如何在目标语言中表示的。在这里的 expr 语言中, 任何变量都会被翻译为 Var 构造器,使用 PopStop 来构造 de Bruijn 索引(即,由于变量本身被绑定,所以要统计有多少个绑定);而任何 λ-表达式都会被翻译为 Lam 构造器。mkLam 函数会简单地忽略其第一个参数,它是用户为变量选择的名字。 我们也可以通过这种方式来重载 let 与依赖函数的语法。现在可以将 fact 写成下面这样了:

fact : Expr G (TyFun TyInt TyInt)
fact = expr (\x => If (Op (==) x (Val 0))
                      (Val 1) (Op (*) (app fact (Op (-) x (Val 1))) x))

在这个新的版本中,expr 声明了下一个要被重载的表达式。我们可以利用习语括号, 通过以下声明再进一步:

(<*>) : (f : Lazy (Expr G (TyFun a t))) -> Expr G a -> Expr G t
(<*>) f a = App f a

pure : Expr G a -> Expr G a
pure = id

注意,它无需成为 Applicative 实现的一部分,因为习语括号记法会直接被翻译为 名字 <*>pure,针对特设(ad-hoc)类型的重载也被允许。现在我们可以写成:

fact : Expr G (TyFun TyInt TyInt)
fact = expr (\x => If (Op (==) x (Val 0))
                      (Val 1) (Op (*) [| fact (Op (-) x (Val 1)) |] x))

使用更加特设的重载、接口,以及新的语法规则,我们甚至可以再进一步:

syntax "IF" [x] "THEN" [t] "ELSE" [e] = If x t e

fact : Expr G (TyFun TyInt TyInt)
fact = expr (\x => IF x == 0 THEN 1 ELSE [| fact (x - 1) |] * x)
[1](1, 2) Edwin Brady and Kevin Hammond. 2012. Resource-Safe systems programming with embedded domain specific languages. In Proceedings of the 14th international conference on Practical Aspects of Declarative Languages (PADL‘12), Claudio Russo and Neng-Fa Zhou (Eds.). Springer-Verlag, Berlin, Heidelberg, 242-257. DOI=10.1007/978-3-642-27694-1_18 http://dx.doi.org/10.1007/978-3-642-27694-1_18
[2]Edwin C. Brady. 2011. IDRIS —: systems programming meets full dependent types. In Proceedings of the 5th ACM workshop on Programming languages meets program verification (PLPV ‘11). ACM, New York, NY, USA, 43-54. DOI=10.1145/1929529.1929536 http://doi.acm.org/10.1145/1929529.1929536
[3]Edwin Brady and Kevin Hammond. 2010. Correct-by-Construction Concurrency: Using Dependent Types to Verify Implementations of Effectful Resource Usage Protocols. Fundam. Inf. 102, 2 (April 2010), 145-176. http://dl.acm.org/citation.cfm?id=1883636