용도에서 바인더까지의 함수를 사용하여 바운드 변수 표시


11

구문으로 바인딩 된 변수를 표현하는 문제, 특히 캡처를 피하는 대체 문제는 잘 알려져 있으며 여러 가지 솔루션이 있습니다.

그러나 그럼에도 불구하고 다른 명백한 접근 방식이있는 것 같습니다. 즉, 기본 구문에서 우리는 하나의 "가변"이라는 용어를 라고 쓰고 , 개별적으로 각 변수를 그 범위에 속하는 바인더에 매핑하는 함수를 제공합니다. 따라서 λ 용어는λ

λx.(λy.xy)

로 작성 됩니다. ( λ . ) 및 제 1 맵핑 함수 것이다 제에 λ 와 제 제에 λ를 . 따라서 그것은 Bruijn 지수와 비슷 합니다. 해당 바인더를 찾기 위해 용어에서 " λ s"를 계산하는 대신 함수를 평가하면됩니다. (구현에서 이것을 데이터 구조로 표현한다면, 각 변수 용어 객체에 해당 바인더 용어 객체에 대한 간단한 포인터 / 참조를 장비한다고 생각합니다.)λ.(λ.)λλλ

분명히 이것은 사람이 읽을 수 있도록 페이지에 구문을 작성하는 데 합리적이지 않지만 브루 인 지수는 아닙니다. 그것은 수학적으로 완벽하게 이해되는 것 같습니다. 특히 캡처 피하기 대체를 매우 쉽게 만듭니다. 대체하는 용어를 바꾸고 바인딩 함수의 결합을 취하십시오. 그것이 "자유 변수"라는 개념을 가지고 있지 않다는 것이 사실이지만, (다시) de de Bruijn 지수도 실제로는 아닙니다. 두 경우 모두 자유 변수를 포함하는 용어는 앞에 "컨텍스트"바인더 목록이있는 용어입니다.

뭔가 빠졌는데이 표현이 작동하지 않는 이유가 있습니까? 고려할 가치가없는 다른 것보다 훨씬 더 나쁜 문제가 있습니까? (내가 지금 생각할 수있는 유일한 문제는 (결합 함수와 함께) 용어 집합이 귀납적으로 정의되지는 않지만 극복 할 수없는 것처럼 보이지 않는다는 것입니다. 또는 실제로 사용 된 장소가 있습니까?


2
단점에 대해 모른다. 어쩌면 형식화 (예 : 교정 조교)가 더 무거울 수 있습니까? 확실하지 않습니다 ... 내가 아는 것은 기술적으로 잘못된 것이 없다는 것입니다. 람다 용어를 보는이 방법은 증거 그물로 표현하여 제안 된 방법이므로 증거를 인식하는 사람들 (나 같은 사람)은 암시 적으로 그것을 사용합니다 항상. 그러나 증거를 인식하는 사람들은 매우 드 rare니다. :-) 아마 전통의 문제 일 것입니다. 추신 : 질문을 더 잘 보이게하기 위해 느슨하게 관련된 몇 가지 태그를 추가했습니다.
Damiano Mazza

이 접근 방식이 고차 추상 구문과 동일하지 않습니까 (예 : 바인더를 호스트 언어의 함수로 표시)? 어떤 의미에서, 함수를 바인더로 사용하면 클로저를 표현할 때 바인더에 대한 포인터가 암시 적으로 설정됩니다.
Rodolphe Lepigre

2
@RodolpheLepigre 나는 그렇게 생각하지 않습니다. 특히, 나의 이해는 HOAS는 복분해가 상당히 약한 경우에만 정확하고이 접근은 임의의 복분해에서는 정확하다는 것입니다.
Mike Shulman

3
따라서 각 바인더는 고유 한 (트리 내) 변수 이름을 사용합니다 (포인터에 대한 포인터는 자동입니다). 이것은 Barendregt 규칙입니다. 그러나 대체 할 때, 고유 이름을 계속 유지하기 위해 대체하는 것을 C로 재 구축해야합니다. 그렇지 않으면 (일반적으로) 여러 하위 트리에 동일한 포인터를 사용하고 변수 캡처를 얻을 수 있습니다. 재건은 알파 이름 변경입니다. 아마도 나무를 인코딩하는 세트의 특성에 따라 비슷한 것이 발생합니까?
Dan Doel

3
@ DanDoel 아, 흥미로운. 나는 대체되는 변수가 발생할 때마다 대체되는 용어의 사본따로 넣을 것이라고 말할 필요가 없다고 생각했다 . 그렇지 않으면 더 이상 구문 트리 가 없습니다 ! 이 사본을 알파-네이밍으로 생각하는 것은 나에게 일어나지 않았지만 이제는 그것을 알 수 있습니다.
Mike Shulman

답변:


11

Andrej와 Łukasz의 답변은 좋은 지적이지만 추가 의견을 추가하고 싶었습니다.

Damiano가 말한 것을 반영하기 위해 포인터를 사용하여 바인딩을 표현하는이 방법은 증거 망에서 제안한 방법이지만 람다 용어로 본 가장 오래된 곳은 Knuth의 오래된 에세이였습니다.

  • 도널드 크 누스 (1970). 형식적 의미론의 예. 에서는 알고리즘 언어의 의미론 심포지엄 , E. Engeler (ED.) 수학 188, 스프링 강의 노트.

(λy.λz.yz)x

$ (\ lambda y. \ lambda z.yz) x $에 대한 Knuth의 다이어그램

람다 용어에 대한 이런 종류의 그래픽 표현은 1970 년대 초 크리스토퍼 워즈워스 (1971 년, Lambda-Calculus의 의미론 및 실용 법 )와 Richard Statman (1974, Structural Complexity )에 의해 두 가지 논문에서 독립적으로 (그리고 더 깊이) 연구되었습니다. 증거의 ). 오늘날 이러한 다이어그램을 종종 "λ- 그래프"라고합니다 (예 : 이 백서 참조 ).

Knuth의 다이어그램에서 용어는 모든 자유 변수 또는 바운드 변수가 정확히 한 번만 발생한다는 점에서 선형 이라는 것을 관찰하십시오. 다른 사람들이 언급했듯이 이러한 종류의 표현을 선형 용어.

α


10

변수 대 바인더 함수가 어떻게 표현되고 어떤 목적으로 사용하고 있는지 잘 모르겠습니다. 백 포인터를 사용하는 경우 Andrej가 지적했듯이 대체의 계산 복잡성은 고전적인 알파 이름 바꾸기보다 낫지 않습니다.

Andrej의 답변에 대한 귀하의 의견에서 나는 어느 정도까지 공유에 관심이 있다고 생각합니다. 여기에 입력 할 수 있습니다.

일반적인 유형의 람다 미적분학에서 다른 규칙과 달리 약화 및 수축에는 구문이 없습니다.

Γt:TΓ,x:At:TW
Γ,x1:A,x2:At:TΓ,x:At:TC

문법을 추가해 봅시다 :

Γt:TΓ,x:AWx(t):TW
Γ,x1:A,x2:At:TΓ,x:ACxx1,x2(t):TC

Cab,c()ab,c

이 구문을 사용하면 모든 변수는 바인드 된 위치와 사용 된 위치에 정확히 두 번 사용됩니다. 이를 통해 특정 구문과 거리를두고 변수와 용어가 모서리 인 그래프로 용어를 볼 수 있습니다.

알고리즘 복잡성에서 변수 에서 바인더로가 아니라 바인더에서 변수로 포인터를 사용할 수 있으며 일정한 시간에 대체가 가능합니다.

또한,이 개혁은 우리가 더 충실하게 삭제, 복사 및 공유를 추적 할 수있게합니다. 하위 용어를 공유하는 동안 용어를 증분 복사 (또는 삭제)하는 규칙을 작성할 수 있습니다. 여러 가지 방법이 있습니다. 일부 제한된 설정 에서 승리는 매우 놀랍습니다 .

이것은 인터랙션 네트, 인터랙션 콤비 네이터, 명시 적 대체, 선형 로직, Lamping의 최적 평가, 공유 그래프, 라이트 로직 및 기타 주제에 가까워지고 있습니다.

이 모든 주제는 저에게 매우 흥미롭고 더 구체적으로 언급 할 것입니다. 그러나이 중 어떤 것이 당신에게 유용한 지, 그리고 당신의 관심사가 무엇인지 잘 모르겠습니다.


6

데이터 구조는 작동하지만 모든 베타 축소에서 모든 인수를 복사해야하며 바인딩 된 변수가 발생하는 횟수만큼 복사해야하기 때문에 다른 접근법보다 효율적이지 않습니다. 이런 식으로 하위 용어 간의 메모리 공유를 계속 파괴합니다. 포인터 조작이 필요하고 오류가 발생하기 쉬운 비 순수 솔루션을 제안하고 있다는 사실과 결합하면 문제의 가치가 없습니다.

그러나 실험을 보게되어 기쁩니다! lambda데이터 구조 (OCaml에는 포인터가 있으며 참조 라고 함 )를 사용하여 구현할 수 있습니다 . 더 이하, 당신은 교체해야 syntax.ml하고 norm.ml당신의 버전. 150 줄 미만의 코드입니다.


감사! 필자는 구현에 대해 매우 열심히 생각하지는 않았지만 브루 인 부기 또는 알파 이름 변경에 대해 신경 쓰지 않고 수학 증명을 할 수 있다는 것에 주로 동의했습니다. 그러나 구현이 복사본을 "필요할 때까지", 즉 복사본이 서로 분리 될 때까지 만들지 않음으로써 메모리 공유를 유지할 가능성이 있습니까?
Mike Shulman

β(λx.e1)e2e1e2

2
수학적 증명과 관련하여, 나는 타입 이론적 구문의 많은 형식화를 겪어 왔으며, 경험은 설정 을 일반화 하고 더 구체적으로 만들지 않고 더 추상적으로 만들 때 이점을 얻는다 는 것입니다. 예를 들어, "바인딩을 처리하는 좋은 방법"으로 구문을 매개 변수화 할 수 있습니다. 그렇게하면 실수하기가 더 어렵습니다. 나는 또한 de Bruijn 지수로 형식 이론을 공식화했습니다. 특히 무의미한 일을 방해하는 의존 유형이있는 경우 너무 끔찍하지 않습니다.
Andrej Bauer

2
또한 기본적 으로이 기술을 사용하는 구현 (포인터가 아닌 고유 한 정수 및 맵)을 사용했으며 실제로 권장하지는 않습니다. 우리는 복제를 제대로 놓친 버그가 많았습니다 (가능한 경우 그것을 피하려고 노력했기 때문에 작은 부분은 아닙니다). 그러나 나는 일부 GHC 사람들이 그것을 옹호하는 논문이 있다고 생각합니다 (그들은 고유 한 이름을 생성하기 위해 해시 함수를 사용했습니다). 정확히 무엇을하고 있는지에 따라 달라질 수 있습니다. 필자의 경우 형식 유추 / 검사였으며 거기에 적합하지 않은 것으로 보입니다.
Dan Doel

@MikeShulman 합리적인 (초급) 복잡성 알고리즘 (대부분의 복사 및 지우기)을 위해 Lamping의 최적 축소의 소위 '추상 부분'은 필요할 때까지 복사하지 않습니다. 추상 부분은 계산을 지배 할 수있는 주석이 필요한 전체 알고리즘과 달리 논란의 여지가없는 부분입니다.
Łukasz Lew

5

다른 답변은 주로 구현 문제를 논의하고 있습니다. 너무 많은 부기없이 수학 증명을하는 것에 대한 주요 동기를 언급 했으므로 여기에 내가 볼 수있는 주요 문제가 있습니다.

“각 범위에있는 바인더에 각 변수를 매핑하는 함수”라고 말하면이 함수의 출력 유형은 소리보다 약간 미묘합니다. 특히, 함수는 "고려중인 용어의 바인더"와 같은 값을 가져와야합니다. 즉 일부 용어는 용어에 따라 달라집니다 (그리고 유용한 방식으로 큰 주변 세트의 하위 집합이 아닙니다). 따라서 대체에서“바인딩 함수의 결합을 취하십시오”는 것은 아닙니다. 대체 결과로 원래 용어의 바인더에서 바인더로의 일부 맵에 따라 값을 다시 색인화해야합니다.

이러한 재색 인화는 반드시“루틴”이어야하는데, 이는 양탄자 밑에서 합리적으로 쓸 수 있거나 어떤 종류의 기능성 또는 자연성 측면에서 멋지게 포장 될 수 있다는 점에서 의미가 있습니다. 그러나 명명 된 변수 작업과 관련된 부기의 경우도 마찬가지입니다. 전반적으로, 이 접근법과 관련하여 최소한 표준 접근 방식보다 최소한의 부기있을 것 같습니다 .

그럼에도 불구하고 이것은 개념적으로 매우 매력적인 접근 방식이며 신중하게 해결되기를 원합니다. 표준 접근 방식과 구문의 일부 측면에서 다른 관점을 제시 할 수 있습니다.


각 변수의 범위를 실제로 추적하려면 부기 (bookkeeping)가 필요하지만 항상 범위가 올바른 구문으로 제한해야한다는 결론으로 ​​넘어 가지 마십시오! 대체 및 베타 감소와 같은 작업은 범위가 잘못된 용어로도 정의 할 수 있으며, 의심의 여지가 있다면이 접근법을 공식화하려는 경우 (실제로 증거-net / "λ- 그래프"의 접근 방식) 증거 조수, 먼저 더 일반적인 작업을 구현 한 다음 범위가 넓은 속성을 유지한다는 것을 증명합니다.
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
*)
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.