Search code examples
common-lisp

Asking for the idomatic way respectively for avoiding a duplicate in the code


(Just for fun) I figured out a way to represent this:

250 : 8 = 31 + 2
 31 : 8 = 3 + 7
∴ (372)8

in the following procedure:

(defun dec->opns (n base)
   (do* ((lst nil (append lst (list pos))) ; this is also not so nice
         (m n (truncate m base))
         (pos (rem m base) (rem m base)) ) ; <<<<<<
        ((< m base) (reverse (append lst (list m)))) ))

The procedure does what it is supposed to do until now.

 CL-USER> (dec->opns 2500000 8)
 (1 1 4 2 2 6 4 0)

At this point, I simply ask myself, how to avoid the two times

(rem m base).

First of all because of duplicates are looking daft. But also they may be a hint that the solution isn't the elegant way. Which also is not a problem. I am studying for becoming a primary school teacher (from 1st to 6nd class) and am considering examples for exploring math in a sense of Paperts Mindstorms. Therefore exploring all stages of creating and refining a solution are welcome.

But to get a glimpse of the professional solution, would you be so kind to suggest a more elegant way to implement the algorithm in an idiomatic way?

(Just to anticipate opposition to my "plan": I have no intentions to overwhelm the youngsters with Common Lisp. For now, I am using Common Lisp for reflecting about my study content and using the student content for practicing Common Lisp. My intention in the medium term is to write a "common (lisp) Logo setup" and a Logo environment with which the examples in Harveys Computer Science Logo style (vol. 1), Paperts Mindstorms, Solomons et. al LogoWorks, and of course in Abelsons et. al Turtle Geometry can be implemented uncompromisingly. If I will not cave in, the library will be found with quickload in the still more distant future under the name "c-logo-s" and be called cλogos ;-) )


Solution

  • The closest to your code

    You can reduce the reversing of the reversing and append -> by using cons only. The duplication of (rem m base) is only an optical issue, since the first (rem m base) gets executed only the first time the loop runs and the second (rem m base) in all other cases. Thus they are actually not a duplication. One cannot use a let here, because of the required syntax within the macro. (<variable> <initial-value> <progression-for-each-round>)

    (defun dec->ops (n base)
      (do* ((acc nil (cons r acc))
            (m n (truncate m base))
            (r (rem m base) (rem m base)))
           ((zerop m) acc)))
    

    The most Common Lispy version

    The rosetta solutions for Common Lisp seems to give the most Common Lisp-like ways - either using write-to-string/parse-integer or even some format quircks.

    (defun decimal->base-n (n base)
      (write-to-string n :base base))
    (defun base-n->decimal (base-n base)
      (parse-integer (format nil "~a" base-n) :radix base))
    
    (defun decimal-to-base-n (number &key (base 16))
      (format nil (format nil "~~~dr" base) number))
    
    (defun base-n-to-decimal (number &key (base 16))
      (read-from-string (format nil "#~dr~d" base number)))
    
    ;; or:
    
    (defun change-base (number input-base output-base)
      (format nil "~vr" output-base (parse-integer number :radix input-base)))
    

    Source: https://rosettacode.org/wiki/Non-decimal_radices/Convert#Common_Lisp

    (decimal-to-base-n 2500000 :base 8)
    ;;=> "11422640"
    

    Solution without format or write-to-string/parse-integer

    Use tail call recursion:

    (defun dec->ops (n base &optional (acc nil))
      (if (< n base) 
          (cons n acc)
          (multiple-value-bind (m r) (truncate n base)
            (dec->ops m base (cons r acc)))))
    

    Try it:

    [41]> (dec->ops 250 8)
    (3 7 2)
    [42]> (dec->ops 250000 8)
    (7 5 0 2 2 0)
    [43]> (dec->ops 2500000 8)
    (1 1 4 2 2 6 4 0)
    

    The do/do* macros are in this case not so nice, because one cannot capture the multiple values returned by truncate nicely (truncate is mod and rem in one function - one should use this fact).

    If you really wants to use do*

    (defun dec->ops (n base)
      (do* ((acc nil (cons (second values) acc))
            (values (list n) (multiple-value-list (truncate (first values) base))))
           ((< (first values) base) (nbutlast (cons (first values) (cons (second values) acc))))))
    

    This works

    [69]> (dec->ops 250 8)
    (3 7 2)
    [70]> (dec->ops 2500000 8)
    (1 1 4 2 2 6 4 0)