Here's another possibility that makes use of a more involved pattern to delimit the arguments from the options. It seems to me that people writing "function definitions" are inclined to think of them rigidly in that way, commonly forgetting that these are still just patterns and can be used (and abused) as such. However, bearing in mind the true nature of these definitions can be useful in circumstances like this:
Options[f] = {"g" -> Identity};
Module[{argumentDelimiter},
SetAttributes[argumentDelimiter, HoldAllComplete]; (* thanks Leonid! *)
f[args : Shortest[___], opts : OptionsPattern[]] :=
f[argumentDelimiter[args], opts];
f[argumentDelimiter[x_: 1, y_: 2, z_: 3], OptionsPattern[]] :=
OptionValue["g"][x + y + z];
];
(As you can see, I made the first argument optional as well for didactic purposes.)
It seems to work well enough:
In[3] := f[]
Out[3] = 6
In[4] := f["g" -> Internal`Square] (* ;) *)
Out[4] = 36
In[5] := f[3, 3, "g" -> Sqrt]
Out[5] = 3
In[6] := f[1, 17, "pianoforte" -> "harpsichord", "g" -> Sqrt]
OptionValue::nodef: "Unknown option pianoforte for f." >>
Out[6] = Sqrt[21]
In fact, I wasn't able to trip this method up at all, which makes me slightly suspicious that I didn't try enough test cases. Can anyone else point out some shortcomings of this approach?
Edit
In response @Mr.Wizard's comments, I thought it best to clarify the following:
Why use argumentDelimiter
, rather than Hold
or HoldComplete
?
Because argumentDelimiter
is created inside a Module
, its uniqueness can be guaranteed. As a result there is no possibility for conflict when one wishes to define functions that accept arguments wrapped with Hold
etc. This approach is commonly seen in packages, although in that case a suitable symbol will usually be created in the package`Private`
context once and for all rather than relying on Module
because of the associated overheads.
Isn't Shortest
redundant?
In principle, yes; however, since OptionsPattern[]
may also make use of Shortest
, in my opinion it is better to avoid unexpected conflicts by explicitly requiring the shortest possible sequence of arguments. As documented for Shortest
, in case of two competing Shortest
patterns appearing in the same expression, the one that appears first has higher priority, so anything that can match OptionsPattern[]
always will in this case.
What if the optional but non-option arguments also happen to match OptionsPattern[]
?
In this case one has problems regardless of using this technique, and I would consider it to be a limitation of OptionsPattern[]
that invalid options also match. To avoid this situation, one must use @Mr.Wizard's suggestion instead of OptionsPattern[]
, i.e.:
validOptionsPattern[f_] :=
Alternatives @@
Replace[Options[f], (h : (Rule | RuleDelayed))[opt_, _] :> h[opt, _], 1] ...;
The approach then becomes:
Module[{argumentDelimiter},
SetAttributes[argumentDelimiter, HoldAllComplete];
f[args : Shortest[Except[_argumentDelimiter] ...], opts : validOptionsPattern[f]] :=
f[argumentDelimiter[args], opts];
f[argumentDelimiter[x_: 1, y_: 2, z_: 3], OptionsPattern[]] :=
OptionValue["g"][x + y + z];
];
Note that Options[f]
must be set before evaluating this definition so that validOptionsPattern[f]
has the correct value. (At least for the time being, because making a validOptionsPattern[]
that works exactly like OptionsPattern[]
except for validating the options seems to be non-trivial.)
So, first with Options[f] = {"g" -> Identity}
:
In[5] := f[1, 17, "pianoforte" -> "harpsichord", "g" -> Sqrt]
Out[5] = Sqrt[18 + ("pianoforte" -> "harpsichord")] (* treated as argument *)
And now with Options[f] = {"g" -> Identity, "clarinet" -> "vuvuzela"}
:
In[10] := f[1, 17, "clarinet" -> "vuvuzela", "g" -> Sqrt]
Out[10] = Sqrt[21] (* option accepted *)
So everything appears to work correctly. However, if your optional arguments could also be interpreted as valid option values, I would consider that situation ill-defined and as such outside the scope of this answer.
f[(* optional arguments *), (* required arguments *), (* options *)]
myself... is there a reason you can't move optional arguments to the front? $\endgroup$Rule
orRuleDelayed
, the typing would prevent options being interpreted as optional arguments... $\endgroup$arg_?(Not[OptionQ[#]]&)
to cover theRule
andRuleDelayed
cases. This doesn't work though if I have optional args with default settings. $\endgroup$