In the function I propose, I build an association with keys that are not supposed to be evaluated. There are some issues with this, see this answer by Taliesin, with the following quote.
generally this just sounds like a dangerous and confusing game to play, to me.
I think the function presented in this answer deals with the complications you mention reasonably well. It uses an option to set the levelspec. To see how patterns are handled, see the section Verbatim.
Concerning point (1),(2) and (3): Before there were a lot of additional complications. But I now that we have Association
we no longer have to deal with those. Making this work with held expressions is just a matter of being thorough in surrounding expressions with Hold
and Unevaluated.
My intuition is also that Association
should have better performance than a Dispatch
table or something similar. An Association
should be unbeatable in terms of how long it takes to look up a particular sub-expression. But maybe we should do a proper comparison.
ClearAll[positionIndexGeneral]
Options[positionIndexGeneral] = {Heads -> True};
SetAttributes[positionIndexGeneral, HoldAll];
positionIndexGeneral[expr_, lev_: {1,Infinity}, OptionsPattern[]] :=
Module[{subExprs, positions, len, together, gathered, hGathered,
gatheredSubExprs, gatheredPos},
subExprs =
Level[Unevaluated@expr, lev, Hold, Heads -> OptionValue[Heads]];
positions =
Position[Unevaluated@expr, _, lev, Heads -> OptionValue[Heads]];
len = subExprs // Length;
together = Transpose[{List @@ Hold /@ subExprs, positions}];
gathered = GatherBy[together, First];
hGathered = Hold@Evaluate@gathered;
gatheredSubExprs = hGathered[[All, All, 1, 1, 1]];
gatheredPos = gathered[[All, All, 2]];
AssociationThread @@ {Unevaluated @@ gatheredSubExprs, gatheredPos}]
Example:
a = 3;
positionAssoc =
positionIndexGeneral[{a, 2, {3, 4, a}}]
positionAssoc[Unevaluated[a]]
{{1},{3,3}}
corresponding to
Position[Unevaluated@{a, 2, {3, 4, a}}, Unevaluated[a]]
{{1},{3,3}}
Verbatim
Note that in general we are simulating how Position
works with Verbatim
.
positionAssoc = positionIndexGeneral[{a, 2, {3, 4, a_}}]
positionAssoc[a_]
{{3,3}}
Corresponding to
Position[Unevaluated@{a, 2, {3, 4, a_}}, Verbatim[a_]]
{{3,3}}
To simulate how Position
works without Verbatim
in this way is probably not very useful. There are infinitely many patterns against which an expression can be tested, so of course we cannot make a big lookup table. For a very specific pattern like List | Hold
we might make some specialised code that looks up both List
and Hold
in the association.
Timing
My function can kind of compete with a specialised function by Mr.Wiz in the 1D case, and of course it dwarfs the built in PositionIndex
for large data.
f[x_] := AssociationThread @@ {Hold[
Unevaluated[x]][[1, {1}, #[[All, 1]]]], #} &@
GatherBy[Range@Length@x, Hold[x][[{1}, #]] &]
Now let's make some data and compare
data = RandomInteger[999, 1*^5];
(jacobGen =
positionIndexGeneral[Evaluate@data, {1, 1}, Heads -> False]) //
Timing // First
(mma1D = PositionIndex[data]) // Timing // First
(wiz1D = f[data]) // Timing // First
Position[data, 115] === jacobGen[115] === List /@ wiz1D[115] ===
List /@ mma1D[115]
0.214873
0.174309
0.164100
True
data = RandomInteger[10, 1*^5];
(jacobGen =
positionIndexGeneral[Evaluate@data, {1, 1}, Heads -> False]) //
Timing // First
(mma1D = PositionIndex[data]) // Timing // First
(wiz1D = f[data]) // Timing // First
0.235508
4.119624
0.153041
data = RandomInteger[10, 1*^6];
(jacobGen = positionIndexGeneral[data, {1, 1}, Heads -> False]) //
Timing // First
(wiz1D = f[data]) // Timing // First
2.294256
1.703060
Possible improvement
When we only want the expressions at level 1, the function provided by Mr.Wizard is faster. With some good metaprogramming it should be possible to get the best of both worlds.
Appendix
It would of course have been cooler to write something like
ClearAll[positionIndexGeneral]
Options[positionIndexGeneral] = {Heads -> True};
positionIndexGeneral[expr_, lev_: {1,Infinity}, OptionsPattern[]] :=
AssociationThread @@
{
Unevaluated @@ #2[[All, All, 1, 1, 1]]
,
Function[{x}, x[[#]] & /@ #[[All, All, 2]]]@
Position[expr, _, lev, Heads -> OptionValue[Heads]]
} &[#, Hold@Evaluate@#] &@
GatherBy[Transpose[{List @@ Hold /@ #, Range[# // Length]}] &@
Level[expr, lev, Hold, Heads -> OptionValue[Heads]], First]
but I prefer the style with Module
(/Block
when possible) for debugging, as well as to immediately see what happens first.
Position[]
-users... :-( $\endgroup$Position
after participating here. I think this site has warped my sensibilities. :D $\endgroup$a = RandomInteger[{1, 1000}, {5000}]; Table[{i, Position[a, i]}, {i, 1, 1000}] // Timing // First
when magnitudes faster would beSort[{#[[1, 1]], #[[All, 2]]} & /@ GatherBy[Transpose[{a, Range@Length@a}], First]] // Timing // First
$\endgroup$PositionFunction
, and that is thatNearest
andInterpolation
are "subject matter" functions whereasPosition
is more a language thing. E.g. hypothetically,Nearest
could be constructed from a variety of data structures because all that matters is the mathematical distance between one unit and another, whereasPosition
is entirely about the specific list structure you are using. Not saying I'd agree with this, but there is this conceptual difference which may have something to do with the absence of aPositionFunction
. $\endgroup$GatherBy
inversion trick, but I think we cannot efficiently use it in the general case. The only thing that is missing now is to use your idea when level 1 andHeads-> False
is specified, but I'd say that is a minor point. $\endgroup$