Search code examples
lisp

Changing the representaion of numbers in lisp?


I a relatively newcomer to lisp and trying to understand how the behaviour of (common) lisp code can be changed programatically.

I have in mind a simple (but entirely academic) example that I'm trying to resolve where certain numeric digits in the source text are replaced. I'm aware of using macros which can manipulate s-expressions, but would like something which applies globally. So anywhere the number 4 appears (for example), the value 5 is used. So, (+ 4 1) would evaluated to 6 and 44 would evaluate to 55.

To clarify, this is the extremely dumb, simplistic approach where any character 4 is treated as if the programmer had actually typed a 5.

In principle this could be extended to every digit (0 to 9), and assigning them another "meaning", but I'm not so much interested in implementation as I am in an example of some kind of transformation of this sort.


Solution

  • Here is a horrible approach to doing what you want. Rather than dealing with the Lisp reader, which is (for reasons which should be obvious) really not meant to deal with this sort of thing since no-one would ever want to do it in real life, this code intervenes in the I/O system, to define a stream class which supports remapping characters, with the default characters that get remapped being decimal digits.

    This is not quite portable CL: it relies on some behaviour which did not make it into the CL standard, known as 'Gray streams'. Many (perhaps all) implementations actually support this proposal but there is some inevitable variation in how they do so. There is a compatibility layer, called trivial-gray-streams which deals with all this.

    I have not used that compatibility layer here: I just defined a conditionalised package which works in the two implementations I use. More significantly I have not actually checked if this code defines enough methods on the class it defines for it really to work properly: I suspect rather strongly that it does not. It does define enough methods for it to work enough for this to work:

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

    So, as you can see, this causes anything read from such a stream to have this digit-rewriting thing. On top of this you could, for instance, build a mad REPL like this

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

    And then

    > (silly-rewriting-repl)
    ?? (defvar *three* 3)
    
    *three*
    ?? *three*
    
    4
    

    and so on.

    One reason that this is not really the right approach is that, in the above REPL:

    ?? "a string containing 3"
    
    "a string containing 4"
    

    This would not be a problem in a version which dealt with things using the readtable.

    Note that I expect any language which supports mechanisms for users intervening in I/O can do something like this, and I expect most reasonable modern languages allow this. Where CL (and Lisp in general) is unusual is that the language itself is defined in terms of

    • a reader, which takes streams and returns Lisp objects (and whose behaviour is reasonably customisable);
    • and one or both of
      • an evaluator which takes Lisp objects (not just strings, or files) and treats them as code to be evaluated,
      • a compiler which takes Lisp objects (again, not just strings or files) representing definitions and compiles them;
    • a printer which knows how to print Lisp objects (and which is also customizable).

    (Note: if there is a compiler then the evaluator can trivially be written in terms of it, so it's possible to have Lisp implementations which have only an evaluator or only a compiler, or both.)


    Here is the code.

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