Search code examples
coding-stylecommon-lispclos

Request input regarding code style / best practices


I'm trying to implement a programming language in Common Lisp by following this implementation in Java (https://craftinginterpreters.com/control-flow.html).

One of the things that was being really bothersome is plenty of slot-values everywhere.

Granted I can add a line in each function with with-slots, but that's also repetitive, and given that my symbols are not :used but I refer to them by the package, as there is plenty of symbols from many modules and I don't want to lose track from where they come from, even with-slots requires the full qualified name. All this seems to me like very bad code smell.

I googled and found rutils. With its @object.slot accessor I managed to clean up my code immensely. Just look at the last commit https://github.com/AlbertoEAF/cl-lox/commit/84c0d62bf29bff9a2e0e77042a9108c168542b96?diff=split excerpt:

enter image description here

(I could paste the code but all of it is available on that repo, I wanted to show the diff highlights)

Not only it removes some characters, but more importantly, there's one less level of depth (slot-value call) and parenthesis to think about - which helps a lot but for the trivial functions.

It gets worse when I have lots and lots of symbol names and can no longer export them all because I start having conflicts in symbols.

Can you guys give me input on the code? This looks much better now but I'm curious as if to are better ways to go about it?

Thanks!


Solution

  • Defining an accessor function in the class definition would also save you some typing while not having to use non-standard syntax

    If you define a CLOS class like this:

    (defclass person ()
      ((name
        :initarg :name)))
    
    (defparameter *p* (make-instance 'person 
                         :name "John"))
    

    The only way to access then the slot value of name slot in *p* are:

    (slot-value *p* 'name)
    ;; "John"
    
    (with-slots (name) *p*
      name)
    ;; "John"
    
    (with-slots ((nm name)) *p*
      nm)
    ;; "John"
    

    But if you define for each slot an :accessor, you can use the function name given as argument of :accessor and don't have to use slot-value or with-slots, to access and to mutate it (setf-able!).

    (defclass person ()
      ((name
        :initarg :name
        :accessor nm)))
    
    (nm *p*)
    ;; "John"
    
    (setf (nm *p*) "Doe")
    ;; "Doe"
    
    (nm *p*)
    ;; "Doe"
    

    The convention however is, to use the slot name also as the :accessor method's name:

    (defclass person ()
      ((name
        :initarg :name
        :accessor name))) ;; but better use the slot name as accessor
    
    (name *p*)
    ;; "John"
    
    (setf (name *p*) "Doe")
    (name *p*)
    ;; "Doe"
    

    The :accessor method (generic function) is specific for the objects of this class. Therefore you don't need to have fear of a name-space clash.

    (defclass house ()
      ((address
         :initarg :address
         :accessor addr))) 
    ;; you cannot use `address` because it is already occupied 
    ;; by a system's function/symbol -> see under `(describe 'address)`
    (defparameter *h* (make-instance 'house :address "Bakerstreet 1"))
    
    (name *h*) 
    ;; EVAL: undefined function name OR: 
    ;; NO-APPLICABLE-METHOD error (in the case
    ;; that other classes exist with a `name` accessor method.
    
    (addr *h*)
    ;; "Bakerstreet 1"
    (addr *p*)
    ;; *** - NO-APPLICABLE-METHOD: When calling #<STANDARD-GENERIC-FUNCTION ADDR>
    ;;       with arguments (#<PERSON #x1B06B79E>), no method is applicable.
    ;;