26

I have a simple function that is supposed to only accept numeric values (i.e. complex/real numbers and constant symbols e.g. Pi, E).

$$f(a,b,c)=a+b+c$$

Edit: I should have chosen a less simple function for this question as there might be approaches that will work for this simple function but not for functions in general (1-see end of question). Please think of a more complicated function, such as $$f(a,b,c)=a^2 \sin(b) \log(c)$$ when you're thinking of an answer.

I know that one can use _?NumericQ for each parameter such that only numeric values of that parameters are entered into the function (click here for more information on putting constrains on patterns).

Clear[f1]
f1[a_?NumericQ, b_?NumericQ, c_?NumericQ] := a + b + c

f1 @@@ {{1, 2, 3}, {x, y, z}, {1, y, z}, {x, 2, z}, {x, y, 3}}
(* {6, f[x, y, z], f[1, y, z], f[x, 2, z], f[x, y, 3]} *)

However, for functions with more than 1 variables, I'm way too lazy to add NumericQ after each parameter. Using /; at the end of the function definition works, but I feel it's still too long and I have to retype the name of the parameters (a,b,c) at the end.

Clear[f2, f3]
f2[a_, b_, c_] := a + b + c /; And @@ (NumericQ[#] & /@ {a, b, c})
f3[a_, b_, c_] := a + b + c /; VectorQ[{a, b, c}, NumericQ]

Is there any way to express the condition only once, and without having to type the list of parameters one more time? I know that this is a frivolous question borne out of sheer laziness but I'd love to hear your ideas.

(1) such as using the double underscore (BlankSequence) to apply NumericQ to any number of arguments passed to Plus (per Kuba's helpful suggestion). This is because these arguments have identical hierarchy in the function--thus having no need for parameters with different names to represent them--and because Plus can take any number of arguments.

Clear[f4]
f4[a__?NumericQ] := Plus[a]

f4 @@@ {{1, 2, 3}, {x, y, z}, {1, y, z}, {x, 2, z}, {x, y, 3}}    
(* {6,f4[x,y,z],f4[1,y,z],f4[x,2,z],f4[x,y,3]} *)
Display Name
  • 2,009
  • 8
  • 17
seismatica
  • 5,101
  • 1
  • 22
  • 33
  • Thank you Kuba. This is a very neat way to apply conditionals to parameters for n-ary functions (eg. Plus) where the arguments have the same hierarchy, but I'm wondering if--and if so, how--to use this for more general functions. Again thank you for showing me this neat trick. – seismatica Jul 04 '14 at 06:48
  • 8
    If you don't mind encasing arguments into a list, something like this would do for any generic function: bob[args : {a_, b_, c_, d_}] := a*b+c/d /; VectorQ[args, NumericQ] – ciao Jul 04 '14 at 06:50
  • @rasher Nice alternative. – Mr.Wizard Jul 04 '14 at 06:50
  • Thanks rasher! Your comment, for now, is the closest to what my inner sloth envisions. – seismatica Jul 04 '14 at 06:56
  • 5
    It seems this works test[PatternSequence[a_, b_, c_]?NumericQ] := {a, b, c} but I'm not exactly sure why :) – Kuba Jul 04 '14 at 06:57
  • @Kuba Why wouldn't it work? seismatica, please see my updated answer for my proposal for this kind of variation. – Mr.Wizard Jul 04 '14 at 07:00
  • I'm studying your post @Mr.Wizard since it's so wonderfully involved. I'm going to bed and might have to read it in more details tomorrow. Thank you again for your effort! – seismatica Jul 04 '14 at 07:03
  • @Kuba PatternTest applies individually to elements of a sequence and PatternSequence is ostensibly a sequence. seismatica, you're welcome. – Mr.Wizard Jul 04 '14 at 07:11
  • @Mr.Wizard Thanks, it is what I was missing. I should read details&options more often :P – Kuba Jul 04 '14 at 07:13
  • Did you delete your first comment (using __) @Kuba? I learned a great deal from that comment so could you perhaps repost it? If you're not inclined to do so I'm more than happy to post what I remembered from that comment. – seismatica Jul 04 '14 at 07:18
  • @seismatica you can add it to your question with explanation that it is not the case because you have to work with named arguments :) I've deleted it since it was off topic, sorry – Kuba Jul 04 '14 at 07:21
  • @seismatica,What IDE do you use as the picture show? –  Jul 05 '14 at 12:24
  • Related, simpler question: (51382) – Mr.Wizard Aug 28 '14 at 12:13

4 Answers4

27

Ramblings

Arguments of the left-hand-side head are evaluated in the course of function definition, therefore you can use a utility function that constructs the patterns that you want. For example:

SetAttributes[nq, HoldFirst]
Quiet[
 nq[s_Symbol] := s_?NumericQ
]

Now:

ClearAll[f]

f[nq @ a, nq @ b, nq @ c] := a + b + c

Definition[f]
f[a_?NumericQ, b_?NumericQ, c_?NumericQ] := a + b + c

Doing this you lose the nice syntax highlighting shown in the original.

If you Block all Symbols you could even Map the utility function, e.g.:

Block[{f, a, b, c},
  Evaluate[nq /@ f[a, b, c]] := a + b + c
]

This hardly feels like a clean solution however. Perhaps you merely want something shorter than verbatim NumericQ? At risk of a tautology you could always do something like:

ClearAll[f]

q = NumericQ;
f[a_?q, b_?q, c_?q] := a + b + c

But this requires you to keep the Global definition q or it will break as it is not expanded to NumericQ in the definition itself:

Definition[f]
f[a_?q, b_?q, c_?q] := a + b + c

Metaprogramming approach

Another approach would be to write a function to modify all Pattern objects on the left-hand-side at the time of assignment. Something like:

SetAttributes[defWithTest, HoldFirst]

defWithTest[(s : Set | SetDelayed)[LHS_, RHS_], test_] := 
  s @@ Join[Hold[LHS] /. p_Pattern :> p?test, Hold[RHS]]

Now:

ClearAll[f]

defWithTest[
  f[a_, b_, c_] := a + b + c,
  NumericQ
]

Definition[f]
f[a_?NumericQ, b_?NumericQ, c_?NumericQ] := a + b + c

Proposed solution

As Kuba and rasher show in the comments you could also use clever alternatives to the explicit form f[a_?NumericQ, b_?NumericQ, c_?NumericQ]. Inspired by those comments I propose:

SetAttributes[numArgsQ, HoldFirst]
numArgsQ[_[___?NumericQ]] := True

Now:

ClearAll[f]

f[a_, b_, c_]?numArgsQ := a + b + c

Test:

f[1, 2, 3]
f["a", 2, 3]
6

f["a", 2, 3]

For examples of advanced argument testing with an emphasis on messages please see:

Note how (some) internal functions pass additional argument checking (and message generation) to an auxiliary function, e.g. ChartArgCheck, much as I did in the minimal application of numArgsQ above.

Mr.Wizard
  • 271,378
  • 34
  • 587
  • 1,371
  • Leave it you you to make it elegant... +1 as always. – ciao Jul 04 '14 at 07:02
  • @rasher Thank you. :-) – Mr.Wizard Jul 04 '14 at 07:04
  • 2
    Very nice. Fun fact: One of the first hit googling for "wizard rambling" is https://www.youtube.com/watch?v=pEMXYJeCNPY – Yves Klett Jul 04 '14 at 11:58
  • @Yves Interesting, though I didn't understand a lot of it. – Mr.Wizard Jul 04 '14 at 18:20
  • The lyrics are below the video ;-) Will remove if you deem it too whimsical. – Yves Klett Jul 04 '14 at 18:53
  • I think I know how your proposed solution works, but I just want to make sure I'm getting it correctly since I'm new to pattern-testing (and patterns in general): numArgsQ will return true for any function whose arguments are numerics? – seismatica Jul 05 '14 at 01:16
  • @seismatica Yes, that's correct, but more specifically it will return True for any expression whose arguments are numeric. For example all four cases are True: numArgsQ /@ {Subscript[Pi, 3.14], E -> I, 3 + 4 I, 3/7}. See the FullForm of the expressions for better understanding. If any remain enigmatic let me know and I'll explain. – Mr.Wizard Jul 06 '14 at 22:23
5

Update

I was reading the comments to the question, and found that @Kuba had already provided the following answer. I think it's the cleanest solution, so it deserves to be an answer, but please credit him with the idea.

Another idea is to use PatternSequence:

ClearAll[f]

f[PatternSequence[a_,b_,c_]?NumericQ] := a+b+c

Examples:

f[1,2,3]
f["a",2,3]

6

f["a", 2, 3]

Generalization

We can generalize the idea behind this answer as follows:

SequencedPattern[a_] := PatternSequence[##]?a&

Then, we can use SequencedPattern as follows:

f[SequencedPattern[NumericQ][a_, b_, c_]] := a + b + c

The previous examples still work:

f[1, 2, 3]
f["a", 2, 3]

6

f["a", 2, 3]

Here is another example using SequencedPattern:

f[SequencedPattern[PrimeQ][a_, b_, c_], d_] := (a+b+c)/d

f[2,3,4,10]
f[2,3,7,11]

f[2, 3, 4, 10]

12/11

Carl Woll
  • 130,679
  • 6
  • 243
  • 355
  • fantastic flocking share!!! this is one of those elusive holy grail functions (short, sweet, elegant, easy to implement) everyone has been looking for. It really needs to be worked into the language or at least added as a ResourceFunction` – Jules Manson Dec 25 '21 at 20:50
3

I think there is another way to do that:

f[s : (PatternSequence[_?NumericQ] ..)] /; Length[{s}] == 3 :=
 Function[{a, b, c}, a^2 Sin[b] Log[c]][s]

It's suitable for the cases that conditions are the same.

WateSoyan
  • 1,854
  • 13
  • 19
  • @Mr.Wizard What about my code? – WateSoyan May 15 '15 at 10:52
  • 2
    FYI: @ notifications don't work unless a user has already commented. This works too (+1), and you don't even need PatternSequence: f[s__?NumericQ] /; Length[{s}] == 3 will work. Personally I would rather avoid introducing a second construct (Function) for the purpose but that's really a matter of opinion. There are many ways to accomplish this goal; in my answer I sought what I considered the cleanest approach. – Mr.Wizard May 15 '15 at 13:15
  • @Mr.Wizard You code s__?NumericQ ,which I have never noticed is new to me – WateSoyan May 16 '15 at 00:41
0

A Slightly More Complex Case Where Level-1 Heads Can Vary

1. My way by defining a short alias for a long pattern because I don't know any better. However this works!

However before we start how can I add the following condition: /;x!=y preferably to cpat or inside the definition zinfy1[(x:cpat,y:cpat)/;x!=y] (does this look correct?) and not in the inside or at the end of the function body?

(* cpat for common pattern *)
(* all arguments Integers >= 0 or +Infinity *)
cpat = _Integer?(# >= 0 &) | Infinity;

zinfy1[x : cpat, y : cpat] := Style[#, 20, Bold, Green] & /@ {x, y}; zinfy1[x_, y_] := Style[#, 20, Bold, Red] & /@ {x, y};

(* should pass - all args Greeen *) {zinfy1[1, 2], zinfy1[3, 2 + 3], zinfy1[Infinity, 1], zinfy1[0, 0]}

(* should fail- all args Red *) {zinfy1[Infinity, -9], zinfy1[3, 2 - 3], zinfy1[-1 Infinity, 0], zinfy1[-1, 1]}

2. An Attempt to Implement `SequencePattern` on same function and test cases completely fails.

And I don't know why. Anyone here feeling a little adventurous up for the challenge? I could sure use a little help solving this.

SequencedPattern[a_] := PatternSequence[##]?a &;
zinfy2[SequencedPattern[cpat][x_, y_]] := 
  Style[#, 20, Bold, Green] & /@ {x, y};
zinfy2[x_, y_] := Style[#, 20, Bold, Red] & /@ {x, y};

(* should pass - all args Greeen *) {zinfy2[1, 2], zinfy2[3, 2 + 3], zinfy2[Infinity, 1], zinfy2[0, 0]}

(* should fail- all args Red *) {zinfy2[Infinity, -9], zinfy2[3, 2 - 3], zinfy2[-1 Infinity, 0], zinfy2[-1, 1]}

Jules Manson
  • 2,457
  • 11
  • 19