Можно ли "расширить" функцию / лямбду / макрос в схеме?

Например: если я хочу функцию equal? узнать свой собственный тип или запись, могу ли я добавить новое поведение equal?? без стирания или перезаписи старого?

Или, например, если я хочу сделать функцию "+" принять также строку?

6 ответов

Решение

Вместо того, чтобы использовать importЛучшее решение состоит в том, чтобы отслеживать исходную функцию letсвязывающий это. Также лучше проверить, что тип аргумента является строкой, а не числом. Использование обоих этих подходов означает, что можно составить технику.

(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)))))

Выше будет производить + функция, которая работает с числами, строками или векторами. В общем, это более расширяемый подход.


Мне удалось убедиться, что все вышеперечисленное работает правильно в схемах MIT/GNU, Guile, Racket, Chicken, TinyScheme и SCSH. Однако в некоторых реализациях, таких как схема Бива, необходимо использовать set! вместо define, В Икарусе set! не может использоваться на импортированном примитиве, и define портит окружающую среду, поэтому необходимо сделать это в два этапа:

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

Обратите внимание, что в соответствии с R5RS, define а также set! должны быть эквивалентны в этом случае:

На верхнем уровне программы, определение

(define <variable> <expression>)

имеет практически тот же эффект, что и выражение присваивания

(set! <variable> <expression>)

если <variable> связан.

Пока что решения работают менее чем оптимально в среде R6RS / R7RS. Я думал о дженериках, когда начал играть с этим, но я не хотел использовать свою собственную систему типов. Вместо этого вы предоставляете процедуру предиката, которая должна гарантировать, что аргументы хороши для этой конкретной процедуры. Он не идеален, но работает аналогично другим ответам R5RS, и вы никогда не переопределите процедуры.

Я написал все в R6RS, но я думаю, что это легко перенести на R7RS. Вот пример:

#!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)

Как вы можете видеть, я импортирую + под другим именем, поэтому мне не нужно переопределять его (что недопустимо).

Вот реализация библиотеки:

#!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)))

Как видите, я использую передачу сообщений, чтобы добавить больше методов.

В R7RS-large (или в любой Схеме) вы можете использовать компараторы SRFI 128, которые объединяют идеи равенства, упорядочения и хеширования, чтобы сделать возможными общие сравнения. SRFI 128 позволяет создавать свои собственные компараторы и использовать их в функциях компаратора. Например, <? берет объект компаратора и два или более объектов типа, связанного с компаратором, и возвращает #t если первый объект меньше второго объекта в смысле предиката упорядочения компаратора.

Вы не можете использовать

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

так как define устанавливает рекурсивную среду для своей формы инициализации. Таким образом, при оценке + в (old+ +) это будет несвязанным. В качестве таких:

> (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: (+)

Следующие работы:

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

хотя это не так красиво.

Хитрость заключается в том, чтобы определить собственную расширенную функцию, чтобы она скрывала стандартную функцию, но при необходимости вызывала стандартную функцию. Внутри вашей расширенной функции вы можете сделать import чтобы получить в стандартной функции. Вот версия + который также принимает строки:

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

(Это немного неаккуратно, поскольку предполагается, что есть хотя бы один аргумент, и он проверяет только тип первого аргумента. Но он иллюстрирует метод.)

Не чисто Scheme, но в Guile, например, вы можете использовать CLO- подобную систему OO:

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
Другие вопросы по тегам