~subsetpark/fugue

Objects for Janet.

98b59f8 Defn new- form

6 days ago

8dd16a2 Initial with-slots

8 days ago

#fugue API

An object system for Janet, inspired by CLOS.

The Janet language provides a simple form of object orientation using tables and prototypes.

#methods

Janet tables can have methods by storing functions at a keyword on the table. When called with method syntax (:method-name object), the call is translated into a function call to that function with the object itself as the first argument.

#inheritance

Janet provides prototypal inheritance. Any table may have a prototype set, whereupon any value access at that table that returns nil will recurse upwards to the table's prototype.

#fugue

fugue provides a more powerful set of commands that extend the existing OO dynamics of Janet with a more elaborate interface and set of features.

#fugue/defproto

defproto is the main entrypoint to fugue, allowing users to define Prototypes. These are Janet tables with some additional metadata included, as well as a constructor method added at :new. defproto takes arguments that govern how new instances are created, as well as allocation rules, ie, the configuration determining which object fields should be populated at the instance level and which at the prototype level.

#fugue/defgeneric / fugue/defmethod

defgeneric provides the interface for creating and managing Generic Functions---functions which can be extended for additional Prototypes. Generic functions may have a default behaviour, which is then specialized for Prototypes via defmethod. Methods can be specialized for a single argument---the first argument---and child prototypes can inherit methods from their ancestors. They can also call ancestor methods by invoking (prototype-method) within their own method bodies.

#fugue/defmulti

defmulti allows the creation of Multimethods. Unlike Generic Functions/Single Methods, Multimethods can be defined for the types of all of their arguments, not just the first one. They are also not limited to fugue Prototypes, being definable over any Janet type or a fugue Prototype. Also unlike Single Methods, they are not inheritable; a multimethod defined for an ancestor Prototype will not be selected for any descendent prototype instances.

#fugue

Root, Root*?, Root?, allocate, declare-open-multi, defgeneric, defmethod, defmulti, defproto, extend-multi, fields, get-type-or-proto, multimethod-types-match?, prototype?

#Root

table | source

@{:_init <function identity> :_meta @{ :prototype-allocations @{} :object-type :prototype :fields () :instance-defaults @{}} :_name "Prototype" :new <function new-from-Root>}

Root of the Fugue object hierarchy.

#Root*?

function | source

(Root*? obj)

Proto ancestor predicate: return if obj is a descendent of Root.

#Root?

function | source

(Root? obj)

Proto instance predicate: return if obj is an instance (that is, a direct child) of Root.

#allocate

function | source

(allocate obj key value)

Allocation-aware put. If obj has inherited an allocation to a specific prototype for this key, then fugue/allocate will put value at key in the appropriate prototype, and it will be inherited by all descendents of that prototype.

#declare-open-multi

macro | source

(declare-open-multi name)

Declare an open multimethod, ie, one that can be extended.

Extending an open multimethod (see extend-multi) from any other environment makes the case extension available wherever the multimethod has been imported.

#defgeneric

macro | source

(defgeneric name args &opt body)

Define a generic function. When this function is called, if the first argument has a method corresponding to the name of the function, call that object 's method with the arguments. Otherwise, evaluate body.

#defmethod

macro | source

(defmethod name proto args & body)

Simple single-dispatch method definition. Roughly equivalent to put ing a function directly into a prototype.

Defines a few symbols for reference in the body of the method.

__parent - Bound to the parent of proto. __super - Bound to the method at name within __parent.

#defmulti

macro | source

(defmulti name multi-types args & body)

Define a multimethod based on all the arguments passed to the function.

Example usage :

> (defproto Foo ())
> (defmulti add [Foo] [f] (put f :value 1))
> (defmulti add [:number] [x] (+ x 1))
> (defmulti add [:string] [s] (string s "!"))
> (def a-foo (:new Foo))
> (add a-foo)
@Foo{:value 1 :_meta @{:object-type :instance}}
> (add 1)
2
> (add "s")
"s!"

defmulti takes a sequence of type-or-prototypes, and builds a function which will check its arguments against those types (as well as all other ones specified in other defmulti calls to the same function name), and execute the function body for the matching type signature.

In addition to type names or prototypes, you can use the symbol _ or keyword :_ as a wildcard that means "match any type". For instance,

repl:2:> (defmulti cat [:string :_] [s1 s2] (string s1 s2))
repl:3:> (cat "hello " "world!")
"hello world!"
repl:4:> (cat "hello " 42)
"hello 42"
repl:5:> (cat 42 "hello")
error: could not apply multimethod <function cat> to args (42 "hello")
  in cat [repl] on line 2, column 1
  in _thunk [repl] (tailcall) on line 5, column 1

Defining a multimethod with the signature [:string :_] will match on any two arguments if the first one is a string.

A multimethod without wilcards will be preferred to one with one in the same position. For instance, if we define an additional multimethod:

repl:8:> (defmulti cat [:string :number] [s n] (string s " #" n))

Then that more specific method will be preferred and the wildcard will be a fallback if the specific one doesn't match:

repl:10:> (cat "hello " @"world")
"hello world"
repl:12:> (cat "hello" 100)
"hello #100"

#defproto

macro | source

(defproto name parent-name & fields)

Object prototype definition.

#Usage

name should be any symbol. The resulting prototype will be named after it.

parent-name is required; it can be an existing prototype, or some null-ish value. If null-ish (nil or () should make the most sense...) the parent of the prototype will be set to fugue/Root.

fields should be 0 or more pairs of the following format:

<field-name> <field-attributes>

Where field-attributes is a struct describing the field. The following attributes are currently recognized:

  • :default: provide a default value for all new instances of this prototype
  • :init?: if truthy, then this field will be a required parameter to the prototype 's constructor
  • :allocation: if :prototype, then fugue/allocate will always act on the prototype when putting this field.
  • :allocate-value: this field will have this attribute set at the prototype, so that any children without their own values will inherit it.
  • :getter: specify a name for the defined function to access this field (by default, has the same name as the field). Specify false to prevent a getter from being defined.

defproto will define a getter function for each of the defined fields, unless :getter is false. It will also define a "qualified" getter function, where the getter-name is prepended by the name of the prototype. This can be used to provide a greater degree of "type-safety", as the qualified getter won 't be defined if that prototype doesn 't have that field.

defproto will also create a :new method in the created prototype. This will take as positional arguments all of the fields specified as init?, and then accept in &keys format any other attributes to set on this object.

The special method :_init will be called as the last step in the :new constructor. It can be defined for a prototype (see defmethod) to take a new instance and to make any arbitrary mutations on the instance or prototype as part of object instantiation. By default it simply returns the instance.

The value provided to a field's :default entry will be inserted directly to the instance. Thus, mutable/referenced terms like tables and arrays will be shared amongst all instances. In cases where you want to insert a new term for each new instance, use the _init method to put a value at that field.

If fields is not of an even length, it wil be taken as an error.


An example usage:

repl:43:> (fugue/defproto Dog () name {:allocate-value "Fido"})
repl:44:> (fugue/defproto Pekingese Dog size {:default "Extremely Small"})
repl:45:> (fugue/defmethod speak Dog [self] (string "My name is " (self :name)))
repl:46:> (fugue/defmethod speak Pekingese [self] (string (prototype-method self) " and I am " (self :size)))
repl:47:> (speak (:new Pekingese))
"My name is Fido and I am Extremely Small"

#extend-multi

macro | source

(extend-multi multi multi-types args & body)

Extend an open multimethod (see declare-open-multi) using the same syntax as defmulti.

See that function's documentation for full usage reference.

Whenever a case is added to multi, that case is available wherever the multimethod is imported.

#fields

function | source

(fields obj)

Return all the defined fields for obj and its prototype hierarchy.

#get-type-or-proto

function | source

(get-type-or-proto obj)

Return the prototype of obj, if it has one, otherwise the keyword output of type.

#multimethod-types-match?

function | source

(multimethod-types-match? args arg-types)

Check to see if the types args match the sequence arg-types, according to multimethod rules (ie, following prototype membership and using :_ as a fallback)

#prototype?

function | source

(prototype? obj)

Is obj the result of a defproto call?