Closed philnguyen closed 2 years ago
Ah, I see why: the performance difference is significant, as in the below.
And maybe the reason for not introducing a new name is to avoid clashes with existing code. If so, did you consider something where the arguments to substitute
are of this shape: (substitute <expr> (<x> <e>) ...)
? It seems a bit nicer to keep the "x" next to "e" that its being replaced with.
#lang racket
(require redex/reduction-semantics)
(define-language λ
(e ::= (λ (x) e) (e e) x)
(x ::= variable-not-otherwise-mentioned)
#:binding-forms
(λ (x) e #:refers-to x))
(default-language λ)
(define size 4000)
(define t
(let loop ([n size])
(cond
[(zero? n)
(for/list ([i (in-range size)])
(string->symbol (~a "x" i)))]
[else (term (λ (,(string->symbol (~a "x" (- n 1))))
,(loop (- n 1))))])))
(define vars (for/list ([i (in-range size)]) 'x))
(define rhses (for/list ([i (in-range size)]) 10))
(define (subst-n)
(for/fold ([t t])
([var (in-list vars)]
[rhs (in-list rhses)])
(term (substitute ,t ,var ,rhs))))
(collect-garbage) (collect-garbage) (collect-garbage)
(define a2 (time (subst-n)))
(collect-garbage) (collect-garbage) (collect-garbage)
(define a1 (time (term (substitute ,t ,vars ,rhses))))
(test-equal a1 a2)
(test-results)
Thanks Robby! I updated the doc, changed the argument to (substitute <expr> (<x> <e>) ...)
, and updated the test to demonstrate where this would differ from applying each substitution one by one.
About the motivation, I think apart from performance, there are cases where it is useful?
For example, if we have the following:
(λxy. x y) y z
, my understanding is we can't just straightforwardly do the substitutions in sequence
(x y)[x -> y][y -> z]
, and we could either re-use the 1-arg substitute
while keeping the remaining bindings untouched (e.g. (λy. x y)[x -> y]
first, then [y -> z]
on the resulting body with y
renamed from capture-avoiding), or re-implement the simultaneous substitution in this PR?
For the record, if we follow Barendregt and old logic books, a term such as
(λxy. x y) y z
is considered "not ready for reduction/substitution". Barendregt resolves this with "behind the scenes" alpha renaming. (Logic books did this explicitly.) So if PL theoreticians were to use the solid foundations laid out 40 years ago, redex would do so too. And for some reason I thought we were doing this after Paul. What I am missing here? (Then again, most PL theory people no longer know about Barendregt and his careful treatment of these issues. So perhaps it doesn't matter.)
I think that maybe @philnguyen and @mfelleisen (or maybe Berendregt) have slightly different interpretations of the term λxy. x y
. If you look at it as implicit currying, then doing the β reduction corresponds to these two calls to substitute
, which works out fine and as matthias writes, the behind-the-scenes stuff is indeed happening, thanks to @paulstansifer .
> (term (substitute (λ (y) (x y)) x y))
'(λ (y«1») (y y«1»))
> (term (substitute (y y«1») y«1» z))
'(y z)
But if you think of it as a n-ary lambda and you try to recur and call substitute
n times directly on the body of the lambda, you get the wrong answer:
> (term (substitute (substitute (x y) x y) y z))
'(z z)
What this means is that if you're using substitute and thinking of the n-ary style λ expressions, you need to write the β rule differently, as in the code below (the examples above were run in the REPL you get from the code below, if you want to try things out).
#lang racket
(require redex)
(define-language λ
(e ::= (λ (x ...) e) (e e ...) x)
(x ::= variable-not-otherwise-mentioned)
#:binding-forms
(λ (x) e #:refers-to x))
(default-language λ)
(define-judgment-form λ
#:mode (--> I O)
[------------------ β0
(--> ((λ () e)) e)]
[----------------------------------------------------- βn
(--> ((λ (x_1 x_2 ...) e) e_1 e_2 ...)
((substitute (λ (x_2 ...) e) x_1 e_1) e_2 ...))])
(define-judgment-form λ
#:mode (-->* I O)
[----------- refl
(-->* e e)]
[(--> e_1 e_2) (-->* e_2 e_3)
---------------------------- tran
(-->* e_1 e_3)])
(show-derivations
(build-derivations
(-->* ((λ (x y) (x y)) y z)
(x_1 ...))))
Yes I know :-)
This should work in all cases. So I am baffled why it fails to work for Paul's extension. That's all.
Is maybe the missing piece that the alpha renaming happens when destructuring the lambda (because it is listed as a binding form)? Does that plus the code examples help unbaffle? So when doing the let
or the n-ary lambda we alpha-rename only once and then substitute twice, which is what makes it go wrong?
Oops, there are two errors that I made in the discussion above.
But if you think of it as a n-ary lambda [...]
First, just below this quoted part, I wrote an example where I called substitute, but I called it wrongly; it won't be called like that in a rule; it'll be called with the freshened variables! So that was totally misleading.
The second error is that the binding-forms declaration should have been
#:binding-forms
(λ (x ...) e #:refers-to (shadow x ...))
Oh, oops: looks like deftogether
was the wrong thing, sorry! This is probably what the docs should have been:
diff --git a/redex-doc/redex/scribblings/ref/languages.scrbl b/redex-doc/redex/scribblings/ref/languages.scrbl
index 8a5844d..ac1e1bf 100644
--- a/redex-doc/redex/scribblings/ref/languages.scrbl
+++ b/redex-doc/redex/scribblings/ref/languages.scrbl
@@ -545,10 +545,9 @@ If the @racket[lang] argument is not supplied, it
defaults to the value of @racket[(default-language)], which must not @racket[#f].
}
-@deftogether[(@defform[#:kind "metafunction"
- (substitute val old-var new-val)]{}
- @defform[#:kind "metafunction"
- (substitute val (old-var new-val) ...)]{})]{
+@defform*[#:kind "metafunction"
+ [(substitute val old-var new-val)
+ (substitute val (old-var new-val) ...)]]{
A metafunction that returns a value like @racket[val], except that any free occurences of
@racket[old-var] have been replaced with @racket[new-val], in a capture-avoiding fashion. The bound
names of @racket[val] may be freshened in order to accomplish this, based on the binding information
I fixed that. Thanks!
Thank you!
Thanks for offering this change, @philnguyen !
I think it needs some documentation, and I'm wondering why overloading
substitute
in this way is the best choice (as compared to having a second function).But mostly I wonder why calling substitute multiple times is a bad idea. I do see that calling
substitute
multiple times returns different results than thesubstitute
in the PR (as in the example below) -- is that the rationale here?