An extensible code selection interface.

The Selectors module provides an extensible way to write code that has to dispatch on different predicates without hardcoding the control flow into a single chain of if statements.

In the following example a selector for a simple condition is implemented and the generated selector code is described:

abstract type MySelector <: Selectors.AbstractSelector end

# The different cases we want to test.
abstract type One    <: MySelector end
abstract type NotOne <: MySelector end

# The order in which to test the cases.
Selectors.order(::Type{One})    = 0.0
Selectors.order(::Type{NotOne}) = 1.0

# The predicate to test against.
Selectors.matcher(::Type{One}, x)    = x === 1
Selectors.matcher(::Type{NotOne}, x) = x !== 1

# What to do when a test is successful.
Selectors.runner(::Type{One}, x)    = println("found one")
Selectors.runner(::Type{NotOne}, x) = println("not found")

# Test our selector with some numbers.
for i in 0:5
    Selectors.dispatch(MySelector, i)

Selectors.dispatch(Selector, i) will behave equivalent to the following:

function dispatch(::Type{MySelector}, i::Int)
    if matcher(One, i)
        runner(One, i)
    elseif matcher(NotOne, i)
        runner(NotOne, i)

and further to

function dispatch(::Type{MySelector}, i::Int)
    if i === 1
        println("found one")
    elseif i !== 1
        println("not found")

The module provides the following interface for creating selectors:


Root selector type. Each user-defined selector must subtype from this, i.e.

abstract type MySelector <: Selectors.AbstractSelector end

abstract type First  <: MySelector end
abstract type Second <: MySelector end

Call Selectors.runner(T, args...) where T is a subtype of MySelector for which matcher(T, args...) is true.

Selectors.dispatch(MySelector, args...)

Return a list of all subtypes of T which do not have further subtypes.

The returned list includes subtypes of subtypes, and it does not distinguish between concrete types (i.e. types which are guaranteed not to have subtypes) and abstract types (which may or may not have subtypes).


Define the matching test for each case in a selector, i.e.

Selectors.matcher(::Type{First}, x)  = x == 1
Selectors.matcher(::Type{Second}, x) = true

Note that the return type must be Bool.

To match against multiple cases use the Selectors.strict function.


Define the precedence of each case in a selector, i.e.

Selectors.order(::Type{First})  = 1.0
Selectors.order(::Type{Second}) = 2.0

Note that the return type must be Float64. Defining multiple case types to have the same order will result in undefined behaviour.


Define the code that will run when a particular Selectors.matcher test returns true, i.e.

Selectors.runner(::Type{First}, x)  = println("`x` is equal to `1`.")
Selectors.runner(::Type{Second}, x) = println("`x` is not equal to `1`.")

Define whether a selector case will "fallthrough" or not when successfully matched against. By default matching is strict and does not fallthrough to subsequent selector cases.

# Adding a debugging selector case.
abstract type Debug <: MySelector end

# Insert prior to all other cases.
Selectors.order(::Type{Debug}) = 0.0

# Fallthrough to the next case on success.
Selectors.strict(::Type{Debug}) = false

# We always match, regardless of the value of `x`.
Selectors.matcher(::Type{Debug}, x) = true

# Print some debugging info.
Selectors.runner(::Type{Debug}, x) = @show x