💾 Archived View for vierkantor.com › xukut › manual › swail › method.gmi captured on 2024-07-08 at 23:27:17. Gemini links have been rewritten to link to archived content

View Raw

More Information

⬅️ Previous capture (2024-05-10)

-=-=-=-=-=-=-

XukutOS manual → Swail → Methods

A method is a special kind of function, that combines with other methods to provide the ability to specialise operations based on the passed arguments. This combination of methods is called a "generic function". By choosing what to do based on the passed objects, methods form one of the building blocks of object-oriented programming. There are various differences between methods in Swail and those in a class- or message-based language (such as Java or Smalltalk, repsectively): all arguments together specify the method, not just one (the one called `this' or `self'), and specialisations are based on general predicates rather than an `instanceof' check. So remember that methods do not belong to a particular class!

Along with the method definition facility described below, Swail includes a metaobject protocol, allowing you to tweak the implementation to suit your needs. For example, you can define your own specialisers that choose an applicable method.

Many built-in definitions in Swail are defined as methods, providing the flexibility needed to support your own definitions. The built-in methods that do not directly relate to the method system (i.e. those that are not part of the metaobject protocol) can be found in their relevant parts of the manual.

Generic functions

A method is a kind of function that specifies (using specialisers) what kind of arguments it accepts. A generic function collects together a number of methods, and the methods it consists of are called its specialisations. When a generic function gets called it chooses the applicable specialisation for the specific arguments. In general, you define methods and call generic functions.

Using methods

To call a generic function, you can call it the same way as a boring old function. The generic function will arrange for the correct specialisation to be called:

  (= 1 2)         (* selects the built-in function `int:=' *)
  (= "foo" "bar") (* selects the built-in function `str:=' *)
  (= 'a 'b)       (* selects the built-in function `is-same' *)
  (= 1 2.0)       (* converts `1' to `1.0', then selects the built-in function `float:=' *)

Individual methods can also be called by themselves, if you can get hold of the method object (rather than the generic function that contains the method). The result is equivalent to calling a generic function containing only that specific method.

Specialisers: how a method is chosen

When a generic function is called, it uses the specialisers of the specialisations to determine which one will be executed. First the specialisations are filtered to remove the ones that don't match the arguments, then they are sorted in order of specificity. The most specific specialisation will be called.

A form that specifies a specialiser is written in a slightly magical dialect, providing a bit of syntactic sugar. This means that `(= 1)' can specify a specialiser while evaluating the form `(= 1)' normally would cause an error due to insufficient parameters. The rules for evaluating a specialiser specification are: if the specification `form' is a list, we evaluate `((specialiser:spec ,(head form)) ,@(tail form))', otherwise we evaluate `(specialiser:spec form)'. By default, `specialiser:spec' simply returns its arguments, but you can override this to give special meanings to operators like `=' that they normally wouldn't have.

Here's a lies-to-small-children explanation of the selection process of specialisers. The actual implementation is noticeably more complicated, especially since half of the described operations use generic functions.

To determine whether a specialisation remains after filtering, the arguments to the call are matched up to the specialisation's parameters. Let's assume there are exactly the same amount of arguments as parameters. Each parameter's specialiser is called as a predicate on the argument (using the generic function(!) `specialiser:match'). If the predicate returns false, the specialisation is removed from the list of options.

After filtering, we get a list of applicable methods. To determine which one of these is called, they are sorted by specificity and the most specific method is called. The sort is by lexicographic order on the parameter lists, where entries in the parameter lists are compared by calling (the generic function!) `specialiser:compare' on the respective specialiser. In other words, the most specific method is the one whose first paramerer is strictly more specific than all other methods' first parameter. If there is a tie for the first parameter, the method whose second parameter is strictly more specific wins, etc. If all parameters tie, an error is raised.

Finally, when the method has been selected, the specialiser gets a chance to massage the argument into the expected format. The actual argument passed to the method is the result of `(specialiser:massage spec arg)' (where `arg' is an argument to the generic function, and `spec' is the corresponding specialiser). The default is, of course, that `massage' simply returns `arg'.

This does not explain the usage of `default', `rest', `kwarg', etc though. In fact, each specialiser is matched with the *remaining list* of arguments in a call to (the generic function!) `specialiser:get'. More specifically, the call is to `(specialiser:get spec name args)'. If the return value is `ff', the match fails, otherwise it should return a cons, containing the massaged argument, and the list of arguments to be passed to the next parameter. The default method for `specialiser:get' returns `ff' on `nil', otherwise it calls `specialiser:match' on the head of the list, returns `ff' on failure and `(cons (massage spec (head args)) (tail args))'. The specialisation remains in the list if no `specialiser:get' returned `ff' and the final call returned an empty remaining argument list.

In fact, the above explanation is also a simplification. The times when each `specialiser:' method is called may change unpredictably: calls can happen right after the method is declared and before it is invoked. Don't expect consistent results if you make the methods impure, or modify them after using them in a method.

Swail offers the following built-in specialisers, in order of increasing specificness. See the section "Defining specialisers" for information on how to add your own specialiser.

swail:rest

Matches all arguments from here until the end of the argument list, as a list. (If there are no arguments to match, the result is `nil'.) Subsequent parameters will encounter an empty argument list.

For example, the below call to `with-rest' behaves the same as the call to `without-rest':

(def-method with-rest (x (y int) (z rest)) (list x y z))
(def-method without-rest (x (y int) (z list)) (list x y z))

(with-rest    'hi 5 "random" 'wacky (++ "arg" "s"))
(without-rest 'hi 5 (list "random" 'wacky (++ "arg" "s")))

swail:tt

This specialiser means all arguments are accepted. This is the least specific specialiser for a single argument.

A parameter of the form `(name tt)' can also be written as just `name' itself.

(swail:optional default)

Matches all objects. If there is no argument to match with this parameter, uses `default' instead.

A parameter of the form `(name (optional nil))' can be abbreviated to `(name optional)'.

(swail:kwarg keyword (default optional))

Treats the remaining arguments as a p-list, and removes the p-list entry corresponding to `keyword'. Uses the entry's value as an argument; the remaining arguments are the p-list with the entry removed. If there is no entry corresponding to `keyword', and a `default' is given, that is used instead. Otherwise, the match fails.

A parameter of the form `(name (kwarg 'name))' can be abbreviated to `(name kwarg)'.

For example:

(def-method kwarg-example ((x (kwarg 'foo)) (y (kwarg 'bar 0)) (z kwarg))
  (list x y z))

(kwarg-example 'foo 1 'bar 2 'z 3) (* = '(1 2 3) *)
(kwarg-example 'z 1 'foo 2 'bar 3) (* = '(2 3 1) *)
(kwarg-example 'z 1 'foo 2) (* = '(2 0 1) *)
(kwarg-example 1 2 3) (* error: there is no method for these arguments *)

(swail:coe ty)

This specialiser coerces its argument to have type `ty', and matches the ones where coercion is successful. In other words, it matches all objects that can be coerced to have type `ty', including those whose type is a subclass of `ty' (including `ty' itself), and massages the argument by calling `(coe ty arg)'.

See the manual page on coercions for more information about `coe'.

(swail:subclass ty)

This specialiser matches all objects whose type is a subclass of `ty' (including `ty' itself).

Each class defines its own specialiser, so instead of writing a parameter of the form `(name (subclass ty))', you can simply write `(name ty)'.

(swail:isa ty)

This specialiser matches all objects whose type is identical to `ty'. Subclasses and coercions do not count.

swail:true

This specialiser matches true values: all objects that result in `tt' when coerced to `bool'. Note the difference between `(swail:true param)' (only those values of `param' that are true) and `(swail:tt param)' (all values of `param', also the false ones).

swail:false

This specialiser matches false values: all objects that result in `ff' when coerced to `bool'. Note the difference between `(swail:false param)' (only those values of `param' that are false) and `(swail:ff param)' (no values of `param', not even `ff').

(swail:generic:extends? fn)

This specialiser matches all generic functions `fn2' such that `(generic:extends? fn fn2)' is true, i.e. those that are obtained by calling `add-method' zero or more times on `fn'.

swail:nil

This specialiser matches only the object `nil'.

(swail:= val)

This specialiser matches only values that compare as `='-equal to `val'.

(swail:is-same val)

This specialiser matches only arguments that are the same object as `val'.

swail:ff

This specialiser means no arguments are accepted. This is the most specific specialiser.

Defining methods

It is possible to create a method without collecting it in a generic function, but in practice doing so is almost always what you want. The most direct way to add a specialisation to a generic function is to use the `def-method' special form, which creates the generic function if needed. If want more control over the creation of the generic function, you can use `def-generic' followed by `add-method'. The preceding are all special forms and work by modifying the local environment. See `method:make', `generic:make' and `generic:add-method' if you want to do this functionally. When new methods are added to a generic function, the generic function object is not modified but a new one is created (modifying the original binding but not its value).

In order to define a method, you need its specialisers. See the relevant sections for a description of the specialisers available in Swail and how to create your own specialiser.

Apart from using specialisers to choose which specialisation method should be called, in a method you can also explicitly call the next method in the list of applicable specialisations using `method:next'.

special form: (swail:def-method name params . body)

The `def-method' special form is the most direct way to define a method. It creates a new method with the given parameters and body.

`params' is a list of parameter names with optional specialisers. Each element of `params' looks like `(name specialiser)', where `name' is a symbol used as the parameter name and `specialiser' specifies a specialiser for that parameter; see the section on specialisers for more details, noting that the computations associated with the this specification happen only once, when the method is being defined. An element of `params' which is just a single symbol will be treated as the name of that parameter, along with the specialiser `tt'.

If `name' is already a bound variable, `def-method' adds the newly created method to the generic function in `name' using `generic:add-method'. If `name' is not bound, `def-method' binds `name' to a new generic function with the method as specialisation.

special form: (swail:def-generic name (params optional))

`def-generic' creates a new generic function with and binds it to `name'. If `params' is passed, these are taken as the parameters to the generic function, to be used for documentation.

special form: (swail:add-method name params . body)

`add-method' looks up the generic function bound to `name' and adds a method with the given parameters and body to it using `generic:add-method'. If `name' is not bound to a function, an error is raised.

(swail:method:of-fn params fn)

Create a new method with given parameters and code. `fn' must be callable with arguments corresponding to `params'.

(swail:method:make params body)

Create a new method with given parameters and body. In contrast to the special forms `def-method' or `add-method', `method:make' is a regular old function and takes the body as a (quoted) list of forms rather than a subform.

(swail:generic:make (params optional))

Create a new generic function. If `params' is passed, these are taken as the parameters to the generic function, to be used for documentation.

(swail:is-generic? obj) -> bool

Is the object a generic function?

(swail:generic:add-method generic-fn method)

Add the method as a specialisation to the generic function. Returns a new generic function (the value in `generic-fn' is unmodified).

When you try to add a method to a non-generic function, it is first converted to a generic function, with one method (with param list `((args rest))') that calls the original function.

((dyn swail:method:next) . args)

Call the next less specific method for the generic function.

Calling `method:next' has analogous effects to calling a copy of the generic function with all methods up to and including the current one removed from the (sorted) list of specialisations. The passed arguments are matched and massaged. If the most general method calls `method:next', an error is raised since no (more) methods apply to the arguments.

(swail:generic:extends? parent child) -> bool

Is `child' a generic function that is obtained from calling `add-method' on `parent' zero or more times? Notice the difference between `=' and `extends?': the return value of `add-method' does not `=' the first argument, but it does `extends?'. `extends?' is the correct specialiser for matching a certain generic function.

Defining specialisers

Specialisers are defined through instantiating the appropriate methods, so that together they implement the specialiser interface. Since these specialisers are being defined using methods, and methods need specialisers, you should not rely on circular definitions working well, or the ability to change built-in specialiser semantics. In particular, do not use a specialiser to implement itself in `specialiser:compare', `specialiser:get', `specialiser:match' or `specialiser:massage'. If you try to redefine the usage of a built-in specifier in `specialiser:get', `specialiser:match' or `specialiser:massage', these modifications may or may not actually be used.

To add a new (group of) specialisers to the existing repertoire, you need to implement `specialiser:compare' and (either `specialiser:get' or (`specialiser:match' and optionally `specialiser:massage')) on the set of objects that will serve as the specialiser.

Let's take the following example: for whatever reason, you want two specialisers: one that matches all arguments, but only on Tuesdays, and one that matches all integers divisible by a given number and uses the quotient as argument. Let's call the first specialiser `on-tuesday', and define it as a literal symbol:

(set on-tuesday 'on-tuesday)

For the second specialiser, we need to store a piece of data (namely the number that the argument should be divisible by), so let's define a new class to do so. This allows us to use the class as a specialiser in the implementation of our new specialiser (make sure that you understand why this is not circular!). Here's a stripped-down definition:

(def-class divisible (by int))

For `on-tuesday' we'll just pass-through the arguments that match, so we will implement `compare' and `match'. For `divisible' we additionally need `massage', to perform the division. Implementing `match' is straightforward in both cases:

(add-method specialiser:match ((spec (= on-tuesday)) arg)
  (= (time-date:weekday time-date:now) time-date:tuesday))
(add-method specialiser:match ((spec divisible) arg)
  (divides? (divisible:by spec) arg))

Similarly, `massage' does not require too much thought:

(add-method specialiser:massage ((spec divisible) arg)
  (/ arg (divisible:by spec)))

For `compare' the implementation is less obvious, since the specialisers could potentially be compared with arbitrarily many new kinds of specialisers introduced elsewhere. The solution is to not care about those until you need to: add methods for everything that you know is less specific and more specific, and fail for the rest. The easiest way to is to accept anything for the second argument and to call the next method on failure. So we get:

(add-method specialiser:compare ((spec1 (= on-tuesday)) spec2)
  (let (cmp-tt (specialiser:compare tt spec2)
        cmp-opt (specialiser:compare optional spec2))
    (cond
      (= cmp:gt cmp-tt) cmp:gt
      (= cmp:eq cmp-tt) cmp:gt
      (= cmp:eq cmp-opt) cmp:lt
      (= cmp:lt cmp-opt) cmp:lt
      otherwise ((dyn method:next) spec1 spec2))))

(Notice how `cmp:gt' is used to mean "less specific than", i.e. the set of elements that match is larger.)

This handles everything except when the second specialiser is also `on-tuesday'. We can re-use the previous definition to do handle this nicely:

(add-method specialiser:compare ((spec1 (= on-tuesday)) (spec2 (= on-tuesday)))
  cmp:eq)
(add-method specialiser:compare (spec1 (spec2 (= on-tuesday)))
  (cmp:op (specialiser:compare spec2 spec1)))

Since `compare' for `divisible' would need to do exactly the same thing, let's use a couple of built-in definitions to reduce duplication.

(add-method specialiser:compare ((spec1 divides) (spec2 divides))
  cmp:eq)
(add-method specialiser:compare ((spec1 divides) spec2)
  ((order:extend-between (dyn method:next) true (specialiser:isa int)) spec1 spec2))
(add-method specialiser:compare (spec1 (spec2 divides))
  (cmp:op (specialiser:compare spec2 spec1)))

Finally, if you would like to make existing forms useful as specialisers, take a look at `specialiser:spec'.

generic function (specialiser:compare spec1 spec2)

Determine whether specialiser 1 or specialiser 2 is more specific.

Returns a comparison result. `cmp:lt' indicates specialiser 1 is more specific (it matches less arguments), `cmp:eq' indicates specialiser 1 and specialiser 2 match exactly the same arguments, and `cmp:gt' indicates specialiser 1 matches more arguments than specialiser 2.

The fallback method returns `cmp:ne', stating that the specialisers are incomparable; incomparable specialisers cannot be used at the same parameter position in the same generic function.

Methods extending this function should ensure any set of comparable specialisers is a well-order.

`specialiser:compare' should only be modified (through adding methods or any other change) as follows: any two specialisers that used to be comparable should remain comparable after the modification, and the result of calling `specialiser:compare' should remain the same. Any modifications breaking this rule may result in the modification not being applied, or being applied inconsistently.

generic function (specialiser:get spec (arglist list))

Match a specialiser with many arguments. Returns `ff' if the specialiser does not match part of the arglist, or `(massaged . new-arglist)' if it does. `massaged' is the value to be bound to the parameter name and `new-arglist' is the list of arguments to be used for the next parameter.

The fallback method returns `ff' on `nil', otherwise it calls `specialiser:match' on the head of the arglist. On failure, it returns `ff' and `(cons (massage spec (head args)) (tail args))' on success.

generic function (specialiser:match spec arg)

Match a specialiser with a single argument. Returns a `bool' indicating whether the argument matches the specialiser.

The fallback method raises an error.

generic function (specialiser:massage spec arg)

Massage an argument before it is bound to the parameter name. `massage' is called after the specialiser matched the argument, and it returns the value that is bound to the parameter name when executing a method.

The fallback method returns the original `arg'.

generic function (specialiser:spec form)

Convert `form' to a specialiser, or an operator that returns specialisers.

This function is used to convert specifications like `(= 1)' to specialisers without causing errors due to insufficient parameters. The rules for evaluating a specialiser specification are: if the specification `form' is a list, we evaluate `((specialiser:spec ,(head form)) ,@(tail form))', otherwise we evaluate `(specialiser:spec form)'.

In particular, the head of the form is evaluated before it gets passed to `spec': when dealing with a specification like `(= 1)' the function that is bound to `=' gets passed to `spec', not the symbol `='.

The fallback method returns the original `form'.

Behind the scenes

This is an explanation of low-level design in XukutOS. We hope that the design works well enough that you can stick to higher-level stuff for day to day activities.

This section explains some of the implementation details, especially how the circularity in the usage of specialisers is resolved.

Methods are stored essentially the same as `fn's: a list of parameters (including specialisers), their lexical context and their code. Generic functions are stored with the list of their specialisations already sorted by specificity. Sorting the methods when storing the generic function (and pre-determining the sorted list for built-in generic functions) takes care of the dependency of method calling on `specialiser:compare'.

The other circularity goes via `specialiser:get', and we solve it by giving `specialiser:get' its own call implementation that handles (a subset of) specialisers by itself, and dispatches to the generic function call implementation for the rest.

Cross-thread contamination of methods is avoided as follows: when adding a new method to a generic function, we create a new generic function object. The macros update the bindings. There should probably be some form of compiler hook to update compiled code inlining generic functions.

Finally, when bootstrapping a Swail image from scratch, we define a small set of methods and generic functions in Assembly, manually ensuring the specificity order is correct, then build the full complement of methods out of those.

Any questions? Contact me:

By email at vierkantor@vierkantor.com

Through mastodon @vierkantor@mastodon.vierkantor.com

XukutOS user's manual: Swail

XukutOS user's manual

XukutOS main page