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

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

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


person Felipe    schedule 27.05.2014    source источник
comment
Этот вопрос становится все более и более интересным, особенно после того, как вы пометили r7rs. Мне бы также понравился тег r6rs. Бьюсь об заклад, вам нужно будет свернуть свои собственные дженерики или что-то в них.   -  person Sylwester    schedule 28.05.2014
comment
Просто добавил r6rs, как вы предложили. Спасибо.   -  person Felipe    schedule 28.05.2014


Ответы (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 Scheme, Guile, Racket, Chicken, TinyScheme и SCSH. Однако в некоторых реализациях, таких как Biwa Scheme, необходимо использовать 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> привязан.

person Lily Chung    schedule 27.05.2014
comment
Разве старый+ не будет неопределенным? - person Sylwester; 27.05.2014
comment
@Sylwester Посмотрите еще раз - это связано с let. - person Lily Chung; 27.05.2014
comment
Заметка о Рэкете. Ни один из подходов не работает по умолчанию. Например. #!racket ; ==> module: duplicate definition for identifier in: + и #!r6rs ; ==> module: identifier is already imported in: +. Он работает в рэкете на устаревшем языке R5RS (не языке модуля #!r5rs) со снятой специальной настройкой Запретить переопределение исходной привязки. В Ikarus вы получите исключение multiple definitions of identifier, если запустите с --r6rs-script. REPL, вероятно, более снисходителен, но это не поможет, если вы используете его для проекта. - person Sylwester; 28.05.2014
comment
@ Сильвестр Я не очень хорошо понял твою похвалу. Это решение работает на r6rs, а также на r7rs? - person Felipe; 28.05.2014
comment
@FelipeMicaroniLalli Я тестировал R6RS как на ikarus, так и на ракетке, и это не работает. Мне удалось заставить его работать в устаревшем режиме r5rs в рэкете. В Chibi-схеме (единственная реализация R7RS) define является letrec, что делает + неопределенным во время привязки old+ (как я и предсказывал в своем первом комментарии), но работает вторая версия, в которой используется временный символ new+. - person Sylwester; 28.05.2014
comment
Это не работает с макросами, верно? - person Sandra; 21.04.2021
comment
@ Сандра Наверное, нет? Но ладно, я написал этот пост 7 лет назад. - person Lily Chung; 21.04.2021

Пока решения работают менее чем оптимально в среде 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)))

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

person Sylwester    schedule 30.05.2014

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

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

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

person Ben Kovitz    schedule 27.05.2014
comment
Это не удастся, если вы хотите использовать тот же подход для расширения функции до другого типа. - person Lily Chung; 27.05.2014
comment
Хорошая точка зрения. Ваш ответ намного лучше. Я оставлю свой, просто чтобы люди видели, как нельзя этого делать. - person Ben Kovitz; 27.05.2014
comment
На самом деле, это хорошо для расширения lambda или макроса, так как они не могут быть связаны в let. Под расширением lambda я имею в виду не расширение a lambda, а переопределение того, что в целом означает само lambda, путем расширения стандартного определения lambda. Было бы лучше использовать технику, позволяющую сцеплять любое предыдущее определение lambda. Есть ли способ сделать это? - person Ben Kovitz; 27.05.2014
comment
AFAICT единственный способ состоит из двух шагов. Например, чтобы переопределить lambda для отображения строки hello каждый раз, когда вызывается определенная с ней функция: (define-syntax old-lambda lambda), затем (define-syntax lambda (syntax-rules () ((_ (args ...) body ...) (old-lambda (args ...) (display "hello") body ...)) ((_ args body ...) (old-lambda args (display "hello") body ...)))) Редактировать: на самом деле, это все равно не будет цепочкой. Проблема в том, что макросы раскрываются лениво, поэтому, если вы попытаетесь связать их в цепочку, они просто войдут в бесконечный цикл. - person Lily Chung; 28.05.2014
comment
Будет ли следующее расширение lambda удалять предыдущее расширение old-lambda? Редактировать: Ой, похоже да. - person Ben Kovitz; 28.05.2014
comment
Я задал новый вопрос по этому вопросу, так как обсуждение становится слишком сложным для commnets (а также так как я не знаю ответа). - person Lily Chung; 28.05.2014

Не чистая схема, но в Guile, например, вы можете использовать CLOS-подобная объектно-ориентированная система:

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
person uselpa    schedule 27.05.2014

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

(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

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

person GoZoner    schedule 27.05.2014
comment
Интересно... у меня работает на схеме MIT/GNU (r5rs). Где в документах говорится, что define создает рекурсивную среду? - person Lily Chung; 27.05.2014
comment
@IstvanChung: Почти уверен, что это не будет работать в R6RS или R7RS. Вы не можете переопределить импортированную привязку. - person leppie; 27.05.2014
comment
@leppie Примечание: пожалуйста, оставьте свои комментарии относительно моего ответа в самом моем ответе. Вопрос также помечен тегом r5rs< /а>. Кроме того, кажется, что это можно сделать практически в каждой реализации, хотя в некоторых требуется несколько шагов. - person Lily Chung; 27.05.2014
comment
@IstvanChung: я отвечал на ваш комментарий, а не на ваш ответ (и многое другое, встроенное в этот ответ). - person leppie; 27.05.2014
comment
R6RS, точнее Икарус. - person GoZoner; 27.05.2014
comment
@IstvanChung Прочитав всю дискуссию, я не понимаю, какой вопрос отметить как правильный. Не могли бы вы, ребята, отредактировать ответ и уточнить, работает ли все это на r5rs, r6rs и/или r7rs? Я добавил тег r6rs, а также предложил. Спасибо, я подожду еще, прежде чем отметить ответ как правильный. - person Felipe; 28.05.2014
comment
@FelipeMicaroniLalli Насколько я могу судить, это не обязательно работает под r6rs и r7rs. Однако неясно, сработает ли какое-либо решение, поскольку существуют ограничения на переопределение импортированных привязок. - person Lily Chung; 28.05.2014

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

person John Cowan    schedule 14.10.2018