用从使用到绑定的函数表示绑定变量


11

用语法表示绑定变量,尤其是避免捕获替换表示绑定变量的问题是众所周知的,并且有许多解决方案:具有alpha等价性的命名变量,de Bruijn索引,局部无名,名词集等。

但是似乎还有另一种相当明显的方法,尽管如此,我仍未在任何地方使用它。也就是说,在基本语法中,我们只有一个“变量”术语,写为,然后分别给出了一个函数,该函数将每个变量映射到其作用域内的绑定器。所以 -term likeλ

λXλÿXÿ

将被写为,该函数会将第一个映射到第一个,将第二个映射到第二个。因此,它有点像de Bruijn索引,只是在您评估一个函数来查找相应的联编程序时不必“计数 s”。(如果在实现中将其表示为数据结构,我会考虑为每个可变项对象配备一个指向相应绑定项对象的简单指针/引用。)λλλλλ

显然,这对于在页面上编写语法以供人类阅读来说并不明智,但是de Bruijn索引也不是。在我看来,从数学上讲,它是完全有意义的,尤其是它使避免捕获的替换变得非常容易:只需替换您要替换的术语并采用绑定函数的并集即可。的确,它没有“自由变量”的概念,但是(再次)de Bruijn索引也没有。在任何一种情况下,包含自由变量的术语都表示为一个术语,其前面带有“上下文”联编程序列表。

我是否缺少某些内容,并且由于某种原因该表示不起作用?是否有问题使它比其他问题严重得多,因此不值得考虑?(我现在唯一能想到的问题是术语集(及其绑定功能)不是归纳定义的,但这似乎并不是无法克服的。)或者实际上是否有使用它的地方?


2
我不知道缺点。也许形式化(例如在校对助手中)更重?我不确定...我所知道的是技术上没有错:这种看待lambda-terms的方式是由其表示为证明网所建议的,因此知道了net-net的人(例如我自己)会隐式地使用它每时每刻。但是具有证明网络意识的人非常罕见:-)因此,也许这确实是一个传统问题。PS:我添加了几个松散相关的标签,以使该问题更加明显(希望如此)。
Damiano Mazza

这种方法是否不等于高阶抽象语法(即,将绑定程序表示为宿主语言中的函数)?从某种意义上说,在闭包的表示中,将函数用作绑定器可以隐式地建立指向绑定器的指针。
Rodolphe Lepigre

2
@RodolpheLepigre我不这么认为。特别是,我的理解是,仅在元理论相当薄弱时,HOAS才是正确的,而在任意元理论中,这种方法才是正确的。
Mike Shulman

3
正确,因此每个活页夹都使用唯一的(在树内)变量名(指向它的指针自动是一个)。这是Barendregt约定。但是,当您替换时,必须重建(用C语言)要替换的内容,以继续具有唯一的名称。否则(通常),您对多个子树使用相同的指针,并且可以获取变量捕获。重建是Alpha重命名。大概会发生类似的情况,具体取决于您将树作为集合进行编码的细节吗?
丹·多尔

3
@DanDoel嗯,很有趣。我认为这很明显,不必提一提,在每次要替换的变量出现时,您都会将其替换为单独的副本;否则,您将不再有语法!我没有想到将此复制视为alpha重命名,但是现在您指出了,我可以看到它。
Mike Shulman

Answers:


11

安德烈(Andrej)和Łukasz(answersukasz)的回答很有意义,但我想补充一点意见。

为了回应达米亚诺的说法,这种用指针表示绑定的方式是证明网建议的一种方式,但是最早用lambda术语看到它的地方是在Knuth的一篇旧文章中:

  • 唐纳德·努斯(Donald Knuth)(1970)。形式语义的例子。在算法语言语义专题讨论会上,E。Engeler(编),Springer的数学讲座188。

在234页上,他绘制了以下表示术语图(他称为“信息结构”):λÿλžÿžX

$(\ lambda y。\ lambda z.yz)x $的Knuth图

1970年代初期,Christopher Wadsworth(1971年,Lambda-Salculus的语义学和语用学)和Richard Statman(1974年,结构复杂性)在两个论点中也独立(更深入地)研究了这种lambda术语的图形表示。证明)。如今,此类图通常被称为“λ图”(例如,参见本文)。

观察到Knuth图中的术语是线性的,这意味着每个自由变量或绑定变量仅发生一次-正如其他人提到的那样,在尝试将这种表示形式扩展到非自由变量时,存在一些不小的问题和选择。 -线性项。

另一方面,对于线性项,我认为这很棒!线性排除了复制的需要,因此您可以同时获得 -equivalence和“免费”替换。这些具有与HOAS相同的优点,我实际上同意Rodolphe Lepigre的观点,两种表示形式之间存在联系(如果不完全等同):从某种意义上说,这些图形结构可以自然地解释为字符串图,表示紧凑的封闭双类别中反身对象的同态(我在这里对此进行了简要说明)。α


10

我不确定您的可变黏合剂功能将如何表示,以及您出于何种目的使用它。如果您使用的是反向指针,那么正如安德烈(Andrej)所指出的那样,替代的计算复杂性并不比经典的Alpha重命名更好。

从对安德烈(Andrej)答案的评论中,我可以推断出您在某种程度上有兴趣分享。我可以在这里提供一些输入。

在典型的λ型演算中,与其他规则相反,减弱和收缩没有语法。

Γ X 1X 2Ť

ΓŤŤΓX一种ŤŤw ^
ΓX1个一种X2一种ŤŤΓX一种ŤŤC

让我们添加一些语法:

Γ X 1X 2Ť

ΓŤŤΓX一种w ^XŤŤw ^
ΓX1个一种X2一种ŤŤΓX一种CXX1个X2ŤŤC

C一种bC正在用完变量和绑定变量。我已经从Ian Mackie的“ 封闭式还原的交互网络实现 ”之一中学到了这个想法。一种bC

使用该语法,每个变量将精确使用两次,一次在绑定位置,一次在使用位置。这使我们能够与特定的语法保持距离,并将该术语视为变量和术语为边的图形。

由于算法的复杂性,我们现在可以使用的指针不是从变量到绑定器,而是从绑定器到变量,并且可以在恒定时间内进行替换。

此外,这种重新制定的格式使我们能够更加精确地跟踪擦除,复制和共享。可以编写规则,在共享子术语时逐步复制(或擦除)术语。有很多方法可以做到这一点。在某些限制条件下,获胜非常令人惊讶

这已经接近交互网络,交互组合器,显式替换,线性逻辑,Lamping的最佳评估,共享图,光逻辑等主题。

所有这些主题对我来说都非常令人兴奋,我很乐意提供更具体的参考,但是我不确定这对您是否有用以及您的兴趣是什么。


6

您的数据结构可以工作,但是它不会比其他方法更有效,因为您需要在每次beta缩减时都复制每个参数,并且必须复制与绑定变量一样多的副本。这样,您就不断破坏子项之间的内存共享。结合以下事实:您将提出一种涉及指针操作的非纯解决方案,因此非常容易出错,因此可能不值得这样做。

但我很高兴看到一个实验!您可以lambda使用数据结构来实现它(OCaml具有指针,它们被称为reference)。或多或少,你只需要更换syntax.ml,并norm.ml与你的版本。不到150行代码。


谢谢!我承认我并不是真的在为实现而认真思考,而主要是在不担心de Bruijn记账或alpha重命名的情况下能够进行数学证明。但是,是否有可能通过不使副本“直到必要”(即直到副本彼此分开)来实现某种实现,从而保留一些内存共享?
Mike Shulman

βλXË1个Ë2Ë1个Ë2

2
关于数学证明,我现在经历了类型理论语法的大量形式化,我的经验是,当我们推广设置并使其更抽象时,而不是在使其更具体时,可以获得好处。例如,我们可以使用“任何处理绑定的好方法”对语法进行参数化。当我们这样做时,更容易出错。我还用de Bruijn指数形式化了类型理论。这并不是太可怕,特别是如果您周围有依赖类型,这会阻止您执行无意义的事情。
安德烈·鲍尔

2
补充一点,我已经研究了一种基本上使用该技术的实现(但是具有唯一的整数和映射,而不是指针),并且我不会真的推荐它。我们肯定有很多错误,这些错误使我们无法正确地克隆东西(绝大部分原因是试图尽可能避免克隆)。但是我认为有些GHC成员在他们的论文中提倡该论文(我相信他们使用哈希函数来生成唯一的名称)。这可能取决于您到底在做什么。在我的情况下,它是类型推断/检查,似乎不太适合那里。
丹·多尔

@MikeShulman对于具有合理(基本)复杂度(在很大程度上复制和擦除)的算法,Lamping的最佳还原的所谓“抽象部分”是直到必要时才进行复制。与完整算法相反,抽象部分也是无争议的部分,完整算法需要一些可以主导计算的注释。
卢卡斯路易斯

5

其他答案主要是讨论实施问题。由于您提到自己的主要动机是在做数学证明时没有过多记账,因此这是我看到的主要问题。

当您说“一个将每个变量映射到其作用域内的活页夹的函数”时:该函数的输出类型肯定比这听起来更微妙!具体来说,该函数必须采用“正在考虑的术语的粘合剂”之类的值,即,一些根据术语而变化的集合(并且显然不是以任何有用的方式作为较大环境集合的子集)。因此,在替换中,您不能仅仅“采用绑定函数的并集”:还必须根据从原始术语中的活页夹到替换结果中的活页夹的一些映射,为它们的值重新编制索引。

这些重新索引肯定应该是“常规的”,从某种意义上说,它们可以合理地扫过地毯,或者可以根据某种功能或自然性很好地包装。但是,处理命名变量所涉及的簿记也是如此。因此,总的来说,在我看来,这种方法至少需要与更标准的方法一样多的簿记工作。

不过,除此之外,这是一种概念上非常吸引人的方法,我很乐意仔细地研究一下它-我可以想象,它可能在语法的某些方面与标准方法有所不同。


跟踪每个变量的范围确实需要簿记,但不要得出一个结论,即总是需要限制使用范围广泛的语法!甚至可以在不宽泛的条件下定义诸如替换和beta减少之类的操作,我的怀疑是,如果有人想在一种方法中形式化这种方法(再次是证明网/“λ-图”的方法)证明助手,首先将执行更通用的操作,然后证明它们保留了范围广的属性。
Noam Zeilberger

(同意值得一试...尽管如果有人已经在形式上证明网/λ-图正式化了,我不会感到惊讶。)
Noam Zeilberger


5

λLazy.t

总的来说,我认为这是一个很酷的表示,但是它涉及一些带有指针的簿记,以避免破坏绑定链接。我猜可以更改代码以使用可变字段,但是用Coq进行编码将不太直接。我仍然坚信这与HOAS非常相似,尽管明确了指针结构。但是,存在的Lazy.t隐含含义意味着可能会在错误的时间评估某些代码。在我的代码中情况并非如此,因为一次只能用变量替换变量force(例如不进行评估)。

(* Representation of a term of the λ-calculus. *)
type term =
  | FVar of string      (* Free variable  *)
  | BVar of bvar        (* Bound variable *)
  | Appl of term * term (* Application    *)
  | Abst of abst        (* Abstraction    *)

(* A bound variable is a pointer to the corresponding binder. *)
and bvar = abst

(* A binder is represented as its body in which the bound variable points to
   the binder itself. Note that we need to use a thunk to be able to work
   underneath a binder (for substitution, evaluation, ...). A name can be
   given for easy printing, but no renaming is done. Only “visual capture”
   can happen since pointers are established the right way, even if names
   can clash. *)
and abst = { body : term Lazy.t ; name : string }

(* Terms can be built with recursive values for abstractions. *)

(* Krivine's notation is used for application (function in parentheses). *)

let id    : term = (* λx.x        *)
  Abst(let rec id = {body = lazy (BVar(id)); name = "x"} in id)

let idid  : term = (* (λx.x) λx.x *)
  Appl(id, id)

let delta : term = (* λx.(x) x *)
  Abst(let rec d = {body = lazy (Appl(BVar(d), BVar(d))); name = "x" } in d)

let weird : term = (* (λx.x) λy.(λx.(x) x) (C) y *)
  Appl(id, Abst(let rec x = {body = lazy (Appl(delta, Appl(FVar("C"),
    BVar(x)))); name = "y"} in x))

let omega : term = (* (λx.(x) x) λx.(x) x *)
  Appl(delta, delta)

(* Printing function is immediate. *)
let rec print : out_channel -> term -> unit = fun oc t ->
  match t with
  | FVar(x)   -> output_string oc x
  | BVar(x)   -> output_string oc x.name
  | Appl(t,u) -> Printf.fprintf oc "(%a) %a" print t print u
  | Abst(f)   -> Printf.fprintf oc "λ%s.%a" f.name print (Lazy.force f.body)

(* Substitution of variable [x] by [v] in the term [t]. Occurences of [x] in
   [t] are identified using physical equality ([BVar] case). The subtle case
   is [Abst], because we need to reestablish the physical link between the
   binder and the variable it binds. *)
let rec subst_var : bvar -> term -> term -> term = fun x t v ->
  match t with
  | FVar(_)   -> t
  | BVar(y)   -> if y == x then v else t
  | Appl(t,u) -> Appl(subst_var x t v, subst_var x u v)
  | Abst(f)   ->
      (* First compute the new body. *)
      let fv = subst_var x (Lazy.force f.body) v in
      (* Reestablish the physical link, using [subst_var] itself again. This
         requires a second traversal of the term. We could probably do both
         at once, but who cares the complexity is linear in [t] anyway. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)

(* Actual substitution function. *)
let subst : abst -> term -> term = fun f v ->
  subst_var f (Lazy.force f.body) v

(* Normalization function (all the way, even under binders). *)
let rec eval : term -> term = fun t ->
  match t with
  | Appl(t,u) ->
      begin
        let v = eval u in
        match eval t with
        | Abst(f) -> eval (subst f v)
        | t       -> Appl(t,v)
      end
  | Abst(f)   ->
      (* Actual computation in the body. *)
      let fv = eval (Lazy.force f.body) in
      (* Here, the physical link is reestablished, but it is important to note
         that the computation of evaluation is done above. So the part below
         only takes a linear time in the size of the normal form of the body
         of the abstraction. *)
      Abst(let rec g = {f with body = lazy (subst_var f fv (BVar(g)))} in g)
  | _         ->
      t

let _ = Printf.printf "id         = %a\n%!" print id
let _ = Printf.printf "eval id    = %a\n%!" print (eval id)

let _ = Printf.printf "idid       = %a\n%!" print idid
let _ = Printf.printf "eval idid  = %a\n%!" print (eval idid)

let _ = Printf.printf "delta      = %a\n%!" print delta
let _ = Printf.printf "eval delta = %a\n%!" print (eval delta)

let _ = Printf.printf "omega      = %a\n%!" print omega
(* The following obviously loops. *)
(*let _ = Printf.printf "eval omega = %a\n%!" print (eval omega)*)

let _ = Printf.printf "weird      = %a\n%!" print weird
let _ = Printf.printf "eval weird = %a\n%!" print (eval weird)

(* Output produced:
id         = λx.x
eval id    = λx.x
idid       = (λx.x) λx.x
eval idid  = λx.x
delta      = λx.(x) x
eval delta = λx.(x) x
omega      = (λx.(x) x) λx.(x) x
weird      = (λx.x) λy.(λx.(x) x) (C) y
eval weird = λy.((C) y) (C) y
*)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.