2

I found myself calling lots of methods whose first argument is a complex object from a given class. Whilst with-slots and with-accessors are useful, generic methods cannot be bound in this way. So I thought: if we could locally curry any functions, slots + accessors + generic functions + functions could all be addressed with the same construct.

Example of code I want to clean up:

(defun clox-string (scanner)
  "Parse string into a token and add it to tokens"
  (loop while (and (char/= #\" (peek scanner))
                   (not (at-end-p scanner)))
        do
           (if (char= #\Newline (peek scanner)) (incf (line scanner))
               (advance scanner)))
  (when (at-end-p scanner)
    (clox.error::clox-error (line scanner) "Unterminated string.")
    (return-from clox-string nil))
  (advance scanner) ;; consume closing "
  (add-token scanner 'STRING (subseq (source scanner)
                                     (1+ (start scanner))
                                     (1- (current scanner)))))

This would be cleaner (I'm imitating this in CL https://craftinginterpreters.com/scanning.html#reserved-words-and-identifiers but I often end up with more verbose and less readable code than in Java - specially when using this classes a lot). As in CL methods don't belong to classes you end up declaring such arguments over and over. This would be a bit better:

(defun clox-string (scanner)
  "Parse string into a token and add it to tokens"
  (let-curry scanner (peek at-end-p line source start current advance add-token)
   (loop while (and (char/= #\" (peek))
                    (not (at-end-p)))
         do
            (if (char= #\Newline (peek)) (incf (line))
                (advance)))
   (when (at-end-p)
     (clox.error::clox-error (line) "Unterminated string.")
     (return-from clox-string nil))
   (advance) ;; consume closing "
   (add-token 'STRING (subseq (source)
                              (1+ (start))
                              (1- (current)))))

sketch of macro (not working):

;; Clearly not as I don't understand macros very well :) non-working code:
(defmacro let-curry (obj functions &body body)
  "Locally curry all functions"
  (let ((fn (gensym)))
    `(flet (loop
             for ,fn in ,functions
             collect (list ,fn (&rest args)
                           (funcall ,fn ,obj args))) 
       ,@body)))

EDIT (ADD): Notice that scanner is a class; start, source, line, etc., accessors to the slots with the same name; add-token a generic function of more than one argument, advance a generic method of one argument:

(defclass scanner ()
  ((source
    :initarg :source
    :accessor source)
   ...
   (...)))

(defmethod advance ((scanner scanner)) ...)
(defmethod add-token ((scanner scanner) token-type) ...)

Simpler Example with error:

;; With 
(defun add (x y) (+ x y))

(defun mul (x y) (* x y))

;; I want to have this:
(let-curry 1000 (add mul)
  (print (add 3))
  (print (mul 3)))


;; expanding to:
(flet ((add (y) (add 1000 y))
       (mul (y) (mul 1000 y)))
  (print (add 3))
  (print (mul 3)))

;; but instead I'm getting:
Execution of a form compiled with errors.
Form:
  (FLET (LOOP
       FOR
       #1=#:G777
       IN
       (ADD MUL
         )
       COLLECT
       (LIST #1#
         (&REST ARGS)
         (FUNCALL #1# 1000 ARGS)))
  (PRINT (ADD 3))
  (PRINT (MUL 3)))
Compile-time error:
  The FLET definition spec LOOP is malformed.
   [Condition of type SB-INT:COMPILED-PROGRAM-ERROR]

Thanks! The basic question is: is it possible to make such macro work?

Alberto
  • 565
  • 5
  • 14
  • 1
    What do you mean by *"Whilst with-slots and with-accessors are useful, generic methods cannot be bound in this way."*? with-accessors is a bit verbose, but it seems to work: https://pastebin.com/y02JqdjW – coredump May 21 '20 at 06:25
  • 2
    Also, note the "crafting interpreter" gives one possible architecture among different valid ones, and in particular the one being presented suits better the Java language. If the translation is a bit too literal, you might have not idiomatic Lisp (the exercise is interesting nonetheless) – coredump May 21 '20 at 11:13
  • See for example: https://pastebin.com/5YyX6qNJ – coredump May 21 '20 at 12:09
  • 1
    @coredump: I meant that some of those are accessors (like line or source), some are generic functions like (defmethod add-token ((scanner scanner) (token-type token-type)) ...). I thought with-accessors only works for when you define :accessor in defclass? I.e.: (defclass scanner () ((source :initarg :source :accessor source) (...) ...))? – Alberto May 21 '20 at 19:52
  • 1
    Still on your comment #1, looking at your code, you mean accessors can be defined elsewhere or defmethods in general can also be used as accessors ? (pastebin.com/y02JqdjW) ? :o <- if your answer is yes then I believe I don't known what an accessor is really – Alberto May 21 '20 at 19:53
  • Regarding your third comment, whoa, I'll need to read it more times to understand, but thank you! – Alberto May 21 '20 at 19:56
  • 1
    Right, I was surprised at first too, but accessor can be any function, the macro just allows to hide function calls behind a symbol. The third comment: all tokens are associated with a [`CL-PPCRE`](https://edicl.github.io/cl-ppcre/) regex, and they are all combined as one big alternation ("|" in regex), with named-registers (order matters, the first match wins). The rest is just calling regex matching over the string, and from the named-registers (capture groups), find back what type of token is read. It is just to show how the approach could be different from the one shown in Java. – coredump May 21 '20 at 20:20
  • Thanks so much @coredump! – Alberto May 21 '20 at 20:59

1 Answers1

6

Your version didn't expand to what you wanted but:

(flet (loop for #:g8307 in (add mul) collect (list #:g8307 (&rest args) (funcall #:g8307 1000 args))) 
  (print (add 3)) (print (mul 3)))

Now the loop needs to be done at macro expansion time. Here is a working version:

(defmacro let-curry (obj (&rest functions) &body body)
  "Locally curry all functions"
  `(flet ,(loop for fn in functions
                collect `(,fn (&rest args)
                            (apply #',fn ,obj args))) 
     ,@body))

;; test it using add and mul from OP
(macroexpand-1 '(let-curry 10 (add mul) (list (add 5) (mul 5))))
;; ==> 
(flet ((add (&rest args) (apply #'add 10 args)) 
       (mul (&rest args) (apply #'mul 10 args))) 
  (list (add 5) (mul 5)))

(let-curry 10 (add mul) (list (add 5) (mul 5)))
;; ==> (15 50)
  • Using gensym is only needed if you are in danger of shadowing/colliding something or to ensure evaluation order is least surprising, but in your case you actually want to shadow the original names with the curried version so it makes sense to just use the original name.
  • If you want to have more than one argument you should use apply
  • since you know the function is in the function namespace you need to call #'symbol instead of symbol.
  • I've done (&rest functions) instead of functions in the prototype that with bad usage (not a list) you get a compile time error and it is more preciese.
coredump
  • 37,664
  • 5
  • 43
  • 77
Sylwester
  • 47,942
  • 4
  • 47
  • 79
  • 1
    SBCL complains about binding `-` to a local function in your example. `Compile-time error: Lock on package COMMON-LISP violated when binding - as a local function while in package COMMON-LISP-USER.` – Martin Buchmann May 21 '20 at 08:17
  • 2
    @MartinBuchmann Thanks. I've changed the functions to avoid this, but there are ways to unlock and avoid this should one need it. It just isn't in scope for this answer :) – Sylwester May 21 '20 at 09:07
  • This is really neat! It's working! However, my new methods are not setf-able :s <- I didn't see this one coming. Is there a way to fix that too? Maybe we need to replace flet with something else more like a curry "macro" that expands each curried function call to the original textual code we'd be using without having let-curry? – Alberto May 21 '20 at 21:46
  • 1
    @Alberto Everything can be fixed, but not in the same question. – Sylwester May 21 '20 at 22:34
  • Ahaha sure I'll open a new one and post the link here when I do, thanks :) – Alberto May 22 '20 at 00:00
  • Just opened the new question here: https://stackoverflow.com/questions/61987738/common-lisp-locally-shadow-function-with-same-name – Alberto May 24 '20 at 16:33