Skip to content

Instantly share code, notes, and snippets.

@joshuabowers
Created January 25, 2024 23:40
Show Gist options
  • Save joshuabowers/4222589c31e86cbdfd62a467f2022479 to your computer and use it in GitHub Desktop.
Save joshuabowers/4222589c31e86cbdfd62a467f2022479 to your computer and use it in GitHub Desktop.
typefn: 015 - Multimethods

Previously, pattern matching was discussed as a mechanism by which data could be interrogated to ascertain whether it adhered to particular constraints, in order to conditionally branch to alternative behaviors. Partially missing from this discussion is one key aspect of pattern matching: deep (tree-/structure-focused) comparisons, which allow the code to ask questions about quite complex data. This allows branches to be quite specific—and confident—about the data they are handling.

As an alternative to (and in some languages, in addition to) pattern matching, consider behavior contextually more bound to function definition: dispatch. It is possible, in many languages, to polymorphically overload a given function name to respond to different types—or more contextually specific constraints—of data, and have the system automatically forward the function invocation to the appropriate overload when given supported data. Via either binding dispatch to specific ownership classes (such as in OOP), or by static, compile time linkages, the system intuits which function overload handles a given request.

However, it is also possible to determine this sort of behavior off of more information, contextually, dynamically, at run-time, and often off of larger sets of parameters. Such systems are said to have dynamic multiple dispatch, or multimethods. In languages which provide this sort of support, the overload which receives a given invocation of a function call is determined at run-time, rather than at compile-time, and usually requires some degree of contextual analysis—such as run-time type information—about the data being passed as arguments to the function call.

Such systems can be very flexible and versatile when needing to select specific branches to conditionally execute when given multiple inputs to consider. They also loan themselves well to certain types of programmatic modularity, such as passing new functionality as callbacks to a HOF. In general, multimethods will attempt to match the first implementation which has conditional constraints which satisfy the data passed to the function, which can allow for both a default fallback case, as well as a series of specific overrides for known exceptions.

JavaScript and TypeScript do not, as of yet, have language-level support for multimethods. However, various libraries do have implementations of this functionality. This series will focus on @arrows/multimethod, but such functionality likely exists in other libraries.

This library has a lot of interesting functionality, some of which will be touched upon over the next article. Reading through its documentation is suggested to get a full feel for its utility. However, to whet the appetite, consider the pattern matching example from the previous article: the exact same behavior is presented—absent typing—in this article's attached source.

The next article will cover some of the highlights of @arrows/multimethod, and touch upon making it more type-safe to use.

import {
multi, method
} from '@arrows/multimethod'
// @arrows/multimethod creates multimethods via a series of HOFs:
// `multi` creates the overall closure function which can be invoked to
// perform the underlying behavior.
// `method`, meanwhile, defines individual branches which can be dispatched
// to. A given method has a condition and a corresponding behavior. The behavior
// is only invoked if the condition passes.
// The first `method` to have a passing condition will execute its corresponding
// behavior, which will be the result of the multimethod; all subsequent
// branches are ignored. If `method` only receives a single argument, as in
// the final use here in this example, it is treated as a default case that
// will always occur if control falls through to it. Thus, any methods occurring
// after this point are ignored. Should no default case exist and no other
// method be valid, the multimethod will generate an error.
// Note that conditions can be quite complicated: they can evaluate types,
// comparisons against specific data or regexps, execute predicate functions,
// or be an array of any of these, in which case each array entry corresponds to
// the associated argument within the multimethod's parameter list.
// In this example, a dispatch function is provided, which translates the
// inputs to the multimethod into a form that can be more easily tested by the
// `method` conditions.
// NB: None of this is typed. More accurately, everything here has TypeScript
// type `any`, so caution should be used in typed contexts. Typings will be
// discussed in this next article.
const flattenConcat = multi(
(value) => typeof value,
method('number', (value) => value.toString()),
method('string', (value) => value),
method((value) => value.map(flattenConcat).join(' '))
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment