~subsetpark/fugue

CLOS for Janet, for some reason.
bugfix: janet/match
55567281 — Zach Smith 3 years ago
fix #14 - eval symbol when processing match
854d1d5c — Zach Smith 3 years ago
Minor cleanup

clone

read-only
https://git.sr.ht/~subsetpark/fugue
read/write
git@git.sr.ht:~subsetpark/fugue

You can also use your local clone with git send-email.

#fugue API

An object system for Janet, inspired by CLOS.


#background

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, match, multimethod-types-match?, new-Root, prototype?, with-slots, with-slots-as

#@

macro | source

(@ proto x &opt y)

Compile-time Prototype field checking.

Accepts two forms:

(@ SomePrototype :some-field) - Translates into :some-field, if some-field is defined on SomePrototype.

(@ SomePrototype some-object :some-field) - Asserts (as above) at compile time that some-field is defined on SomePrototype; at runtime, checks that some-object is a descendent of SomePrototype and if so, translates to (some-object :some-field).

#Root

table | source

@{:_init <function identity> :_meta @{ :fields () :getters @{} :instance-defaults @{} :object-type :prototype :prototype-allocations @{}} :_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 & rest)

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 nil)
> (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 multimethod specifications, 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.

A multimethod specification can be any of the following data types:

  • a keyword representing the name of a simple or abstract type; for instance, :number or :string. This will match against values of this type.

  • A symbol referring to an existing table. This will match against tables which have this table as a prototype.

  • The fallback symbols :_ and _. These will match against anything.

  • A match specification, ie, a pattern understood by the match macro, as long as it isn't one of the above values. This will match against any value that would be matched in an execution of match. This includes tuples with arbitrary predicates.

Multimethod specs match in order of most specific to least specific; that is:

  1. Match specifications
  2. Prototypes
  3. Simple types
  4. Fallback
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 & rest)

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-name is a field to define on the prototype and field-attributes is a struct describing the field. The following field 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.

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 of an odd length, the last element will be treated as a prototype attributes struct. There is currently one valid prototype attribute:

  • :constructor : Set the name of the defined function that calls :new. If false, no additional constructor will be defined. By default, will be set to new-<prototype name>.

An example usage:

repl:43:> (fugue/defproto Dog nil 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.

#match

macro | source

(match x & cases)

Prototype-aware version of match. Introduces one new case form:

  • (@ <prototype-name> <dictionary>): Will pattern match against an instance of prototype-name. Additionally, will validate at compile-time that every key in dictionary is a field that's present on the specified prototype.

#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)

#new-Root

function | source

(new-Root & rest)

Constructor for Root. Return a new object with Root as the prototype.

#prototype?

function | source

(prototype? obj)

Is obj the result of a defproto call?

#with-slots

macro | source

(with-slots proto obj & body)

Anaphoric macro with transformed getter/setters.

Introduces two useful forms for referring to obj.

It introduces a reference symbol - @ by default (see with-slots-as to specify the symbol).

The pattern (@ <field name>), where <field name> is a symbol, is transformed into (obj (keyword <field name>)), if and only if <field name> is defined for proto, so that (@ name) or its setter form (set (@ name) foo) do the right thing.

The reference symbol by itself is introduces as a reference to obj.

Returns obj.


Example :

repl:2:> (defproto Foo nil name {:default "Jane Doe"})
repl:4:> (with-slots Foo (new-Foo)
(set (@ name) "Cosmo Kramer")
(print (@ name))
(print (Foo? @)))
Cosmo Kramer
true
@Foo{:_meta @{:object-type :instance} :name "Cosmo Kramer"}

#with-slots-as

macro | source

(with-slots-as proto obj as & body)

Anaphoric macro with transformed getter/setters.

Specifies as as the reference symbol for with-slots.

See with-slots documentation for more details.