Generic operator predicate dispatch semantics in Clojure
Lately, I have been working on a propagation library in Clojure as a Lisp in Summer Project. In doing so, I found the need for a generic operator predicate dispatch system that would fit into a general library, and I’ve therefore worked on some different generic operator implementation strategies.
I found that different semantics of generic operator strategies leads to different usage trade-offs. Performance is only one side of the story; core.match has efficient predicate dispatch as a final goal, but it isn’t there yet, and even if it was done, it wouldn’t nessecarialy fit the particular needs of this project.
In this post I will go through some different strategies for implementing generic operators based on predicate dispatch and their usage trade-offs. The aim is not to present efficient implementations, but to present different use-cases and possible solutions.
Problem description
In this particular case, I needed a predicate dispatch system that would allow me to perform operator overloading using predicates without tarnishing the runtime. By tarnishing the runtime, I mean that it should be possible to assign new operations to a generic operator and get a new generic operator, while keeping the old. These two reasons rule out the use of multimethods, as they do not support predicate dispatch, and they tarnish the runtime.
In an ideal scenario, it should be possible to do something like this:
How to get there is non-trivial. Here are two approaches that each get some of the way, but both fall short of the final solution.
Simple recursion
Using simple recursion to create new functions, much like wrappers in Ring, allows for different extensions of a generic operator without tarnishing the runtime, but recursion is not possible.
Function with predicates stored in meta-data
We can store the predicates in the meta-data for the generic operator instead. This will gain us the ability to recur, but we will no longer be able to use old versions of a generic operator. Every time we add an operator with predicates to our generic operator, this is what will be seen by the system. This somewhat resembles how multimethods work, but we can use general predicates.
Conclusion
This post described two types of semantics for generic operators using predicate dispatch in Clojure, but was unable to give an elegant solution to the original problem definition. The question is, does the original problem definition even describe an approach that is desirable to use as a generic operator system?
In my use case, I want to supply users with a particular generic operator, which they need to extend to support new datatypes or behaviours to the library. Given these constraints, the different options can be summarised as follows:
Multimethods - Well understod by users. Does not support general predicates. Tarnishes generic operator (but a new one can be generated using macros). Support recursion. Implemented in Clojure.
Simple recursion - Simple for users to understand. Supports predicate dispatch. Does not tarnish generic operator. Does not support recursion. Implemented and described here.
Function with predicates in meta-data - Simple for users to understand. Supports predicate dispatch. Tarnishes generic operator (but a new one can be returned by function-call). Supports recursion. Implemented and described here.
Solution from problem definition - Harder for users to understand. Supports predicate dispatch. Doe not tarnish generic operator. Supports recursion. Is not currently implemented.
Given these properties, I think I will go forward with the meta-data approach. If you need generic operators with predicate dispatch, I suggest you try out a couple of different strategies. If you come up with a nice solution for the original problem, do get in touch, thanks!