Interface and extension rules
This page documents the public sorted-search API surface and the contract a custom SearchStrategy subtype must satisfy.
Public API surface
The sorted-search public API is the pair of FFF-owned generic functions dispatched on a strategy as the first positional argument:
searchsorted_first(strategy, v, x[, hint]; order = Base.Order.Forward)
searchsorted_last(strategy, v, x[, hint]; order = Base.Order.Forward)strategy is a StrategyKind enum value (KIND_BRACKET_GALLOP, …), a singleton strategy struct (BracketGallop(), … — forwards through strategy_kind and constant-folds for a literal argument), or a stateful strategy (Auto, GuesserHint).
The in-place batched variants:
searchsortedfirst!(idx_out, v, queries; strategy = Auto(), order = Base.Order.Forward)
searchsortedlast!(idx_out, v, queries; strategy = Auto(), order = Base.Order.Forward)FindFirstFunctions does not extend Base.searchsortedfirst / Base.searchsortedlast — all strategy dispatch happens through the FFF-owned names above, which compose with the same Base.Order orderings.
FindFirstFunctions.searchsortedfirst! — Function
searchsortedfirst!(idx_out, v, queries; strategy = Auto(), order = Base.Order.Forward,
queries_sorted = nothing)In-place batched Base.searchsortedfirst. See searchsortedlast! for behavior.
FindFirstFunctions.searchsortedlast! — Function
searchsortedlast!(idx_out, v, queries; strategy = Auto(), order = Base.Order.Forward,
queries_sorted = nothing)In-place batched Base.searchsortedlast. Writes one index per element of queries into idx_out (which must be the same length).
If queries is sorted under order, the previous result is used as a hint for the next query, so the total cost is O(length(v) + length(queries)) under strategy = BracketGallop().
If queries is not sorted, falls back to per-element searchsortedlast with no hint regardless of strategy.
The queries_sorted kwarg controls the runtime issorted(queries) check:
nothing(default): runissorted(queries; order = order)on every call.true: skip the check and take the sorted-loop path unconditionally.false: skip the check and take the unsorted-loop path unconditionally.
Returns idx_out.
FindFirstFunctions.searchsortedrange — Function
searchsortedrange(strategy, v, lo, hi[, hint]; order = Base.Order.Forward)
-> UnitRange{Int}Return the index range of all entries v[i] satisfying lo ≤ v[i] ≤ hi under order. Equivalent to searchsorted_first(strategy, v, lo[, hint]; order) : searchsorted_last(strategy, v, hi[, hint]; order).
When a hint is supplied it seeds the lo search directly; the hi search is seeded with max(first_idx, hint), since the upper endpoint can never precede the lower one. Strategies that ignore the hint treat the hinted form as a pass-through.
Rules of the interface
- Hint is optional and is an
Integer. When supplied it must be a valid index intov(firstindex(v) ≤ hint ≤ lastindex(v)). An out-of-range hint is silently treated as "no hint" by every built-in strategy. A strategy is allowed to ignore the hint entirely (InterpolationSearch,BinaryBracket). - Strategies are singletons or wrappers.
LinearScan,BracketGallop,ExpFromLeft,InterpolationSearch, andBinaryBracketare zero-field singletons mapped to theirStrategyKindtag bystrategy_kind.Autocarries a resolved kind plus aSearchPropertiespayload;GuesserHintis a thin wrapper around aGuesser. New strategies should follow the same pattern: parameters that change behaviour belong on the type; parameters that change cost only should be tuned internally. - No mutation of
vorx. A strategy never writes to the searched vector or to the query. The only state that may change across calls is hint state carried by the strategy itself (e.g.GuesserHintupdatesguesser.idx_prev). - Order is honored. Every strategy accepts an
order::Base.Order.Orderingkeyword and returns indices consistent withBase.searchsortedfirst/Base.searchsortedlastunder that ordering. Strategies that are only efficient underForwardordering (e.g.InterpolationSearch,ExpFromLeft) fall back toBinaryBracketfor non-Forwardorderings. AbstractRangeis already O(1).Base.searchsortedfirst(r::AbstractRange, x)has a closed-form implementation. Strategies do not need range fast paths — theBinaryBracketfallback already calls into Base's range-aware method.
Anatomy of a strategy
A built-in singleton strategy consists of a StrategyKind enum value, a pair of kernel functions (_kernel_last_<name> / _kernel_first_<name> in src/kernels.jl), and branches in the four dispatch switches in src/kinds.jl (hinted/unhinted × last/first). The "no hint" branch of a hint-using strategy falls back to BinaryBracket; the hinted branch of a hint-ignoring strategy discards the hint.
A custom strategy defined outside the package cannot add an enum value (the enum is closed), so it provides its own searchsorted_last / searchsorted_first methods instead — these are more specific than the generic SearchStrategy fallback and take precedence:
# Required: the hinted form (the strategy's reason for existing).
FindFirstFunctions.searchsorted_last(::MyStrategy, v, x, hint::Integer; order) = ...
FindFirstFunctions.searchsorted_first(::MyStrategy, v, x, hint::Integer; order) = ...
# Required: the unhinted form. Most strategies just fall back to BinaryBracket.
FindFirstFunctions.searchsorted_last(::MyStrategy, v, x; order) =
FindFirstFunctions.searchsorted_last(FindFirstFunctions.KIND_BINARY_BRACKET, v, x; order)
FindFirstFunctions.searchsorted_first(::MyStrategy, v, x; order) =
FindFirstFunctions.searchsorted_first(FindFirstFunctions.KIND_BINARY_BRACKET, v, x; order)If your strategy ignores the hint, define just the unhinted form and have the hinted form delegate to it (see BinaryBracket and InterpolationSearch in the source).
How to add a new strategy
Two steps.
1. Define the strategy type
Pick a name that describes the algorithm, not the use case. Make it a subtype of SearchStrategy:
"""
MyStrategy <: FindFirstFunctions.SearchStrategy
One sentence on what it does. One sentence on when it wins. One sentence on
when it falls back.
"""
struct MyStrategy <: FindFirstFunctions.SearchStrategy endIf your strategy carries state (like GuesserHint), make it a parametric struct:
struct MyStrategy{S} <: FindFirstFunctions.SearchStrategy
state::S
end2. Implement the dispatch methods
At minimum, the hinted forms:
function FindFirstFunctions.searchsorted_last(
::MyStrategy, v::AbstractVector, x, hint::Integer;
order::Base.Order.Ordering = Base.Order.Forward
)
# Algorithm body. Must return the same index that
# `Base.searchsortedlast(v, x, order)` would.
...
end
function FindFirstFunctions.searchsorted_first(
::MyStrategy, v::AbstractVector, x, hint::Integer;
order::Base.Order.Ordering = Base.Order.Forward
)
...
endPlus unhinted forms (typically fallbacks):
FindFirstFunctions.searchsorted_last(s::MyStrategy, v::AbstractVector, x; order = Base.Order.Forward) =
FindFirstFunctions.searchsorted_last(FindFirstFunctions.KIND_BINARY_BRACKET, v, x; order = order)
FindFirstFunctions.searchsorted_first(s::MyStrategy, v::AbstractVector, x; order = Base.Order.Forward) =
FindFirstFunctions.searchsorted_first(FindFirstFunctions.KIND_BINARY_BRACKET, v, x; order = order)When a strategy contributes to the package itself, add a StrategyKind enum value, the kernel pair, the four dispatch-switch branches, and a strategy_kind method instead — see src/kinds.jl and src/strategy_kind.jl.
Correctness check
Every strategy must return the same answer as plain Base.searchsortedlast / Base.searchsortedfirst for every (v, x[, hint]) triple. Test with random inputs against Base:
using Test, Random
using FindFirstFunctions: searchsorted_last, searchsorted_first
Random.seed!(0)
for trial in 1:10_000
v = sort!(randn(rand(1:1000)))
x = randn()
hint = rand(1:length(v))
@test searchsorted_last(MyStrategy(), v, x, hint) == searchsortedlast(v, x)
@test searchsorted_first(MyStrategy(), v, x, hint) == searchsortedfirst(v, x)
endHooking into Auto
Auto's decision tree lives in _auto_resolve_kind (construction-time) and _auto_batched_kind (batched). It is not extensible from outside — new strategies do not register themselves with Auto automatically. If you believe Auto should pick your strategy in some regime, open an issue with benchmark numbers across the regime grid in Auto: heuristics and benchmarks.