Should function arguments to functions be disallowed by default

Rebol2, R3-Alpha, and Red all protect against passing unsets to functions, requiring you to mark parameters that tolerate them explicitly. So instead of takes-void: func [x] [...] you have to write takes-void: func [x [any-type!]] [...] for takes-void () to work.

Ren-C tweaks this around a little. Firstly, ANY-VALUE! has replaced ANY-TYPE!, due to potential confusion over those who might think that in the pattern like ANY-SERIES! (instance of block!, instance of string!, etc.) that ANY-TYPE! might mean ANY-DATATYPE!... so the datatypes block!, string!, integer! etc. And secondly, ANY-VALUE! specifically excludes voids, as they are not values.

So the way to indicate an argument can take void is the <opt> tag. takes-void: func [x [<opt> any-value!]] [...]

But there is an additional twist, because FUNCTION! was then disallowed for parameters or return results unless you say ANY-VALUE! or include it specifically. I don't know how much good this has done in the scheme of things, but the rationale for trying it was to make it unnecessary for the average routine to have to bulletproof itself against function arguments.

Consider, for instance:

check-secret: func [guess] [
    secret: "abracadabra"
    print ["guess:" guess "actual:" secret]
    return guess = secret
]

tricky: func [whatever pass] [
    print ["HAHA YOUR PASSWORD IS" pass]
    quit
]

check-secret (get 'tricky)

The output of this is HAHA YOUR PASSWORD IS abracadabra.

I do not want to bemoan the concept of "security" here for several reasons. I'm just wondering if it's possible to make it a little easier to avoid getting functions when the callee doesn't expect them.

Let's look at what might be thought of as a "good use" of passing a function in:

value: none
count: 0
cacher: does [
     unless value [
         print "calculating!" ;-- imagine this were slow
         value: reverse "arbadacarba"
     ]
     print "returning!"
     return value
]

check-secret (get 'cacher)

That will output:

calculating!
returning!
guess: abracadabra actual: abracadabra
returning!
== true

Because check-secret uses guess twice, we see two calls. But it only does the calculation the first time. So here we get a trick, that because guess has no special syntax to say it is-or-isn't a function call, it was able to handle being either. It's being cooperative, though--expecting that the caller wanted it to act "value-like".

But the picture gets a bit complicated when GET (or a GET-WORD!) is used. Consider this:

 something: func [arg key [word!]] [
     word: either some-condition ['arg] [key]
     return get word
 ]

Here we see a code author who tries to abstract out the place to get their value from. They'll either get it from the arg parameter, or from the word passed in. This won't work if arg is a function and the intent the caller had was for something to invoke the function and used a call of it as if it were a value.

Question: How's a Function's Parameters Different from Any Object or Word?

So basically, let's say I have a function foo: func [bar] [...] and I know bar is not a function, but it might be a block or an object, and I write something like bar/:baz, or maybe :bar/:baz. All the same issues seem to apply, bar might be a block with a function in it or not. Maybe it's a MAP! with a function in it. There's only one level of protection provided by guarding the function arguments from being functions...and it's going to need to be overridable anyway.

So given that, is it worth the inconsistency? In practice, how many times do people actually try passing functions as arguments when they weren't expected?

When @earl weighed in on the topic long ago, he said he probably made more functions-that-take-functions than most. The casual convenience of not having to mark them as such helped for quick-and-dirty programming. This isn't a defense of the idea that one can easily write a function that didn't expect a function argument will have any chance of handling it intelligently. Just that "well, what else can it handle intelligently anyway? so why not let the people who want type signatures pay the cost, they really should be putting type signatures on anyway if they want the routines to be any good".

I have previously discussed this with @hostilefork before - https://github.com/metaeducation/ren-c/pull/481#pullrequestreview-34974129

I think this is an important feature that @hostilefork has added here.

I think the choice for community is whether this should be....

  • on by default?
  • show a warning instead of runtime error? (bit like Perl's use warnings, which is a pragma you can turn on/off or even force warning into runtime error)
  • set by command-line switch (eg. part of perhaps? ./r3 +s) or in Rebol header (eg. Rebol [secure: yes]?

So one step back on the increase in strictness is that I've made it so functions no longer require a RETURN: annotation to return NULLs or ACTION!s.

The key motivator for doing this right now is the "de-stigmatization" of NULL. With so many actions returning them now, writing simple wrapper functions becomes complex.

But it was already hanging by a thread, anyway. Over a fair period of usage, I feel like the return result checking probably was creating about as much headache as it was catching any bugs. The upside of any annoyance it brought is that at least a lot more functions wound up documenting their return types!