Меняется ли представление чисел в lisp?
Я относительно новичок в LISP и пытаюсь понять, как поведение (общего) кода LISP может быть программно изменено.
Я имею в виду простой (но полностью академический) пример, который я пытаюсь решить, где заменяются определенные числовые цифры в исходном тексте. Я знаю об использовании макросов, которые могут манипулировать s-выражениями, но хотели бы что-то, что применяется глобально. Таким образом, где бы ни появлялось число 4 (например), используется значение 5. Таким образом, (+ 4 1) будет оцениваться до 6, а 44 - до 55.
Для пояснения, это чрезвычайно глупый, упрощенный подход, где любой символ 4 обрабатывается так, как если бы программист фактически набрал 5.
В принципе, это можно распространить на каждую цифру (от 0 до 9) и присвоить им другое "значение", но я не столько заинтересован в реализации, сколько в примере какого-то преобразования такого рода.
1 ответ
Вот ужасный подход к тому, что ты хочешь. Вместо того, чтобы иметь дело со считывателем Lisp, который (по причинам, которые должны быть очевидны) на самом деле не предназначен для того, чтобы иметь дело с такого рода вещами, поскольку никто никогда не захочет делать это в реальной жизни, этот код вмешивается в ввод-вывод система, чтобы определить класс потока, который поддерживает переназначение символов, с символами по умолчанию, которые переназначаются, будучи десятичными цифрами.
Это не совсем переносимый CL: он основан на некотором поведении, которое не вошло в стандарт CL, известный как "серые потоки". Многие (возможно, все) реализации на самом деле поддерживают это предложение, но есть некоторые неизбежные различия в том, как они это делают. Существует слой совместимости, называемый trivial-gray-streams
который имеет дело со всем этим.
Я не использовал этот уровень совместимости здесь: я только определил условный пакет, который работает в двух реализациях, которые я использую. Что еще более важно, я фактически не проверял, достаточно ли этот код определяет достаточное количество методов в классе, который он определяет, чтобы он действительно работал должным образом: я довольно сильно подозреваю, что это не так. Он определяет достаточно методов, чтобы он работал достаточно для этого:
> (compile-file "silly-remapping-stream" :load t)
#P"/Users/tfb/play/lisp/silly-remapping-stream.dx64fsl"
nil
nil
> (with-input-from-string (s "(print 123)")
(let ((srs (make-instance 'silly-rewriting-stream :parent s))
(read srs)))
(print 234)
Итак, как вы можете видеть, это приводит к тому, что все, что читается из такого потока, имеет эту функцию перезаписи цифр. Кроме того, вы можете, например, создать безумный REPL, как этот
(defun silly-rewriting-repl (&optional (stream *standard-input*))
(let ((srs (make-instance 'silly-remapping-stream :parent stream)))
(flet ((pread ()
(format t "~&?? ")
(force-output)
(read srs nil srs)))
(loop for f = (pread)
while (not (eq f srs))
do (pprint (eval f))))))
А потом
> (silly-rewriting-repl)
?? (defvar *three* 3)
*three*
?? *three*
4
и так далее.
Одна из причин того, что это не совсем правильный подход, заключается в том, что в приведенном выше REPL:
?? "a string containing 3"
"a string containing 4"
Это не было бы проблемой в версии, которая имела дело с вещами, использующими читаемый файл.
Обратите внимание, что я ожидаю, что любой язык, который поддерживает механизмы для пользователей, которые вмешиваются в операции ввода-вывода, может сделать что-то подобное, и я ожидаю, что большинство разумных современных языков позволяют это. CL (и Lisp в целом) необычен тем, что сам язык определяется в терминах
- читатель, который принимает потоки и возвращает объекты Lisp (и чье поведение разумно настраивается);
- и один или оба
- вычислитель, который берет объекты Lisp (не только строки или файлы) и обрабатывает их как код, который нужно оценить,
- компилятор, который принимает объекты Lisp (опять же, не только строки или файлы), представляющие определения, и компилирует их;
- принтер, который знает, как печатать объекты Lisp (и который также настраивается).
(Примечание: если есть компилятор, то оценщик может быть тривиально записан в терминах этого, так что возможно иметь реализации Lisp, которые имеют только оценщик или только компилятор, или оба.)
Вот код
;;;; A silly remapping stream
;;;
;;; This uses just enough of Gray streams to define a stream which
;;; remaps digits in an amusing way.
;;;
;;; ALMOST CERTAINLY other methods need to be defined for this stream
;;; class to be legitimate. THIS CODE IS NOT SUITABLE FOR REAL USE
;;;
;;; This code has been 'tested' (as in: I checked READ did what I
;;; thought it should) in LW 7.1.1 and the development version of CCL.
;;; Other implementations will need changes to the package definition
;;; below, or (much better) to use a compatibility layer such as
;;; trivial-gray-streams
;;; (https://github.com/trivial-gray-streams/trivial-gray-streams),
;;; which is available via Quicklisp.
;;;
(defpackage :org.tfeb.example.silly-remapping-stream
(:use :cl
#+LispWorks :stream
#+CCL :ccl)
(:export #:silly-remapping-stream))
(in-package :org.tfeb.example.silly-remapping-stream)
(defclass silly-remapping-stream (fundamental-character-input-stream)
((parent :initarg :parent
:reader srm-parent
:initform (error "no parent"))
(map :initarg :map
:reader srm-map
:initform '((#\1 . #\2)
(#\2 . #\3)
(#\3 . #\4)
(#\4 . #\5)
(#\5 . #\6)
(#\6 . #\7)
(#\7 . #\8)
(#\8 . #\9)
(#\9 . #\0)))))
(defmethod stream-read-char ((stream silly-remapping-stream))
(let ((got (stream-read-char (srm-parent stream))))
(typecase got
(character
(let ((mapped (assoc got (srm-map stream))))
(if mapped (cdr mapped) got)))
(t got))))
(defmacro define-srm-proxy-method (gf (s &rest other-args))
;; just a way of defining methods which forward to the parent stream
`(defmethod ,gf ((s silly-remapping-stream) ,@other-args)
(,gf (srm-parent ,s) ,@other-args)))
(define-srm-proxy-method stream-unread-char (s char))