76
$\begingroup$

Is it possible to create a function with optional arguments that also takes options?

Here is a simple example. I have a function f with option "g". It also has optional arguments y and z which are set to default values.

Options[f] = {"g" -> Identity};

f[x_, y_: 2, z_: 3, OptionsPattern[]] := 
 OptionValue["g"][x + y + z]

Now, if I give values for all of the arguments and an option value it works just fine.

In[3]:= f[1, 2, 3, "g" -> (#^2 &)]

Out[3]= 36

If I give only the required argument, no problems.

In[4]:= f[1]

Out[4]= 6

However, if I give the required arg and an option value but don't give the optional arguments, I run into trouble..

In[5]:= f[1, "g" -> (#^2 &)]

Out[5]= 4 + ("g" -> (#1^2 &))

Is there a good way around this?

EDIT:

Obviously, it is possible to write multiple definitions...

f[x_, y_, z_, OptionsPattern[]] := OptionValue["g"][x + y + z]

f[x_, y_, OptionsPattern[]] := OptionValue["g"][x + y + 3]

f[x_, OptionsPattern[]] := OptionValue["g"][x + 2 + 3]

I'm curious if there is a clean way to do it with a single definition.

$\endgroup$
10
  • $\begingroup$ I tend to do the construction f[(* optional arguments *), (* required arguments *), (* options *)] myself... is there a reason you can't move optional arguments to the front? $\endgroup$ Commented Feb 10, 2012 at 6:25
  • 11
    $\begingroup$ @JM when writing code for others (as I often do) it is typical to reserve the first arg for the most critical input, say the data in a statistical test and leave the remaining args for extra tweaking. Thus, the ordering is important. I typically write multiple definitions but was wondering if it is possible to do in one go. $\endgroup$
    – Andy Ross
    Commented Feb 10, 2012 at 6:32
  • $\begingroup$ There's also the possibility of strict typing for your optional arguments. If your optional arguments cannot have the head Rule or RuleDelayed, the typing would prevent options being interpreted as optional arguments... $\endgroup$ Commented Feb 10, 2012 at 6:37
  • 3
    $\begingroup$ @JM I often use arg_?(Not[OptionQ[#]]&) to cover the Rule and RuleDelayed cases. This doesn't work though if I have optional args with default settings. $\endgroup$
    – Andy Ross
    Commented Feb 10, 2012 at 7:01
  • $\begingroup$ See my answer for how I believe that pattern can be used. $\endgroup$
    – Mr.Wizard
    Commented Feb 10, 2012 at 7:43

7 Answers 7

41
$\begingroup$

Mathematica has to be able to tell that the default arguments can't be rules. So, for some special cases, you could do

Options[f] = {"g" -> Identity};

f[x_, y_Integer: 2, z_Integer: 3, OptionsPattern[]]:= OptionValue["g"][x + y + z]

Testing:

f[1, 2, 3, "g" -> (#^2 &)]
36
f[1]
6
f[1, "g" -> (#^2 &)]
36
$\endgroup$
1
  • 3
    $\begingroup$ Is it time for OptionsObject? .. . entered via keyboard shortcuts and or: <o| "g"-> Identity |o> . [The small o looked better than cap] $\endgroup$ Commented Apr 5, 2015 at 20:51
37
$\begingroup$

Here is another method that I learned through reading Inside the Mathematica Pattern Matcher:

Options[f] = {"g" -> Identity};

f[x_,
  Shortest[y_: 2, 1],
  Shortest[z_: 3, 2],
  OptionsPattern[] 
 ] := OptionValue["g"][x + y + z]

From the documentation for Shortest:

Shortest[p, pri] is given priority pri to be the shortest sequence. Matches for shortest sequences are tried first for Shortest objects with higher priorities.

$\endgroup$
2
  • $\begingroup$ This is maybe the most elegant answer. $\endgroup$
    – FJRA
    Commented Feb 14, 2012 at 5:12
  • $\begingroup$ @FJRA thank you. I thought it was worthy of its own post as it is unrelated to my first method(s). Also, welcome to the site. :-) $\endgroup$
    – Mr.Wizard
    Commented Feb 14, 2012 at 5:54
30
$\begingroup$

Edit: better answer here.

I voted for Rojo's answer. If for some reason you cannot be that specific about your arguments you might use the converse:

nr = Except[_?OptionQ];

f[x_, y : nr : 2, z : nr : 3, OptionsPattern[]] := OptionValue["g"][x + y + z]

If for some further reason you need the optional arguments to be rules themselves, you could filter out specifically valid options:

Options[f] = {"g" -> Identity};

notOpts = 
  Except[Alternatives @@ Replace[Options[f], h_[a_, _] :> h[a, _], 1]];

f[x_, y : notOpts : 2, z : notOpts : 3, OptionsPattern[]] :=
  {OptionValue["g"], x, y, z}

f[1, "a" -> 7, "g" -> "g value"]
{"g value", 1, "a" -> 7, 3}
$\endgroup$
8
  • $\begingroup$ @MrWizard This is pretty cool, why does this work when nr = x_/;!OptionQ[x] does not? $\endgroup$
    – Andy Ross
    Commented Feb 10, 2012 at 7:45
  • $\begingroup$ @Andy Condition applies to the entire function pattern. Think about f[1, 2, x_ /; x > 5] := ... and f[1, 2, 3] $\endgroup$
    – Mr.Wizard
    Commented Feb 10, 2012 at 8:01
  • $\begingroup$ @MrWizard I guess I'm just not getting it.. its 2:00 am here so that may be why :) What I'm seeing from your example is obvious, the 3rd arg will not admit anything smaller than 5. But !OptionQ will exclude only Rule, {}, and RuleDelayed, (I think). $\endgroup$
    – Andy Ross
    Commented Feb 10, 2012 at 8:09
  • $\begingroup$ @Andy I mean that if the condition fails the entire definition of f does not match. $\endgroup$
    – Mr.Wizard
    Commented Feb 10, 2012 at 8:27
  • 2
    $\begingroup$ Something that bit me in the past about optional arguments and options was not noticing that OptionsPattern[] can match not only a sequence of options, but also any nested list of options. Luckily, OptionQ also returns true for such lists! Thus this is not a loophole in your method. But the discovery that OptionQ[{a->1, b->2}] is True is new to me. EDIT: Well, it's pretty clearly stated in the usage messages, but I didn't look there ... $\endgroup$
    – Szabolcs
    Commented Sep 9, 2016 at 13:39
21
$\begingroup$

A bit late-to-the-party post, and complementary to the other solutions. Several answers addressed the question quite well IMO. I had my shot on a similar one here, with a solution similar to the one by @Mr.Wizard. But now I just want to stress one subtle point missed by other answers: using OptionQ will leak evaluation for functions which are HoldAll and take optional arguments and options. As an example, consider a modified solution of @Mr.Wizard:

ClearAll[f];
nr = Except[_?OptionQ];
Options[f] = {"g" -> Identity};
SetAttributes[f, HoldAll];
With[{nr = nr},
  f[x_, y : nr : 2, z : nr : 3, OptionsPattern[]] := OptionValue["g"][Hold[x + y + z]]
]

Note by the way that we had here to inject nr. In my solution in the linked Mathgroup thread, I used notOptionQ[x_] := ! OptionQ[x];, and the patterns looked like _?notOptionQ, so for this one injecting is unnecessary. In any case, try this code:

f[1, Print["*"], "g" -> 10]

You will see that printing happens as a part of the pattern-matching procedure. To avoid that, one should use this pattern instead:

nr = Except[_?(Function[elem, OptionQ[Unevaluated[elem]], HoldAll])];

Parentheses around Function are mandatory, and injecting is still required in this approach. The f redefined with this pattern does not leak evaluation:

f[1, Print["*"], "g" -> 10]

(*
  ==> 10[Hold[1 + Print["*"] + 3]]
*)

Admittedly, the case of Hold*-functions is not that often in practice, but I thought this subtlety should be mentioned here as well.

$\endgroup$
3
  • 2
    $\begingroup$ As always you take it to another level. $\endgroup$
    – Mr.Wizard
    Commented Feb 10, 2012 at 10:12
  • $\begingroup$ @Mr.Wizard Thanks! So, back the the good old name? I must admit I got used to Mr.Wizard and the transition to Spartacus in our correspondence wasn't easy, so I am happy now. Congrats with becoming a mod, by the way! This will surely help make this site a better place. $\endgroup$ Commented Feb 10, 2012 at 10:20
  • $\begingroup$ Thank you Leonid. I always intended to switch back. $\endgroup$
    – Mr.Wizard
    Commented Feb 10, 2012 at 10:24
15
$\begingroup$

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.

$\endgroup$
6
  • $\begingroup$ +1. I would add however that depending on one's perspective, your own In[6] := lines shows this "tripping up" which is why I provided the methods that I did. $\endgroup$
    – Mr.Wizard
    Commented Feb 12, 2012 at 5:30
  • $\begingroup$ Also, what is the reason for using argumentDelimiter rather than Hold or HoldComplete? $\endgroup$
    – Mr.Wizard
    Commented Feb 12, 2012 at 5:36
  • 1
    $\begingroup$ @MrWizard In principle Shortest should be redundant but I don't know if OptionsPattern[] makes use of Shortest, so per the documentation of Shortest (first Shortest wins) I used it anyway. And I used a unique symbol to avoid conflicts with functions that take Hold/HoldComplete as part of their normal arguments. $\endgroup$ Commented Feb 12, 2012 at 5:43
  • 1
    $\begingroup$ I agree that this is normal behavior. I was simply looking at the corner case where one of the named arguments should be in the form of a rule, as you asked us to point out shortcomings and it's the only one I can see. $\endgroup$
    – Mr.Wizard
    Commented Feb 12, 2012 at 5:44
  • 3
    $\begingroup$ Good stuff - +1. I often use Hold or HoldComplete to simply separate options from arguments, like f[Hold[x_,y_,x_],opts:OptionsPattern]. This allows me to not make f HoldAll (assuming I want to hold x,y and z). The advantage is that I can call it as f[Hold[a,b,c],opts] and this will match if opts stores some options. Otherwise, I'd have to make f HoldAll and call it as f[a,b,c,Evaluate[opts], and in practice it is very easy to forget Evaluate and get bugs, particularly if I expose the function to the user, who can not be expected to remember about Evaluate. $\endgroup$ Commented Feb 12, 2012 at 10:41
13
$\begingroup$

I too like Rojo's answer. And Mr. Wizard's answer is intriguing as always. A third possibility is adding the condition that the Heads of second and third arguments are not Rule after the LHS. So, the following also works

 Options[f] = {"g" -> Identity};
 f[x_, y_: 2, z_: 3,  OptionsPattern[]] /; (Head[y] =!= Rule && Head[z] =!= Rule):= 
 OptionValue["g"][x + y + z]

The condition could be alternatively stated as (OptionQ[y] =!= True && OptionQ[z] =!= True).

$\endgroup$
5
  • 1
    $\begingroup$ @Mr.Wizard, hope the new condition works? $\endgroup$
    – kglr
    Commented Feb 10, 2012 at 9:53
  • 1
    $\begingroup$ I didn't think that would work. I like being proven wrong: it's embarrassing but I learn from it. +1 $\endgroup$
    – Mr.Wizard
    Commented Feb 10, 2012 at 10:17
  • $\begingroup$ Thanks @kglr, I have read through all the answers and found that yours is the most elegant and efficient. For other answers , either it does not work anymore since OptionQ does not exist in recent versions of MMA and there is even no items related OptionQ in online documentation, or its algorithm complexity is much more greater than yours. Just now I ran a test on my computer using your solution to modify my program and it gave me a all-success. Pitifully I am not the poster, so I can only vote up your solution. If I were the poster, I would definitely accept your answer as the best! $\endgroup$
    – AlbertLew
    Commented May 19, 2022 at 6:35
  • $\begingroup$ @AlbertLew, thank you for the vote and kind words. OptionQ works in recent versions too (I tried version 11.3.0 and version 13.0.1). It is still not documented but Information @ OptionQ gives sufficient info. $\endgroup$
    – kglr
    Commented May 19, 2022 at 8:27
  • $\begingroup$ I see. In my previous belief, if a built-in function cannot be found in the latest documentation then it is obsolete , not working anymore and might have been replaced by some other new built-in function. Never thought there are functions like OptionQ which is built-in , not documented but still works! A nice learning experience :). By the way, even those solutions with OptionQ still works, their algorithm efficiencies still lag far behind yours. Good Job!@kglr $\endgroup$
    – AlbertLew
    Commented May 19, 2022 at 11:27
2
$\begingroup$

One simple trick nobody mentioned yet is to use PatternSequence for your positional arguments:

f[PatternSequence[x_, y_ : 2, z_ : 3], OptionsPattern[]]

Why it works, though? No idea :). I hate how Optional and Default works in general, it doesn't make sense to me in many many cases.

$\endgroup$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.