12
$\begingroup$

After reading this post I recalled an old question I once met. We know that we can set default values for variables of a function:

f[u_: 1, v_: 2, w_: 3] := u^2 + v^3 + w^4

We can use the default values like this:

f[]

90

like this:

f[u]

89 + u^2

and like this:

f[u, v]

81 + u^2 + v^3

However, what if I want to call the default value of a variable not at end separately? Something like

f[, v, w]

only gives

Null^2 + v^3 + w^4

while what I want to see is

1 + v^3 + w^4

Of course it's easy to circumvent, but I still wonder if there's a more straight forward solution.

$\endgroup$
5
  • $\begingroup$ @Nasser Well isn't that what options are? $\endgroup$
    – sebhofer
    Commented Jul 26, 2013 at 11:12
  • $\begingroup$ Just to clarify "value...not at end separately": The input f[ , v, w] is identical to f[Null, v, w], so the value Null is being passed for u. The problem is how to program a certain behavior when a particular value, Null, is passed as a parameter. $\endgroup$
    – Michael E2
    Commented Jul 26, 2013 at 12:44
  • $\begingroup$ @Nasser Sure they could, but I don't think the solution would fit the OP's expectation. $\endgroup$
    – sebhofer
    Commented Jul 26, 2013 at 14:15
  • $\begingroup$ Thanks for the Accept. I added two more methods that should be easily extensible; I hope you like them. :-) $\endgroup$
    – Mr.Wizard
    Commented Jul 28, 2013 at 8:58
  • $\begingroup$ xzczd, I think you'll want to see the new answer I just posted. My brain finally warmed up it seems. :^) $\endgroup$
    – Mr.Wizard
    Commented Jul 28, 2013 at 16:14

4 Answers 4

8
$\begingroup$

A simple solution

After writing a very long answer with one related method and four workarounds it suddenly hit me:
We need the behavior of a Symbol with the OneIdentity attribute!

Observe:

Attributes[foo] = {OneIdentity};

MatchQ[Null, foo[Null, i_: 1]]

True

Times is such a function, therefore we may use:

f[(Null i_: 1) | i_, (Null j_: 2) | j_, (Null k_: 3) | k_] := {i, j, k}

f[, ,]
f[, "b",]
f["a", "b",]
f["a", , "c"]
{1, 2, 3}
{1, "b", 3}
{"a", "b", 3}
{"a", 2, "c"}

Success!

An edge case

Well, there is an edge case that must be addressed: when an argument is of the form Times[Null, . . .] and one actually wants that passed to the RHS:

f[Null * "x", "b", "c"]
{"x", "b", "c"}  (* failure *)

This can be addressed by using a localized Symbol with the OneIdentity attribute:

ClearAll[f]

Module[{Star},
  Attributes[Star] = {OneIdentity};
  f[(Null⋆i_: 1) | i_, (Null⋆j_: 2) | j_, (Null⋆k_: 3) | k_] := {i, j, k}]
]

f[Null * "x", ,]
{"x" Null, 2, 3}

Meta-programming automation

Although I think this pattern is simple enough to use directly (at last!), we can also automate its creation if desired. This function uses a very general replacement on any Optional pattern; more specificity may be desired such as targeting only the left-hand-side or looking no deeper than the first level; these are omitted for clarity but can be added if requested.

SetAttributes[makeFluidDefaults, HoldFirst]

makeFluidDefaults[x_Set | x_SetDelayed] :=
  Module[{oi},
    Attributes[oi] = {OneIdentity};
    Unevaluated[x] /. op_Optional :> oi[Null, op] | op
  ]

Now:

ClearAll[f]

makeFluidDefaults[
  f[i_: 1, j_: 2, k_: 3] := {i, j, k}
]

f[, ,]
f[, "b",]
f["a", "b",]
f["a", , "c"]
f[Null*"x", "b", "c"]
{1, 2, 3}
{1, "b", 3}
{"a", "b", 3}
{"a", 2, "c"}
{"x" Null, "b", "c"}
$\endgroup$
13
  • $\begingroup$ Brilliant stuff, and quite interesting behavior on it's own. So what's happening is that Replace[Null, foo[Null, i_: 1] :> i] Matches the pattern, but still doesn't match the pattern, and therefore returns the default. This is kind of the behavior I would have expected from Alternatives, and I actually thought the difference in behavior was the inability of Mathematica to do this kind of simultaneous match/no match. Turns out that wasn't so. $\endgroup$
    – jVincent
    Commented Jul 28, 2013 at 17:24
  • $\begingroup$ Excellent! After you left your comment, I tried to figure out something similar -- different approach, not nearly so elegant, and not yet bug free. $\endgroup$
    – Michael E2
    Commented Jul 28, 2013 at 20:06
  • 1
    $\begingroup$ @jVincent I think the case with Alternatives is that each element is considered sequentially; as soon as Null in Null | i_:1 matches the rest is ignored; this is possibly an important optimization even though it's a sad tradeoff in this context. With this pattern Null i_:1 it is a single expression; because of OneIdentity, Null is transparently converted to (or at least treated like) Times[Null] which then matches Times[Null, i_:1] with i_:1 playing the usual role it would in any other expression. $\endgroup$
    – Mr.Wizard
    Commented Jul 28, 2013 at 21:58
  • $\begingroup$ @Michael out of curiosity: what was your avenue of attack? $\endgroup$
    – Mr.Wizard
    Commented Jul 28, 2013 at 21:59
  • 1
    $\begingroup$ The second sample is now broken because of the issue mentioned here: mathematica.stackexchange.com/questions/224987/1871 $\endgroup$
    – xzczd
    Commented Jun 30, 2020 at 10:06
9
$\begingroup$

Prioritizing patterns

If you only want the default values for the leading parameters (or some other order you choose), and not an arbitrary parameter at the time of the function call, you can prioritize the patterns as described here.

f[
  Shortest[u_: 1, 3],
  Shortest[v_: 2, 2],
  Shortest[w_: 3, 1]
 ] := u^2 + v^3 + w^4

f[v, w]
1 + v^3 + w^4

Workaround No.1

I am still seeking a pattern-based solution to the arbitrary parameter problem, but until and unless I find it here is a work-around you may consider:

g[Null | u_, Null | v_, Null | w_] :=
 With[{uu = # &[u, 1], vv = # &[v, 2], ww = # &[w, 3]},
  {{"u", uu}, {"v", vv}, {"w", ww}}
 ]

g[, v, w]
g[u, , w]
g[u, v,]
{{u,1}, {v,v}, {w,w}}
{{u,u}, {v,2}, {w,w}}
{{u,u}, {v,v}, {w,3}}

How this works:

  • The pattern Null | param_ is used for each parameter: if Null matches, param in the RHS is effectively replaced with Sequence[].

  • The function # & will return the first argument be there one or more than one; by specifying the optional/default values as the second paramter this will be returned iff the first is removed via Sequence[].

  • With may not always be necessary but it is used for robustness.

Workaround No.2

I think this is a bit cleaner in principle than No.1 though neither is fully satisfying.

h[Null | aa_, Null | bb_, Null | cc_] :=
  {{aa}, {bb}, {cc}} /. {{a_: 1}, {b_: 2}, {c_: 3}} :> {a, b, c}

h[5, ,]
h[, 7,]
h[, , 13]
{5, 2, 3}
{1, 7, 3}
{1, 2, 13}

Workaround No.3

I don't think this is as clean as No.2 but it should be easier to extend.

For functions without a hold attribute you could use this:

ClearAll[f]

lhs : f[___, Null, ___] :=
  With[{def = {"1", "2", "3", "4", "5", "6", "7", "8", "9"}},
    MapIndexed[# /. Null -> def[[First @ #2]] &, Unevaluated[lhs]]
  ]

f[1, , 3, 4, , 6, , , 9] // InputForm
f[1, "2", 3, 4, "5", 6, "7", "8", 9]

The list def gives defaults by position for each parameter. Here I chose numeric strings to make the positions evident while still illustrating the substitution.

If your function does have Hold attributes you will need a slightly more complicated replacement. I will make use of RuleCondition(1):

ClearAll[f]
SetAttributes[f, HoldAll]

lhs : f[___, Null, ___] :=
  Module[{i = 1, def = ToString ~Array~ 9},
    Replace[
      Unevaluated[lhs],
      {Null :> RuleCondition @ def[[i++]], x_ /; (i++; True) :> x},
      {1}
    ]
  ]

f[1, , 3, 2 + 2, , 6, , , 3^2] // InputForm
f[1, "2", 3, 2 + 2, "5", 6, "7", "8", 3^2]

Notice that 4 and 9 have been replaced with 2 + 2 and 3^2 and that these correctly remain unevaluated after the substitution.

Workaround No.4

Michael E2 showed what is perhaps the most declarative method. I don't think by itself it is as easily extensible, but then again there must be a limit to how many arguments a user is going to count anyway. Nevertheless one could automate that method with meta-programming, so let's do it.

makeDefaults[fname_Symbol, defaults_List] :=
  fname @@@ {Pattern[#, _] & /@ #, #} & @ Table[Unique["$"], {Length @ defaults}] //
     Do[
        SetDelayed @@ ReplacePart[#, {{1, n} -> Null, {2, n} -> defaults[[n]]}],
        {n, Length @ defaults}
     ] &

Now:

makeDefaults[fn, {"one", "two", "three"}]

fn[u_, v_, w_] := u^2 + v^3 + w^4

?? fn
Global`fn

fn[Null, $2_, $3_] := fn["one", $2, $3]

fn[$1_, Null, $3_] := fn[$1, "two", $3]

fn[$1_, $2_, Null] := fn[$1, $2, "three"]

fn[u_, v_, w_] := u^2 + v^3 + w^4
$\endgroup$
7
  • $\begingroup$ That's interesting :) but what I expect is just a f which works for f[, v, w], f[u, , w], f[ , , w] and so on simultaneously… $\endgroup$
    – xzczd
    Commented Jul 26, 2013 at 9:19
  • $\begingroup$ @xzczd Alright. That's a harder problem. I cannot think of a way to do that with patterns alone. Should I give my own work-around or since you said "it's easy to circumvent" is that not of interest? $\endgroup$
    – Mr.Wizard
    Commented Jul 26, 2013 at 9:25
  • $\begingroup$ Just add it, according to my experience, your work-around will probably show me something new :) $\endgroup$
    – xzczd
    Commented Jul 26, 2013 at 9:31
  • 3
    $\begingroup$ I've always felt that this sort of thing should have been implemented by allowing Optional to take default values when not matched in Alternatives. It just seems like a more intuitive process, that whenever an optional isn't matched, it returns it's default. If that had been the case, this particular design pattern would be a trivial pattern match to Null|v_:1. $\endgroup$
    – jVincent
    Commented Jul 26, 2013 at 10:16
  • 1
    $\begingroup$ @jVincent That certainly would make this particular case easier! It might however complicate other uses? I'm afraid Alternatives in this application doesn't get a lot of attention; I wouldn't be surprised if these things were never seriously considered before the implementation was chosen. $\endgroup$
    – Mr.Wizard
    Commented Jul 26, 2013 at 10:20
6
$\begingroup$

Here's one way to get the desired behavior, although it is not done with a single pattern:

Clear[f];
f[Null, v_, w_] := f[1, v, w];
f[u_, Null, w_] := f[u, 2, w];
f[u_, v_, Null] := f[u, v, 3];
f[u_, v_, w_] := u^2 + v^3 + w^4;

f[, v, w]
f[u, , w]
f[u, v,]
f[u, ,]
f[, v,]
f[, , w]
f[, ,]
1 + v^3 + w^4
8 + u^2 + w^4
81 + u^2 + v^3
89 + u^2
82 + v^3
9 + w^4
90
$\endgroup$
4
  • $\begingroup$ It's worth to mention that the order of the 4 definitions is important :) $\endgroup$
    – xzczd
    Commented Jul 26, 2013 at 13:00
  • 1
    $\begingroup$ @xzczd The ordering of definitions is usually important, but I find it doesn't matter in this case if I randomly order them. (The first three are specific and order makes no difference with them; the fourth is general, so Mathematica reorders it last no matter what.) Is there an order you find does not work? $\endgroup$
    – Michael E2
    Commented Jul 26, 2013 at 13:09
  • $\begingroup$ Ah… I should have checked it more carefully, you are right, the order doesn't care in this case. $\endgroup$
    – xzczd
    Commented Jul 26, 2013 at 13:20
  • $\begingroup$ This is a nice, declarative method that I should have included in the first place (you've got my vote). I added an automated version of this as "No.4" to my answer; take a look if you're interested. $\endgroup$
    – Mr.Wizard
    Commented Jul 28, 2013 at 9:00
1
$\begingroup$

Not sure if this is something in the direction of the named parameters as mentioned by Nasser, however a possible alternative could be:

gg[u[uu_:1],v[vv_:2],z[zz_:3]]:=Print[Column[{Row[{"u=", uu}],Row[{"v=", vv}],Row[{"z =",zz}]}]]

gg[u[1], v[2], z[3]]
u = 1 
v = 2 
z = 3

gg[u[], v[2], z[3]]
u = 1 
v = 2 
z = 3

gg[u[], v[5], z[]]
u = 1 
v = 5 
z = 3

Indeed, without naming the parameters or as alternative using Null (or any symbol like a placeholder), I guess there is no other way to say to the RHS of the definition, which variables is missing.

$\endgroup$
1
  • $\begingroup$ Forgive me, but this seems to be the worst of all worlds as it were. You get neither the simplicity of using Null nor the flexibility of using Options. If one is using named parameters I think in all cases it would be better to use Options directly, e.g. gg["u" -> 1, "v" -> 2]. $\endgroup$
    – Mr.Wizard
    Commented Jul 28, 2013 at 9:03

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.