33

I have a newbie question: is it possible to write a function that counts the arguments (total and optionals) of a given function? Possibly it should be able to work with built-in and custom functions as well.

For example, if I define

f1[x_Integer] := x + 1;
f2[x_Integer, y_Integer: 1] := x + y;
g[x_Real, y_] := x - y;

I would like to have

countArgs[f1]
{1,0}
countArgs[f2]
{2,1}
countArgs[g]
{2,0}

and also, for example,

countArgs[Sin]
{1,0}

thank you.

@celtschk

Well, I didn't even know the use of UpValues, but basically what I am asking is the number of inputs the function needs, I don't care what the function does with those inputs. In your examples I would say

  • {Infinity,0} for foo? It's more a question than an answer, sorry, but I had not thought aboute these unusual cases.
  • this is nasty, I didn't think of a case like that either, I would say {3,{2}}, the {2} meaning exactly 2, to avoid bar[1,2], which is not legal.
  • {2,0} but just because you wrote f[g,g], so it's practically a guess, I don't know what are UpValues and if they go against the spirit of my question by messing with the function. I hope I was clear.
  • the last one I would say {3,0} as they were flattened.

Thank you, I start seeing that my question is not so obvious because there are too many complicated definitions for functions to take into account.

For now I understood that is possible with built-in functions with

SyntaxInformation[f]

(thank you Heike) but that mybe is a little too much asking for a general custom function.

Jane T.
  • 495
  • 4
  • 8
  • 5
    For built in functions you can use SyntaxInformation. For example SyntaxInformation[Mod] returns {"ArgumentsPattern" -> {_, _, _.}} indicating 3 arguments of which the last one is optional. – Heike Jun 19 '12 at 14:33
  • 3
    What should it return for foo[x__Integer] := {x}? And what for bar[x_] := 1; bar[x_, y_, z_] := 2? Should it also consider UpValues like f[g,g] ^= 0? And would a[x_, {y_, z_}] := x+y/z count as taking two or three arguments? – celtschk Jun 19 '12 at 14:39
  • 1
    This becomes challenging, output for the following cases ? f[x_]:=1,f[x_,y_]:=1,f[x_,OptionsPattern[]] – image_doctor Jun 19 '12 at 14:52
  • Then there are things like p[n_][x_] := (* stuff *)... – J. M.'s missing motivation Jun 19 '12 at 15:03
  • @imnage_doctor In order I would say:
    • {1,0}
    • {2,0}
    • {1+n,n} assuming that OptionsPattern takes up to n optional values
    – Jane T. Jun 19 '12 at 15:04
  • @Jane T. for foo[x__Integer] := {x} x is a BlankSequence and thus can be any number of arguments. Then {1,0} would not necessary be correct... – freddieknets Jun 19 '12 at 15:14
  • @J.M What is the difference between f[x_,y_] and f[x_][y_] ? Is f[x] alone defined in the latter case? If not, I would say {2,0}, otherwise {2,1}. – Jane T. Jun 19 '12 at 15:16
  • @freedieknets thanks, I didn't see the double downscore, in that case i'd say {Infinity,0}, but I think it's pushing the question too far... – Jane T. Jun 19 '12 at 15:18
  • "Is f[x] alone defined in the latter case" - well, it allows you to do things like f[x] /@ {a, b, c, ...} – J. M.'s missing motivation Jun 19 '12 at 15:20
  • @JaneT. If all three definitions for f coexist perhaps the output might be a list: {{1,0,False},{2,0,False},{1,0,True}} where {_,_,_}->{required,optional,options} – image_doctor Jun 19 '12 at 15:30
  • @image_doctor yes, very clever indeed. But i don't know how to work that out either with all the extreme examples posted. – Jane T. Jun 19 '12 at 15:34
  • @J.M. in that case y in f[x_][y_] is sort of optional... but it's getting too confusing for me, too many special cases I didn't think about. – Jane T. Jun 19 '12 at 15:37
  • @JaneT. Just wait a few minutes and mma.se will work its magic. – image_doctor Jun 19 '12 at 15:40
  • What about this pattern, f[a:x_ + y_:4], what should your function return, then? – rcollyer Jun 20 '12 at 01:54

3 Answers3

18

Here is my attempt. The function below will work on functions with default args and options, as well as those having multiple definitions. I made the following assumptions:

  • Only DownValues - based definitions are considered
  • Default arguments, if present, are always to the right of mandatory arguments.
  • Options, if present, are always to the right of all other arguments, and are declared either by OptionsPattern[] or opts:OptionsPattern[] pattern.

Here is the code:

ClearAll[countArgs];
SetAttributes[countArgs, {HoldAll, Listable}];
countArgs[f_Symbol] := 
   With[{dv = DownValues[f]}, countArgs[dv]];

countArgs[Verbatim[HoldPattern][HoldPattern[f_Symbol[args___]]] :> _] :=
  countArgs[f[args]];

countArgs[
   f_[Except[_Optional | _OptionsPattern | 
         Verbatim[Pattern][_, _OptionsPattern]], rest___]] :=
  {1, 0, 0} + countArgs[f[rest]];

countArgs[ f_[o__Optional, rest___]] := 
  {0, Length[HoldComplete[o]], 0} +  countArgs[f[rest]];

countArgs[f_[_OptionsPattern | Verbatim[Pattern][_, _OptionsPattern]]] := 
  {0, 0, 1};

countArgs[f_[]] := {0, 0, 0};

This function represents a mini-parser for the function's declarations. It returns a list of 3-element sublists, of the length equal to a number of DownValues. In each sublist, the first number is a number of normal arguments, the second one is a number of default arguments, and the last one (which can only be 0 or 1), tells us whether or not there are options declared.

Some examples:

ClearAll[f1, f2, f3, f4, g]
f1[x_Integer] := x + 1;
f2[x_Integer, y_Integer: 1] := x + y;
f3[x_, y_, z_: 1, q_: 2, opts : OptionsPattern[]] := x + y + z + q;
f4[x_, y_: 1] := x + y;
f4[x_, y_, z_] := x + y + z;
g[x_Real, y_] := x - y;

Now applying our function:

countArgs /@ {f1, f2, f3, f4, g}
{{{1, 0, 0}}, {{1, 1, 0}}, {{2, 2, 1}}, {{1, 1, 0}, {3, 0, 0}},{{2, 0, 0}}}
Leonid Shifrin
  • 114,335
  • 15
  • 329
  • 420
  • Impressive! Thank you Leonid. Can I accept more than 1 answer? Yours truly deserves it. (PS. you typed two f4). – Jane T. Jun 19 '12 at 15:43
  • @Jane T. You can only accept one answer, and generally it is a good idea to wait a little bit before accepting any, so that people are motivated to write alternative answers. You can also unaccept an already accepted answer and accept another one if you like it better (please not that this is just generaly FYI, not an advice for this particular case). As for f4, this is intentional. In case you did not know, Mathematica functions can have more than one definition at the same time, and the one to be used is the one whose pattern will first match. – Leonid Shifrin Jun 19 '12 at 15:47
  • @Leonid Shifrin Excellent :) Perhaps the 3rd element of the result might reflect Length@Options@f ? – image_doctor Jun 19 '12 at 15:47
  • @image_doctor Thanks :). " Perhaps the 3rd element of the result might reflect Length@Options@f" - I don't think this is really necessary, but it could be a possibility (although Options might not be defined at the time when definitions are considered, although this is admittedly a rather contrived situation). – Leonid Shifrin Jun 19 '12 at 15:49
  • @LeonidShifrin Perhaps counting options should be an option to countArgs ;) I think Options returns an empty list if the options for f aren't defined. Which I guess could be interpreted as logically correct from one viewpoint. – image_doctor Jun 19 '12 at 15:54
  • @image_doctor Well, perhaps :). – Leonid Shifrin Jun 19 '12 at 15:56
  • @JaneT. Please feel free to unaccept my answer, I won't be offended :-) – Simon Woods Jun 19 '12 at 16:01
  • @LeonidShifrin Your code is very sophisticated, thank you. – Jane T. Jun 19 '12 at 16:02
  • @JaneT. Thanks for the accept. You may be also inetrested in somewhat similar discussions here and here, where, at least in my answers, somewhat similar parsers were constructed. – Leonid Shifrin Jun 19 '12 at 16:09
  • @SimonWoods You are very kind. I did it. – Jane T. Jun 19 '12 at 16:10
  • @LeonidShifrin I'll check the links, thank you. – Jane T. Jun 19 '12 at 16:13
  • Leonid, I hate to burst your bubble, but what about f[a : {{_, _} ..} : {{1, 2}}] := {a} or g[a : x_ + y_: 4] := {a, x, y}? I get {{{0, 1, 0}}, {{1, 0, 0}}} from mapping over them. – rcollyer Jun 20 '12 at 02:06
  • @rcollyer I think this is the correct behavior. Your f has a single optional arguments a, thus I get {{0,1,0}} for it. Your g has a single non-optional argument Plus[x_,y_:4], which, while having an optional part, is, as a whole, non-optional, so I get {{1,0,0}}. You probably meant g1[a:(x_+y_):4]:={a,x,y}, in which case, I get {{0,1,0}}, since the argument is now indeed optional. – Leonid Shifrin Jun 20 '12 at 03:12
  • I thought I had you. You're right. I missed the fact that the 4 was attached to the y, and not the whole sum. – rcollyer Jun 20 '12 at 03:31
  • @rcollyer Not so easy :). But I don't pretend to have captured all cases, I am sure it is not too difficult to construct cases where my current implementation will fail. It is however easily extensible. – Leonid Shifrin Jun 20 '12 at 03:38
  • But, that doesn't mean we won't stop trying. :) (Of course, the more we try, the harder it gets to succeed as you get better.) I just remember how difficult it was to get something like this to work consistently when trying to determine variable names, so you're right, there is likely flaws. – rcollyer Jun 20 '12 at 04:09
  • @rcollyer I certainly got a lot better during my time on SO and then here, particularly in this sort of parsing stuff. But, as you said, writing a complete parser is no easy task, since you have to account for all details, and special cases. – Leonid Shifrin Jun 20 '12 at 04:18
9

This only works for simple cases such as those in the question:

countArgs[f_]:=Module[{countarg},
countarg[x___,y___Optional]:=Length/@{{x},{y}};
(DownValues[f]/.f->countarg)[[All,1,1]]]

E.g.

f1[x_]:=x+1;
f2[x_,y_:1]:=x+y;
g[x_,y_]:=x-y;
g[x_]:=x+1;

countArgs@f1
Out[6]= {{1,0}}

countArgs@f2
Out[7]= {{1,1}}

countArgs@g
Out[8]= {{2,0},{1,0}}

The example for g shows how it produces a list of answers for functions with more than one definition.

Simon Woods
  • 84,945
  • 8
  • 175
  • 324
  • Thank you Simon, works perfectly. As others has showed, a general answer is asking too much. – Jane T. Jun 19 '12 at 15:23
  • I am new to stackexchange, so I was eager to vote for an answer as a sign of my appreciation, I was not aware it was a better custom to wait (as Leonid explained). Your answer is very good even if I have to choose Leonid. – Jane T. Jun 19 '12 at 16:09
  • @JaneT. Voting for many answers is fine, but you can only "accept" (click the "tick") for one answer. If several people give good answers, it is definitely fine to vote for them all (click the uparrow above the number at the top left of each answer), and "accept" the one that is most useful for you. And a belated welcome to Mathematica.StackExchange!! – Verbeia Jun 29 '12 at 11:25
6

This is not intended as an answer, or not at the moment. I just thought it was worth mentioning a built-in that is documented but not in the Doc Center

ArgumentCountQ[head, len, min, max] tests whether the number len of arguments of a function head is between min and max.

ArgumentCountQ[head,len,{Subscript[m, 1],Subscript[m, 2],...,Subscript[m, i]}] tests whether the number len of arguments of a function head is one of the Subscript[m, i].

EDIT

Ok, this is totally useless to the question, but for now I'm leaving this since people seem to like to learn new functions.

This is the predicate that normally issues the famous message xxx called with 2 arguments; X arguments are expected.. It receives the head, the number of arguments input into the expression, and the number of expected arguments, either as a range or as a list of possible numbers. If the number of arguments is right, it returns True. If it's not, it returns False and issues a message. Furthermore, there's the function System`FEDump`NonOptionArgCount possibly useful for argument counting. A sample of use could be

f2[i___, OptionsPattern[]] := {i} /; 
  ArgumentCountQ[f, System`FEDump`NonOptionArgCount[{i}], {2}]

A perhaps less simple but more common use case could be

f[i_, j_, OptionsPattern[]] := i + j
f[i___] := (ArgumentCountQ[f, 
   System`FEDump`NonOptionArgCount[{i}], {2}]; Null /; False)

The first definition is your usual function definition. The second one catches all, and in all cases runs the first argument of the CompoundExpression: ArgumentCountQ[f, System`FEDump`NonOptionArgCount[{i}], {2}]. This gives True if the number of arguments passed, not counting the options, is exactly 2, but we don't care about that. We only care about the message it issues when it's not. After that, the definition is not applied since there's a hard-coded condition set to False, so it returns unevaluated.

Rojo
  • 42,601
  • 7
  • 96
  • 188