Skip to content

Latest commit

 

History

History
204 lines (157 loc) · 6.96 KB

README.org

File metadata and controls

204 lines (157 loc) · 6.96 KB

A data-structure and interface-type for Emacs-Lisp

This package defines a new data-structure named Struct and an interface-like type called Trait.

Struct

A struct is a simple data-structure containing a set of properties. As such it is much like c-struct or cl-defstruct.

(Struct:define Rectangle width height)

This would define

  1. a constructor function Rectangle accepting the properties as keywords-value pairs.
  2. a predicate Rectangle? that checks if any object is a Rectangle,
  3. another type-constructor in form of a macro which supports a form of spread-syntax and
  4. a cl-type predicate which can be used with cl-check-type.

A struct value is represented as a plain list where the car holds the name (symbol) and the cdr its properties. This makes a struct self-evaluating, except that properties are always printed in definition-order.

(Rectangle :height 10 :width 20)     ; (Rectangle :width 20 :height 10)
(let ((other (Rectangle :height 10)) ; (Rectangle :width 20 :height 10)
      (width 20))
  (Rectangle* ,@other width))

Struct Properties

Instead of just using symbols, properties can be defined using the extended list-form. This allows for the definition of a number of meta-properties.

(Struct:define Rectangle
  (width :default 0)
  (height :default 0))

The following list describes the meta-properties that can be defined in this way.

default
Provides a default value for this property. The form may access the values of properties declared earlier in the definition.
documentation
Defines a documentation string for this property.
mutable
Determines whether this property can be changed after the struct value was created. Properties are immutable by default.
type
Provides a type for this property, see also Emil.

Here is another definition of a rectangle using all of those properties.

(Struct:define Rectangle
  "Defines a rectangle."
  (width 
   :documentation "The width of the rectangle."
   :default 0
   :mutable t
   :type number)
  (height 
   :documentation "The height of the rectangle."
   :default width
   :mutable t
   :type number))

With this definition the following invocations of the constructor would lead to the mentioned results.

(Rectangle)                       ; (Rectangle :width 0 :height 0)
(Rectangle :width 100)            ; (Rectangle :width 100 :height 100)
(Rectangle :width 10 :height 20)  ; (Rectangle :width 10 :height 20)
(Rectangle :width "10")           ; Wrong type argument: number, "10"

There are a number of related functions operating on struct-values and -definitions available.

(Struct:get (Rectangle :width 20) :width)    ; 20
(Struct:set (Rectangle :width 20) :width 10) ; 10
(Struct:get '(Circle) :width) ; Wrong type argument: Struct:Name, Circle
(Struct:Type:get 'Rectangle)  ; (Struct:Type :name Rectangle ...)

Struct implementation

Having defined a struct, we can implement some methods for it via the Struct:implement macro.

(Struct:implement Rectangle
  (fn area (self -> number)
    (* self.width self.height))

  (fn <= (self (other Rectangle) -> boolean)
    (<= (self.area) (other.area))))

There are a couple of things to note here.

  1. The Rectangle “namespace” is automatically added to the definition of the functions, i.e. we can use area and the macro will define a function Rectangle:area.
  2. Both functions provide type-annotations in their arguments as well as return value. (The self argument automatically has type Rectangle.)
  3. These type-annotations allow for the type-system to verify that self and other both have properties width and height, as well as a method area.
  4. This allows for an implementation of a kind of dot-operator when accessing properties and methods, e.g. self.width and (self.area). It is not really an operator, since it can only be used as part of a symbol.

The above snippet will roughly expand to the code below.

(defun Rectangle:area (self)
  (cl-check-type self Rectangle)
  (*
   (plist-get (cdr self) :width)
   (plist-get (cdr self) :height)))

(defun Rectangle:<= (self other)
  (cl-check-type self Rectangle)
  (cl-check-type other Rectangle)
  (<=
   (Rectangle:area self)
   (Rectangle:area other)))

The following implementation however would not compile, since it contains 2 type-errors. Note the typo in heigth.

(Struct:implement Rectangle
  (fn area (self -> number)
    (* self.width self.heigth))     ;Type error: "Can not find property `heigth' in type `Rectangle'"

  (fn <= (self (other Rectangle) -> boolean)
    (<= (self.size) (other.size)))) ;Type error: "Can not find method `size' in type `Rectangle'"

Traits

A trait is very similar to what is called an interface in other languages. It defines a set of function signatures, each with a mandatory self argument and an optional default implementation.

Invocations of these functions are dynamically dispatched on the first argument, which must be a type having implemented the corresponding trait. These implementations are independent of the definition of the type. Thus a trait can be implemented for somebody elses type.

Here is a definition of the ubiquitous Shape trait:

(Trait:define Shape ()
  (fn area (self -> number)))

This can then be implemented for the Rectangle type defined earlier.

(Trait:implement Shape Rectangle
  (fn area (self)
    (* self.width self.height)))

Let’s add a second shape-type and also implement the trait for it.

(Struct:define Circle
  (radius :type number))

(Trait:implement Shape Circle
  (fn area (self)
    (* pi self.radius self.radius)))

We can use these implementations as in the following example.

(Shape:area (Circle :radius 4))             ; 50.26548245743669
(Shape:area (Rectangle :width 4 :height 5)) ; 20

Or use the trait in yet another type. This snippet also demonstrates the use of the setf macro on properties.

(Struct:define ShapeCollection
  (shapes :type (List (Trait Shape)) :mutable t))

(Struct:implement ShapeCollection
  (fn area (self)
    (let ((sum 0))
      (dolist (shape self.shapes sum)
        (cl-incf sum (shape.area)))))

  (fn add (self (shape (Trait Shape)))
    (setf self.shapes (cons shape self.shapes))))
(let ((collection (ShapeCollection :shapes (list (Circle :radius 4))))) 
  (ShapeCollection:add collection (Rectangle :width 4 :height 5))
  (ShapeCollection:area collection))    ; 70.26548245743669