Is it possible to "extend" a function / lambda / macro in Scheme?
Asked Answered
P

6

6

For example: if I want the function equal? recognize my own type or record, can I add a new behavior of equal?? without erasing or overwriting the old one?

Or for example if I want to make the function "+" accept also string?

Panchito answered 27/5, 2014 at 0:34 Comment(2)
This question becomes more and more interesting, specifically since you tagged r7rs. I would have liked a r6rs tag as well. I'd bet you'll need to roll your own generics or something in those.Ferino
Just added r6rs as you suggested. Thank you.Panchito
C
5

Rather than using import, a better solution is to keep track of the original function by let-binding it. It's also better to check that the type of the argument is a string, rather than that it is not a number. Using both of these approaches means that it's possible to compose the technique.

(define +
  (let ((old+ +))
    (lambda args
      (if (string? (car args))
          (apply string-append args)
          (apply old+ args)))))

(define +
  (let ((old+ +))
    (lambda args
      (if (vector? (car args))
          (apply vector-append args)
          (apply old+ args)))))

The above will produce a + function that works on numbers, strings, or vectors. In general, this is a more extensible approach.


I was able to verify that the above works correctly in MIT/GNU Scheme, Guile, Racket, Chicken, TinyScheme, and SCSH. However, in some implementations, such as Biwa Scheme, it is necessary to use set! instead of define. In Ikarus, set! cannot be used on an imported primitive, and define messes up the environment, so it is necessary to do this in two steps:

(define new+
  (let ((old+ +))
    (lambda args
      (if (string? (car args))
          (apply string-append args)
          (apply old+ args)))))
(define + new+)

Note that according to R5RS, define and set! are supposed to be equivalent in this case:

At the top level of a program, a definition

(define <variable> <expression>)

has essentially the same effect as the assignment expression

(set! <variable> <expression>)

if <variable> is bound.

Comity answered 27/5, 2014 at 1:9 Comment(6)
Wouldn't old+ be undefined?Ferino
@Ferino Look again- it's bound in the let.Comity
A note on Racket. None of the approaches work by default. E.g. #!racket ; ==> module: duplicate definition for identifier in: + and #!r6rs ; ==> module: identifier is already imported in: +. It works in racket in legacy language R5RS (not #!r5rs module language) with the special setting "Disallow redefinition of initial binding" unchecked. In Ikarus you'll get exception multiple definitions of identifier if run with --r6rs-script. REPL is probably more forgiving but that won't help if you are using it for a project.Ferino
@Ferino I did not understand your commend very well. This solution works on r6rs and also on r7rs?Panchito
@FelipeMicaroniLalli I tested for R6RS on both ikarus and racket and this does not work. I did manage to get it to work on r5rs legacy mode in racket. In Chibi-scheme (The only R7RS implementation) define is a letrec making + undefined at the time of the old+ binding (like I predicted in my first comment), but the second version that uses a temporary symbol new+ works.Ferino
@Sandra Probably not? But idk, I wrote that post 7 years ago.Comity
F
1

So far the solutions work less than optimal in an R6RS / R7RS environment. I was thinking generics when I started playing around with this, but I didn't want to roll my own type system. Instead you supply a predicate procedure that should ensure the arguments are good for this particular procedure. It's not perfect but it works similar to the other R5RS answers and you never redefine procedures.

I've written everything in R6RS but I imagine it's easily ported to R7RS. Here's an example:

#!r6rs

(import (sylwester generic)
        (rename (rnrs) (+ rnrs:+))
        (only (srfi :43) vector-append))

(define-generic + rnrs:+)
(add-method + (lambda x (string? (car x))) string-append)
(add-method + (lambda x (vector? (car x))) vector-append)
(+ 4 5)                ; ==> 9
(+ "Hello," " world!") ; ==> "Hello, world!"
(+ '#(1) '#(2))        ; ==> #(1 2)

As you can see I import + by a different name so I don't need to redefine it (which is not allowed).

Here's the library implementation:

#!r6rs

(library (sylwester generic)         
  (export define-generic add-method)
  (import (rnrs))

  (define add-method-tag (make-vector 1))

  (define-syntax define-generic
    (syntax-rules ()
      ((_ name default-procedure)
       (define name 
         (let ((procs (list (cons (lambda x #t) default-procedure))))
           (define (add-proc id pred proc)
             (set! procs (cons (cons pred proc) procs)))

           (add-proc #t
                 (lambda x (eq? (car x) add-method-tag))
                 add-proc)
           (lambda x
             (let loop ((procs procs))
               (if (apply (caar procs) x)
                   (apply (cdar procs) x)
                   (loop (cdr procs))))))))))

  (define (add-method name pred proc)
    (name add-method-tag pred proc)))

As you can see I use message passing to add more methods.

Ferino answered 30/5, 2014 at 19:31 Comment(0)
T
0

The trick is to define your own, extended function so it shadows the standard function but calls the standard function when it's needed. Inside your extended function, you can do an import to get at the standard function. Here's a version of + that also accepts strings:

(define +
  (lambda args
    (if (number? (car args))
        (let ()
          (import (scheme))
          (apply + args))
        (apply string-append args))))

(This is a little sloppy in that it assumes there is at least one argument and it only checks the type of the first argument. But it illustrates the technique.)

Tannertannery answered 27/5, 2014 at 0:55 Comment(6)
This fails if you want to use the same approach to extend the function to another type.Comity
Good point. Your answer is much better. I'm leaving mine up, just so people can see how not to do it.Tannertannery
Actually, one thing this is good for is extending lambda or a macro, since those can't be bound in a let. By extending lambda, I don't mean extending a lambda, but redefining what lambda itself means in general, by extending the standard definition of lambda. It would be better to use a technique that allows chaining to whatever the previous definition of lambda was. Is there a way to do that?Tannertannery
AFAICT the only way is in two steps. For instance, to redefine lambda to display the string "hello" every time a function defined with it is called: (define-syntax old-lambda lambda), then (define-syntax lambda (syntax-rules () ((_ (args ...) body ...) (old-lambda (args ...) (display "hello") body ...)) ((_ args body ...) (old-lambda args (display "hello") body ...)))) Edit: actually, that one won't chain, anyway. The issue is that macros are expanded "lazily", so it just goes into an infinite loop if you try to chain it.Comity
Will the next extension of lambda trash the preceding extension's old-lambda? Edit: Oops, that looks like a yes.Tannertannery
I asked a new question on this subject, since the discussion's getting a little too complex for commnets (and also since I don't know the answer).Comity
T
0

Not pure Scheme, but in Guile for example you can use the CLOS-like OO system:

scheme@(guile-user)> (use-modules (oop goops))
scheme@(guile-user)> (define-method (+ (x <string>) ...) (string-append x ...))
scheme@(guile-user)> (+ "a" "b")
$1 = "ab"
scheme@(guile-user)> (+ "a" "b" "c")
$2 = "abc"
scheme@(guile-user)> (+ 1 2)
$3 = 3
scheme@(guile-user)> (+ 1 2 3)
$4 = 6
Tic answered 27/5, 2014 at 9:32 Comment(0)
G
0

You can't use

(define +
  (let ((old+ +))
    ...))

because define sets up a recursive environment for its init form. Thus when evaluating + in (old+ +) it will be unbound. As such:

> (define + 
   (let ((old+ +))
     (lambda (a b) (display "my+") (old+ a b))))
Unhandled exception
 Condition components:
   1. &undefined
   2. &who: eval
   3. &message: "unbound variable"
   4. &irritants: (+)

The following works:

> (define old+ +)
> (define + (lambda (a b) (display "my+\n") (old+ a b)))
> (+ 1 2)
my+
3

although it is not so pretty.

Giff answered 27/5, 2014 at 14:18 Comment(7)
Interesting... it works for me on MIT/GNU Scheme (r5rs). Where in the docs does it say that define makes a recursive environment?Comity
@IstvanChung: Pretty sure this wont work in R6RS or R7RS. You can't redefine an imported binding.Mesne
@Mesne Note: please put your comments regarding my answer on my answer itself. The question is also tagged r5rs. In addition, there appears to be some way to do it in pretty much every implementation, though it takes a couple steps in some.Comity
@IstvanChung: I was replying at your comment, not your answer (and more of inline with this answer).Mesne
R6RS, specifically Ikarus.Giff
@IstvanChung After reading the whole debate I'm confusing which question to mark as correct. Can you guys please edit the answer and let more clear if all this works on r5rs, r6rs and/or r7rs? I added the r6rs tag as well as suggested. Thank you, I'll wait more before mark the answer as correct.Panchito
@FelipeMicaroniLalli As far as I can tell, it doesn't necessarily work under r6rs and r7rs. However, it's unclear whether any solution could possibly work, as there are restrictions on redefining imported bindings.Comity
L
0

In R7RS-large (or in any Scheme, really), you can use SRFI 128 comparators, which package up the ideas of equality, ordering, and hashing, in order to make generic comparisons possible. SRFI 128 allows you to create your own comparators and use them in comparator-aware functions. For example, <? takes a comparator object and two or more objects of the type associated with the comparator, and returns #t if the first object is less than the second object in the sense of the comparator's ordering predicate.

Lomond answered 14/10, 2018 at 3:59 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.