replace one function call with another using a macro
Asked Answered
K

2

5

How can I replace all function calls to f with function calls to g using a racket macro? I'm new to racket and I don't know how to process syntax objects, but I believe the use case I have in mind is something a racket macro could do. Consider the following example, where I want to replace plus with mul. The macro replace-plus-with-mul just returns current-seconds as a placeholder because I don't how what to do with the syntax object to replace plus with mul. Can a macro do this?

#lang racket

(define-syntax replace-plus-with-mul
  (lambda (stx) #'(current-seconds)))

(define plus (lambda (x y) (+ x y)))
(define mul (lambda (x y) (* x y)))

(define a 4)
(define b 2)
(define c (plus a b))
(replace-plus-with-mul d c) ;; (define d (mul a b))
(print d) ;; should print 8
Kittrell answered 15/2, 2019 at 20:55 Comment(4)
Is it okay if you also have to redefine define?Devoid
I'd actually also be interested in seeing a solution that redefines define, or if it involves using some custom-define instead of define in my example, that'd also be interesting.Kittrell
Okay. I'll try to post that sometime this evening, since you're interested.Devoid
Okay, I've posted an answer that describes two macros, a custom define define/replacable and replace-plus-with-mul, which communicate using define-syntax and syntax-local-valueDevoid
S
5

I don't see an easy way to precisely get what you want to work, but with an additional restriction, it's certainly possible.


If you are OK with the restriction that the macro invocation must syntactically contain plus, then simply recursively replace all plus with mul inside the macro

;; main.rkt
#lang racket

(define plus (lambda (x y) (+ x y)))
(define mul (lambda (x y) (* x y)))

(define-for-syntax (replace stx)
  (syntax-case stx ()
    [(a . b)
     (datum->syntax stx (cons (replace #'a)
                              (replace #'b)))]
    [_
     (and (identifier? stx)
          (free-identifier=? #'plus stx))
     #'mul]
    ;; FIXME: need more cases (like box or vector), but 
    ;; this is sufficient for the demo
    [_ stx]))

(define-syntax (replace-plus-with-mul stx)
  (syntax-case stx ()
    [(_ id expr)
     #`(define id
         #,(replace (local-expand #'expr 'expression '())))]))

(replace-plus-with-mul c (plus 3 (let ([plus 10]) plus)))
c                               ; prints 30
(plus 3 (let ([plus 10]) plus)) ; prints 13

If you are OK with the restriction that plus that you would like to change must not already be used, like the below code:

(define (c) (plus 3 2))
(replace-plus-with-mul d (c))

Then there are several approaches to this. One is to override #%module-begin to replace all plus to (if (current-should-use-mul?) mul plus) and expand replace-plus-with-mul to (parameterize ([current-should-use-mul? #t]) ...). Here's the full code:

;; raquet.rkt
#lang racket

(provide (except-out (all-from-out racket)
                     #%module-begin)
         (rename-out [@module-begin #%module-begin])
         plus
         mul
         replace-plus-with-mul)

(define plus (lambda (x y) (+ x y)))
(define mul (lambda (x y) (* x y)))
(define current-should-use-mul? (make-parameter #f))

(define-for-syntax (replace stx)
  (syntax-case stx ()
    [(a . b)
     (datum->syntax stx (cons (replace #'a)
                              (replace #'b)))]
    [_
     (and (identifier? stx)
          (free-identifier=? #'plus stx))
     #'(if (current-should-use-mul?) mul plus)]
    ;; FIXME: need more cases (like box or vector), but 
    ;; this is sufficient for the demo
    [_ stx]))

(define-syntax (@module-begin stx)
  (syntax-case stx ()
    [(_ form ...)
     #'(#%module-begin (wrap-form form) ...)]))

(define-syntax (wrap-form stx)
  (syntax-case stx ()
    [(_ form) (replace (local-expand #'form 'top-level '()))]))

(define (activate f)
  (parameterize ([current-should-use-mul? #t])
    (f)))

(define-syntax (replace-plus-with-mul stx)
  (syntax-case stx ()
    [(_ id expr)
     #`(define id (activate (lambda () expr)))]))

and

;; main.rkt
#lang s-exp "raquet.rkt"

(define (c) (plus 3 (let ([plus 10]) plus)))
(replace-plus-with-mul a (c))
a    ; prints 30
(c)  ; prints 13

In a sense, what you want to do needs a kind of lazy evaluation, and this is a huge semantic change. I'm not sure if there's a good way to do it while not "damaging" other code.

Salesgirl answered 15/2, 2019 at 22:48 Comment(0)
D
2

You can do this by defining your own version of define, which saves the expression at compile time, which replace-plus-with-mul can get later.

The two macros define/replacable and replace-plus-with-mul have to work together using define-syntax and syntax-local-value:

  1. define/replacable uses define-syntax to associate compile-time information with the identifier it defines.
  2. replace-plus-with-mul uses syntax-local-value to look up that compile-time information.

First Pass, saving a function directly in the define-syntax

#lang racket
(require syntax/parse/define
         (for-syntax syntax/transformer))

(define-syntax-parser define/replacable
  [(_ name:id expr:expr)
   #:with plus (datum->syntax #'name 'plus)
   #:with mul (datum->syntax #'name 'mul)
   #'(define-syntax name
       ;; Identifier Identifier -> Expression
       ;; Replaces plus and mul within the expr
       ;; with the two new identifiers passed to
       ;; the function
       (lambda (plus mul)
         (with-syntax ([plus plus] [mul mul])
           #'expr)))])

(define-syntax-parser replace-plus-with-mul
  [(_ name:id replacable:id)
   (define replace (syntax-local-value #'replacable))
   #`(define name #,(replace #'mul #'mul))])

With these definitions, this program works:

(define plus (lambda (x y) (+ x y)))
(define mul (lambda (x y) (* x y)))

(define a 4)
(define b 2)
(define/replacable c (plus a b))
(replace-plus-with-mul d c) ;; (define d (mul a b))
(print d)
;=output> 8

However, c in this example cannot be used as a normal expression. It can be used within replace-plus-with-mul, but only within that. This can be fixed by adding a struct.

Second Pass, saving a struct so that normal uses also work

In the first version, the two macros communicated like this:

  1. define/replacable uses define-syntax to associate compile-time information with the identifier it defines.
  2. replace-plus-with-mul uses syntax-local-value to look up that compile-time information.

However, this doesn't allow the identifiers to have normal behavior. To do that we need something like this:

  1. define/replacable uses define-syntax to associate the identifier it defines with a compile-time struct which contains both:
    • normal behavior
    • replace behavior
  2. replace-plus-with-mul uses syntax-local-value to look up that compile-time struct, and get the replace behavior out of it
  3. The normal Racket macro expander uses syntax-local-value to look up that compile-time struct, and uses it as a procedure to apply as a macro. Because of this, we should make the struct have #:property prop:procedure with the normal behavior.

This struct can look like this:

(begin-for-syntax
  ;; normal : Expression -> Expression
  ;; replace : Identifier Identifier -> Expression
  (struct replacable-id [normal replace]
    #:property prop:procedure (struct-field-index normal)))

Now the define/replacable macro should generate a define-syntax that constructs one of those:

(define-syntax name
  (replacable-id ???
                 (lambda (plus mul)
                   ...what-we-had-before...)))

If we want the normal behavior to look like a variable, we can fill in the ??? hole using make-variable-like-transformer from syntax/transformer:

(require (for-syntax syntax/transformer))

(begin-for-syntax
  ;; Identifier -> [Expression -> Expression]
  (define (make-var-like-transformer id)
    (set!-transformer-procedure (make-variable-like-transformer id))))

Then define/replacable can generate something like this:

(define normal-name expr)
(define-syntax name
  (replacable-id (make-var-like-transformer #'normal-name)
                 (lambda (plus mul)
                   ...what-we-had-before...)))

Putting it all together:

#lang racket
(require syntax/parse/define
         (for-syntax syntax/transformer))

(begin-for-syntax
  ;; Identifier -> [Expression -> Expression]
  (define (make-var-like-transformer id)
    (set!-transformer-procedure (make-variable-like-transformer id)))

  ;; normal : Expression -> Expression
  ;; replace : Identifier Identifier -> Expression
  (struct replacable-id [normal replace]
    #:property prop:procedure (struct-field-index normal)))

(define-syntax-parser define/replacable
  [(_ name:id expr:expr)
   #:with plus (datum->syntax #'name 'plus)
   #:with mul (datum->syntax #'name 'mul)
   #'(begin
       (define normal-name expr)
       (define-syntax name
         (replacable-id (make-var-like-transformer #'normal-name)
                        (lambda (plus mul)
                          (with-syntax ([plus plus] [mul mul])
                            #'expr)))))])

(define-syntax-parser replace-plus-with-mul
  [(_ name:id replacable:id)
   (define value (syntax-local-value #'replacable))
   (define replace (replacable-id-replace value))
   #`(define name #,(replace #'mul #'mul))])

And trying it out:

(define plus (lambda (x y) (+ x y)))
(define mul (lambda (x y) (* x y)))

(define/replacable a 4)
(define/replacable b 2)
(define/replacable c (plus a b))
(replace-plus-with-mul d c) ;; (define d (mul a b))
(print d)
;=output> 8
Devoid answered 21/2, 2019 at 2:8 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.