9
\$\begingroup\$

I needed a way to find the number of arguments a function or operation in Rebol accepts (the functions arity). I could not find any direct support for this in the language, so I made my own arity function:

arity: func [
    "Returns the arity of a function, disregarding refinements."
    :f [function! op! native! action!]
    /local spec result
] [
    result: 0
    spec: copy third :f
    loop length? spec [
        switch type?/word first spec [
            word! [ result: result + 1 ]
            refinement! [ break ]
        ]   
        spec: next spec
    ]   
    result
]   

Let's take it for a spin:

REBOL/Core 2.7.8.4.2 (2-Jan-2011)
>> do load %arity.reb
>> help arity
USAGE:
    ARITY :f 

DESCRIPTION:
     Returns the arity of a function, disregarding refinements.
     ARITY is a function value.

ARGUMENTS:
     f -- (Type: function op native action)
>> arity print
== 1
>> arity +
== 2
>> arity either
== 3
>> arity quit
== 0

Questions I have:

  1. Are there really no easier, built in way of getting the arity?
  2. Am I covering everything (all function types) by specifying [function! op! native! action!]?
  3. Are there any corner cases where this function will give the wrong answer?
  4. Does it make sense to copy the parameter block from f like I do here?
  5. Finally: Can you suggest any improvements to my code?
\$\endgroup\$
3
  • \$\begingroup\$ Related question: stackoverflow.com/questions/17707073/… \$\endgroup\$
    – rgchris
    Mar 29 '15 at 21:28
  • \$\begingroup\$ On naming, would arity-of be more consistent with other reflection functions here? \$\endgroup\$
    – rgchris
    Mar 29 '15 at 21:29
  • \$\begingroup\$ @rgchris it probably would. I did not know about words-of.., the only *-of words in the dictionary I'm using is maximum-of and minimum-of. \$\endgroup\$
    – Torbjørn
    Mar 30 '15 at 7:57
5
\$\begingroup\$

Are there really no easier, built in way of getting the arity?

That exact function is not in the box. But the most important thing most people want to do with arity (a single-step evaluation) is covered by do/next. Usually you can't really look past the refinements, so a function that skips them probably doesn't come up as often as you might think.

Am I covering everything (all function types) by specifying [function! op! native! action!]?

I presume this is Rebol2 (as it would not run in Rebol3). But in Rebol3, you missed closure!.

But...an easier way is to use the typeset any-function!, along with its corresponding test any-function?. Though I think from a user-level all the types should be collapsed into function! and function?. A few tests like infix? could take care of the other attributes.

Are there any corner cases where this function will give the wrong answer?

In Rebol3 you wouldn't be able to do reflection by picking values like that. It introduced spec-of, words-of, and body-of.

Also in Rebol3, that's not the way that a get-word! parameter would work. If you pass in a word! to :f expecting a function, you'd get a type error. The distinction between how :f and 'f are handled in Rebol3 is actually very useful...both are quotes, but a lit-word! will still evaluate parens.

Does it make sense to copy the parameter block from f like I do here?

In Rebol2 if you pick positionally you seem to be able to modify...so copying is safer to prevent accidents. But what you actually want here is words-of, and I believe that all those reflectors return copies.

Finally: Can you suggest any improvements to my code?

arity: func [
    "Returns the arity of a function, disregarding refinements."
    'f [any-function! word!]
    /local result
] [
    result: 0
    if word? :f [assert [any-function? f: get f]]
    foreach value words-of :f [
        unless word? value [break]
        ++ result
    ]
    result  
]

This has the advantage of working in Rebol2 or Rebol3, allows either a word! that it gets or a function-valued parameter:

>> arity append
== 2

>> arity :reverse
== 1

Plus in Rebol3 you can to use expressions via parentheses, which is a good instance of why the "quote everything but parens" is pretty nice:

>> funs: reduce [:append :reverse]

>> arity (first funs)
== 2

>> arity (second funs)
== 1

It's probably the case that you don't actually want to make ARITY take a word and get it. When people see a word they usually assume that means it's a call. Compare the above with:

>> arity first funs
== [...] ; block of two functions

>> arity second funs
== [...] ; same...

Usually breaking the rule is done very selectively in cases where you want to make something easy to use in the console (like help). So unless you have a good reason, I'd suggest the simpler:

arity: func [
    "Returns the arity of a function, disregarding refinements."
    f [any-function!]
    /local result
] [
    result: 0
    foreach value words-of :f [
        unless word? value [break]
        ++ result
    ]
    result  
]
\$\endgroup\$
2
  • \$\begingroup\$ I did not realize Rebol had any-function! (only any-function?), and I had seen words-of nowhere in the docs. Thanks! Also your point about breaking the rules is probably a good one. \$\endgroup\$
    – Torbjørn
    Mar 30 '15 at 7:49
  • \$\begingroup\$ @Torbjørn Breaking the rules is fun when you're doing console stuff. Interesting examples in the Ren Garden dialects if you haven't seen that. :-) Also, there's a chat here... \$\endgroup\$ Mar 30 '15 at 11:59
7
\$\begingroup\$

Finally: Can you suggest any improvements to my code?

Fair warning ahead: I've only tried the following with Rebol 3. That said: going into a different direction than the improvements suggested by @HostileFork, here's an alternative approach:

arity: func [
    "Returns the arity of a function, disregarding refinements."
    f [any-function!]
] [
    -1 + index? find join words-of :f /sentinel refinement!
]

(No get-/lit-argument tricks here, sorry. Also refer to @HostileFork's answer for that particular aspect.)

The Gory Details

For those not that familiar with Rebol, let's dissect that step by step. First, a fully parenthesised variant of the core expression:

-1 + (index? (find (join (words-of :f) /sentinel) refinement!)

@HostileFork's answer already mentions WORDS-OF, which is a convenient way to get only the core parts of a function spec:

>> words-of :append
== [series value /part length /only /dup count]

>> words-of :+
== [value1 value2]

Building on that, the core idea is to use FIND with a datatype, to position to the first refinement! argument:

>> find words-of :append refinement!
== [/part length /only /dup count]

Great! But what about functions which have no refinements at all, like + shown above?

>> find words-of :+ refinement!
== none

To counter that, we simply add a sentinel value at the end, so that we can ensure that there'll always be a refinement! present:

>> join words-of :+ /sentinel
== [value1 value2 /sentinel]

>> find join words-of :+ /sentinel refinement!
== [/sentinel]

For our final step, we utilise the fact that Rebol blocks are "cursor-like". FIND does not return a copy of the original block with just the remaining elements after and including the match, FIND returns a new block "cursor" which is positioned right after the match within the original data. We can use INDEX? to ask this "cursor" its current position:

>> index? find join words-of :+ /sentinel refinement!
== 3

As we can see, this expression using INDEX? gives us the (1-based) position of the first refinement. Subtract 1 from that, and we have the count of non-refinement arguments. (If we'd have zero-based functions already, we could get rid of that final step.)

Et voilà:

>> -1 + index? find join words-of :+ /sentinel refinement!
== 2

I hope you enjoyed the ride :)

\$\endgroup\$
1
  • \$\begingroup\$ Seems to work great in Rebol 2 as well. I like your answer, it's more functional/concatenative; I don't need to iterate and use local variables. A bit harder to reason about. I think I'll accept HostileForks answer, but thank you! \$\endgroup\$
    – Torbjørn
    Mar 30 '15 at 7:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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