bugfix: janet/match
fix #14 - eval symbol when processing match
Minor cleanup
An object system for Janet, inspired by CLOS.
The Janet language provides a simple form of object orientation using tables and prototypes:
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.
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
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.
@, 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)
.
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.
function | source
(Root*? obj)
Proto ancestor predicate: return if obj
is a descendent of Root.
function | source
(Root? obj)
Proto instance predicate: return if obj
is an instance (that is, a direct child) of Root.
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.
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.
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
.
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
.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:
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"
macro | source
(defproto name parent-name & rest)
Object prototype definition.
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"
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.
function | source
(fields obj)
Return all the defined fields for obj
and its prototype
hierarchy.
function | source
(get-type-or-proto obj)
Return the prototype of obj
, if it has one, otherwise the keyword
output of type
.
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.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)
function | source
(new-Root & rest)
Constructor for Root. Return a new object with Root as the prototype.
function | source
(prototype? obj)
Is obj
the result of a defproto
call?
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"}
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.