Introduction
As we know, when we assign DownValues
to a symbol, Mathematica creates a dispatch table for "non-pattern" definitions (i.e. expressions that, when used as patterns, can match only single expression), and can match such definition in constant time (independent of number of definitions).
To match definitions containing "real patterns" (in the sense of expressions that "appear to be able to" match more than one expression) Mathematica tries them one by one, so in worst case Mathematica needs time linear in number of such definitions.
Main question
How can we decompose arbitrary pattern containing Alternatives
to collection of separate patterns that when used as DownValues
will give exactly the same outputs (and possible side effects) as single pattern, but, if possible, will take advantage of dispatching mechanism for "non-pattern" expressions.
Context
I'm creating a user modifiable predicate: somethingQ
. Users will be able to declare arbitrary patterns for which predicate will give True
. Expected number of such patterns can grow to the order of $10^6$, but most of them are expected to be "non-pattern" expressions.
In the context of somethingQ
it will be "natural" to declare patterns similar to f[g[a | b | c | _h], x]
. I could try to document that giving separate patterns: f[g[a], x]
, f[g[b], x]
, f[g[c], x]
, f[g[_h], x]
will give somethingQ
better performance, but explaining in detail when to decompose pattern and when not to (e.g f[g[a | b], _h]
should non be decomposed) is difficult and decomposing patterns by hand can be tedious. Instead of burdening users with understanding DownValues
matching and manual decomposition, I'd like to automate this process.
Benchmarks
When we have $10^6$ "non-pattern" expressions "hidden" inside Alternatives
we can get noticeable performance gain by assigning "non-pattern" expressions to separate DownValues
and gathering "real patterns" to single down value with Alternatives
.
To test it let's create $10^6$ patterns with Alternatives
, first $9 \times 10^5$ patterns contains only Alternatives
of non-pattern expressions rest contains also "real patterns":
ClearAll[a, b, c, g, h, x]
patts = Join[
{a | Unique[]}, Table[Unique[] | Unique[], 9 10^5],
{b | _g}, Table[Unique[] | Blank@Unique[], 10^5], {c | _h}
];
Assign them to DownValues
of somethingQ
:
somethingQ // ClearAll
DownValues@somethingQ = HoldPattern@somethingQ@# :> True & /@ patts;
somethingQ@_ = False;
and test time of calling somethingQ
for arguments matching first and last non-pattern and "real pattern" giving True
and pattern giving False
:
somethingQ@a // RepeatedTiming (* {2.8*10^-7, True} *)
somethingQ@c // RepeatedTiming (* {0.14, True} *)
somethingQ@g[] // RepeatedTiming (* {0.13, True} *)
somethingQ@h[] // RepeatedTiming (* {0.14, True} *)
somethingQ@x // RepeatedTiming (* {0.14, False} *)
Matching expressions from first down value is fast, but matching last down values is slow since Mathematica needs to test all down values one by one.
Putting all patters in one huge Alternatives
makes matching last down values three times faster:
somethingQ // ClearAll
somethingQ[Join @@ patts] = True;
somethingQ@_ = False;
somethingQ@a // RepeatedTiming (* {3.*10^-7, True} *)
somethingQ@c // RepeatedTiming (* {0.042, True} *)
somethingQ@g[] // RepeatedTiming (* {0.037, True} *)
somethingQ@h[] // RepeatedTiming (* {0.042, True} *)
somethingQ@x // RepeatedTiming (* {0.042, False} *)
Extracting non-patterns and putting them in separate down values while putting all "real patterns" in one Alternatives
gives constant time for all expressions matched by "non-pattern" down values. Worst case time for matching "real patterns" is linear in number of only "real patterns", not in number of all patterns as previously:
somethingQ // ClearAll
DownValues@somethingQ = HoldPattern@somethingQ@# :> True & /@
Append[#@Symbol, Alternatives @@ #@Blank]& @
GroupBy[List @@ Join @@ patts, Head];
somethingQ@_ = False;
somethingQ@a // RepeatedTiming (* {2.*10^-7, True} *)
somethingQ@c // RepeatedTiming (* {1.8*10^-7, True} *)
somethingQ@g[] // RepeatedTiming (* {4.*10^-7, True} *)
somethingQ@h[] // RepeatedTiming (* {0.0055, True} *)
somethingQ@x // RepeatedTiming (* {0.0055, False} *)
Of course here extraction of "non-pattern" expressions was trivial since all patterns had known simple structure, but I'm looking for a general way of extracting them from arbitrary patterns.
Alternatives
are tried in order. Converting these DownValues changes the evaluation itself, not only the speed of execution. I worry that automatic conversion could make things behave unexpectedly. IssomethingQ
written in such a way that this is extremely unlikely to happen? $\endgroup$Condition
orPatternTest
. I believe this is taken into account in my answer. $\endgroup$_Pattern
beside_Condition
and_PatternTest
as pattern with possible side effects, should take care of potential problems with pattern variable binding. $\endgroup$