6.21 Classes - naver/lispe GitHub Wiki

LispE Class System Documentation

The LispE class system provides a simple yet powerful way to create and manage objects. It integrates seamlessly with other language features like pattern matching and predicates.

1. Defining a Class ✏️

You define a class using the class@ macro. The definition includes the class name, its fields, and its methods.

Syntax

(class@ ClassName (field1 field2 ...)(defun method1 ...)(defun method2 ...))

Fields

Fields are the data members of the class. They are defined in a list following the class name.

  • Simple Fields: Declared as a simple atom (e.g., x, y). Their initial value is provided during instantiation.

  • Fields with Default Values: Declared as a list containing the atom and its default value (e.g., (z 1)). This value is used if one is not provided during instantiation.

Methods

Methods are functions bound to a class that operate on its instances. They are defined using defun inside the class block. Methods have direct access to the fields of their instance.

Example

; Defines a class named 'truc' with fields x, y, and z
(class@ truc (x y z)

   (defun appel(u)
      (setq x 100)
      (println x y z)
      (+ x y z u)
   )
)

; Defines a class named 'machin' with fields x, y, and z (with a default value of 1).

(class@ machin (x y (z 1))
    ; Defines a method 'appel' that takes one argument 'u'.
    (defun appel(u)
       (println 'machin)(+ x y z u)) 

    ; Defines a method 'configure' that takes one argument 'e'.
    (defun configure(e)
        (setqi x e)
        (setqi v 100))) ; add a new field: `v`

2. Creating an Instance (Instantiation) ✨

You create an instance of a class by calling the class name as if it were a function.

Syntax

(ClassName arg1 arg2 ...)

The arguments are mapped positionally to the fields defined in the class.

  • (setq r (truc 10 20 30)) creates an instance of truc, setting x to 10, y to 20, and z to 30.

  • (setq m (machin 10 12)) creates an instance of machin, setting x to 10, y to 12, and z to its default value of 1.

3. Working with Instances ⚙️

Once you have an instance, you can call its methods and access or modify its fields.

Calling Methods

To call a method, you use the instance variable as a function, passing the method call as an argument.

  • Single Call: (r (appel 10)) calls the appel method on the r instance with the argument 10.

  • Chained Calls: You can chain multiple method calls. The value of the last call in the chain is returned.

    • (m (configure 1000) (appel 20)) first calls configure on m and then calls appel. The final result is the value returned by (appel 20).

Call with class definition

Note that in order to speed processing, you can also provide the class definition in the call: (instance class (call) (call)...). This call is a bit more verbose, but it allows the interpreter to know in advance the type of the instance and the execution is then more efficient.


(r truc (appel 10)) ; we add the class definition in the call itself

Accessing Fields: The @ Operator

To access field values from outside the class, use the @ operator.

  • Access by Name: (@ m 'x) retrieves the value of field x from instance m.

  • Access by Index: (@ m 1) retrieves the value of the second field (0-indexed), which is y, from instance m.

Modifying Fields: setq vs. setqi

You can modify an instance's fields from within its methods.

  • setq: This is the standard assignment operator. It will modify the value of an existing field locally in the function, but won't modify the field itself.

  • setqi: This is a special "instance" assignment operator with dual functionality.

    • Modify: If the field exists, setqi modifies the existing field x.

    • Create: If the field does not exist, setqi dynamically adds the new field to the instance. (setqi v 100) creates a new field v on the instance and sets its value to 100.

  • Access by Name: (set@ m 'x 1000) modifies the value of field x from instance m.

  • Access by Index: (set@ m 1 23) modifies the value of the second field (0-indexed), which is y, from instance m.

4. Derivation

You can derive a class from another class with the operator: from@. from@ replaces the argument definition of your class. When you derive from another class, the arguments are copied into the new class. You can then add other arguments after:


(class@ mother (x y)
   (defun displaying()
      (println x y)
    )
   
    (defun test(a)
       (if (eq a x) y x)))

(class@ daughter (u) (from@ mother) ; we have now: u x y as arguments
    (defun test(a) ; this function replaces the mother function...
       (if (eq a x) u x))

    (defun call_mother(a)
        (from@ mother (test a)))

; note that, if the class doesn't add arguments:

(class@ daughterbis (from@ mother) ; we have now: x y as arguments
    (defun test(a) ; this function replaces the mother function...
       (if (eq a x) (+ x y) x))

    (defun call_mother(a)
        (from@ mother (test a)))

Note that the from@ can also be used to call the mother function of a derived class.

5. Constructor and destructor

It is possible to define constructor and destructor functions. However, contrary to most languages, there is no specific name for these functions. The constructor is simply a function that can be called when creating an instance. While the destructor is a function, which is defined with (toclean 'destructor).

(@class test(x init)
     (defun config() 
          ; clean is now the function that will be called when the instance is deleted
          (toclean 'clean)
          100
      )
      (defun clean()
          (println 'cleaning)
      )
)

(test 10 (config)) ; the constructor is an inner method in test called as an argument. The result of config is stored in: init

; At the end of the process:
; x is 10
; init is 100

Any functions in the class can be used either as a constructor or as a destructor.

6. Advanced Integration 🧩

LispE classes are first-class citizens and work with other parts of the language.

Pattern Matching with defpat

You can deconstruct class instances using defpat. This allows you to write functions that react differently based on the class of their argument.

; Defines a pattern for 'teste' that matches instances of the 'truc' class,
; binding its fields to local variables x, y, and z.

 (defpat teste([truc x y z])
     (println 'TRUC x y z))

; Provides a different behavior for instances of 'machin'.

 (defpat teste([machin x y z u])
     (println 'MACHIN x y z u))

Usage in Control Flow

Method calls can be used as conditions in if statements or within predicates defined with defpred. A non-nil return value is treated as true.

  • Predicate: (defpred pred (x) (m (appel x)) ...) defines a predicate pred that succeeds if the (m (appel x)) call returns a true value.

  • Conditional: (if (r (appel 10)) (println 'ok) ...) executes the first branch because the result of the appel method is a number (160), which is treated as true.