Intermingling Operator Forms & Linguistic Connections
This answer attempts to draw out linguistic connections in understanding why operator forms seem so useful, a process that can perhaps point to further utility. Operator Forms are a type of modularization with the standard re-use and combinatory advantages but IMO the biggest benefit is cognitive - it allows you to focus on what is relevant in defining/applying an operation. Natural language does this pervasively effectively modularizing by repeatedly sampling contexts.
Note this post was generated (and adapted) from a notebook as part of road-testing a developing markdown - hence its formatting may be less than optimal. It's also long.
V10.3.1 provides functionality for viewing all built-in operator forms (via so-called Curryable symbols) weighted here according to SE usage (bearing in mind that most of this predates operator form's introduction).
[See also other listings].
curryable =
WolframLanguageData[
EntityClass["WolframLanguageSymbol", "Curryable"]] //
Join[#, WolframLanguageData /@ {CellularAutomaton, TuringMachine,
SubstitutionSystem}] & // Sort
ImageCollage[{Rasterize[#, "Image", RasterSize -> 400,
ImageSize -> {500, Automatic}],
"StackExchange" /.
EntityValue[#,
EntityProperty["WolframLanguageSymbol", "Frequencies"]]} & /@
curryable]
Without weighting all operator forms can be listed.
ImageCollage[(Rasterize[#, "Image"] & /@ curryable), Automatic, 500,
Method -> "ClosestPacking"]
We can compare non-operator forms with their corresponding canonical form to observe a prevailing systemization and in particular, the types of arguments being picked off.
findFirstLast[
ent_] := (StringCases[
ent["Name"] ~~
"[" ~~ (st__ /; ! StringEndsQ["Association"]@st) ~~ "]" //
Shortest]@(ent[
EntityProperty["WolframLanguageSymbol", "PlaintextUsage"]]) //
Query[{First, Last}]);
findFirstLast[
Entity["WolframLanguageSymbol",
"ReplaceAll"]] := {"ReplaceAll[expr,rules]", "ReplaceAll[rules]"};
FirstLastUsageExpressions =
ToExpression[#, InputForm, HoldForm] & /@ findFirstLast /@ curryable;
makeGrid[pair_] :=
Grid[{ExpressionCell[#, "Input",
FontFamily -> "American Typewriter"]} & /@ pair,
Alignment -> Right];
ImageCollage[(Rasterize[#, "Image", RasterSize -> 700] & /@
makeGrid /@ FirstLastUsageExpressions), "Fit",
ImagePadding -> 4, Padding -> Orange,
Method -> "Columns"]
This collage illustrates a clear idiom at play; the argument being dropped from the original function invariably refers to a "fixed" structure that is subsequently operated on by the corresponding operator form. The fixed structures identified as future operands are collected in the collage below left whereas the below right collage collects all the remaining argument(s) used to define the operator form (as per the code that follows).
(* code for the collage/dataset immediately above *)
fixedCount =
FirstLastUsageExpressions[[All, 1, All,
1]] // DeleteCases[HoldForm@f] // Counts //
Sort // Reverse;
(* Remove f as in MapAt the first argument is not the fixed on \
jettisoned for the corresponding operator form *)
operatorArgs =
Replace[_[args__] -> Invisible[""][args]] @@@
FirstLastUsageExpressions[[All, 2,
All]] // Rest // Counts // Sort // Reverse;
rasterizeKeys =
KeyMap[Rasterize[
ExpressionCell[#, "Input", FontFamily -> "American Typewriter"],
"Image", RasterSize -> 200] &];
splitDataset[splitAt_] :=
Function[d,
Row[{d[[;; splitAt]],
d[[splitAt + 1 ;;
Length@d]]}]];
Grid[{
{GraphicsGrid[{
{ImageCollage[rasterizeKeys@fixedCount],
ImageCollage[rasterizeKeys@operatorArgs]}
}, ImageSize -> {900, Automatic}, Spacings -> {90, 0}]},
{
Grid[{{Spacer@90, Dataset[HoldForm /@ fixedCount], Spacer@120,
Dataset[HoldForm /@ operatorArgs] // splitDataset@12}},
Alignment -> {Center, Top}]}
}, Alignment -> {Center, Top}
]
The value of elucidating this dilineation is two-fold: 1) It highlights a network effect enhancable not only through system operator forms but also through an intermingling with user-defined operator forms 2) It points to Operator Form's connection to the central notion of Mutability/Immutability
Network Effect & User-defined Operator Forms
A network effect emerges from being able to combine operator forms both built-in and user-defined. It is straightforward to create user-defined operator forms although several SE questions suggest a possible ongoing under-utilization. Given how much it can improve a code-base's readabiity, a template is perhaps worth making explicit for new users:
Given a function head
with the fx
argument corresponding to a nominally "fixed" structure
head_[argsH___, fx_, argsT___] := code
(* code includes argsH, fx, argsT *)
An operator form ohead
can be defined as follows:
ohead_[argsH___, argsT___] := Function[fx, code]
(* code includes argsH, fx, argsT *)
with subsequent invocations taking the form:
head[argsH, argsT]@fx
Note:
A more direct implementation involving subvalues head[_argsH___,argsT__][fx_]:= code
although there are certain advantages for the more general idiom shown.
fx
, is typically the first argument called (i.e. argsH
typically matches the empty sequence) in system functions with the notable exceptions of MapAt
, Insert
and Entity
.
The operator form typically consists of a single argument (with exceptions at the same functions just mentioned).
Here the operator form ohead
is distinguished from the canonical form head
to ensure their disambiguation although built-in operator forms avoid this by limiting their functional scope.
For consistency I have found it worthwhile sticking to these conventions in creating my own user-defined operator forms although having said this, sometimes it can be useful to loosen such restrictions and to what extent this can/should be done is to be canvassed.
Immutability/Mutability
The importance of the Wolfram Language's immutability in rapid development and experimentation has been well-documented and in this context manifests through operator forms' ability to create new structures each time they are applied - thereby leaving untouched underlying definitions. When such underlying definitions do need to be changed/augmentation (say following a period of experimentation/prototyping) then all this operator form functionality can again be harnessed this time by an operator-form of Set
(most naturally via an infix operator akin to @=
, as previously advocated).
Finally, note some anomolies/variations in the 10.3.1 version: The operator forms for CellularAutomation
,TuringMachine
, and SubstitutionSystem
assume default parameters for their last arguments (the time step) while the operator form of AssociationThread
and DataBinAdd
are yet to appear in the Documentation Centre [also DataBinAdd
's implementation violates the just-described idiom]
Different Targets
The consistency involved in omitting a "fixed structure" in the operator form adds greatly to the idiom's pedagogy (which IMO makes the implementation of DatabinAdd
a mistake [unless I've missed a compelling use-case justifying such a violation]) but even "second best" operator forms can still be useful. Take MemberQ
as an example:
The systemic version translates the linguistic request:
Select[{{a, b, c}, a, z, {x, y, z}}, MemberQ@a]
(* {{a, b, c}} *)
Alternatively if our "fixed" structure is a potential list element we might instead request
oMemberQ[ls_List] := Function[elem, MemberQ[ls, elem]];
Select[{{a, b, c}, a, z, {x, y, z}}, oMemberQ@{a, b, c, x, y, z}]
(* {a, z} *)
with perhaps a more natural application being a function
f[x_?(oMemberQ@{a, b, c, x, y, z})] := expr;
{f[a], f[{a, b, c}]}
(* {expr, f[{a, b, c}]} *)
Multi-argument Operator Forms
Note that almost all system-defined operator forms contain a single argument and in the rare cases where two-arguments are used (MapAt
, Insert
, Entity
) their corresponding canonical form always contain three or more arguments. Hence, to avoid ambiguity, the number of arguments in an operator form apparently always needs to be less than the canonical usage. As a consequence, operators forms become restricted to this canonical usage (currying is a slight misnomer here as it is more accurately an example of partial function application although admittedly partialfunctioning doesn't quite have the same ring) .
Take for example, Lookup
which takes a handy third argument to be returned if the Lookup
fails. The natural operator form Lookup[key, default]
is avoided since the two-argument version has been already been reserved for the canonical Lookup[assoc, key]
; nonetheless we can define a handy helper that
oLookup[key_, def_] := Function[assoc, Lookup[assoc, key, def]];
oLookup["a", "def"]@<|"b" -> c|>
(* "def" *)
Composing Operator Forms
This is one area especially ripe for development given its amenablity to network effects.
Suppose we wanted to perform a previous search but this time wholly with operator forms
Select[MemberQ@a]@{{a, b, c}, a, z, {x, y, z}}
(* {{a, b, c}} *)
This form is only possible as of V10.3 since both Select
and MemberQ
now possess operator forms whose composition can now therefore lead to a new operator form. There are however, more general cases where we might want to define new operator forms from functions that currently do not, or seem unlikely to have, operator forms of their own (at least under current design idioms).
Suppose, for example, we wanted to only
we might be tempted to try
Select[!MemberQ@a]@{{a, b, c}, a, z, {x, y, z}}
(* {} *)
but this fails because feeding an operator form to Not
doesn't yield another operator form as it's not geared for this construction. Rectifying:
\[Dalet][op_] := Function[oper, Not[op@oper]];
Select[\[Dalet]@MemberQ@a]@{{a, b, c}, a, z, {x, y, z}}
(* {a, z, {x, y, z}} *)
Clearly FreeQ
could have been used here but sometimes one wants a particular mode of expression while Boolean operator forms frquently arise where built-in alternatives don't exist.
For example, It feels like we should be able to translate
Select[MemberQ@c || MemberQ@z]@{{a, b, c}, a, z, {x, y, z}}
(* {} *)
but without ||
accepting operator forms we are instead forced back to pure functions.
Select[((MemberQ@c)@# ) || ((MemberQ@z)@#) &]@{{a, b, c}, a, z, {x, y, z}}
(* {{a, b, c}, {x, y, z}} *)
Rectifying by co-opting [vee] we have:
op1_\[Vee]op2_ := Function[arg, Or[op1@arg, op2@arg]];
Select[MemberQ@c\[Vee]MemberQ@z]@{{a, b, c}, a, z, {x, y, z}}
(* {{a, b, c}, {x, y, z}} *)
I find these so useful that I have all Boolean variants re-defined but this idiom's utility is not restricted to logic operations. Suppose we wanted to
SortBy[-Length]@{{a, b, c}, {x, y, z}}
This feels like something we ought to be able to do without pure functions; likewise for arbitrary-depth queries
Select[Length > 2 && EvenQ@Length]@{{a, b, c}, {x, y, z}}
Hence a more general solution would entail operator forms automatically "ascending" to the nearest relevant operator form. Not straightforward but extremely useful given multiplying benefits from network effects.
Disambiguation
As the number of these operator function's grow so to does the challenge of disambiguation; There are three strategies:
- Named Derivatives
- Short-form Notation
- Heuristics
(1) is used for simplicity in the above examples but is not so clean (n.b. also Sort
and SortBy
), (2) is useful for the most common operator forms (as per the first collage, it is no accident the most fundamental/popular functions Map
, Apply
and ReplaceAll
all had their own (short-form) "operator forms" long before V10 arrived) whereas the constant naming in (3) seems the only sustainable solution. Clearly such heuristics can advantageously consist of typing and pattern-matching but for mine the most interesting and promising avenue involves incorporating context to the same extent as routinely occurs with natural language interpretation.
The natural language in Wolfram Alpha still seems too imprecise to be reliably intermingled with Wolfram Language while the latter's precision tends to mitigate against opening spaces for flexibility and fruitful ambiguity. Operator forms perhaps offer a case study/benchmark for how these might be effectively coalesced.
Map[f]{a,b,c}
butMap[f][{a,b,c}]
. $\endgroup$// Map[..#..&]
or//Select[..#..&]
onto the end of expressions. $\endgroup$