Should we kill off defaults-in-function-specs via GROUP!

UPDATE: Hmmm, it seems that this feature is needed for <static> variables, because they need to be initialized once, hence can't be done every function run. So long as it exists for statics, we might keep it. But I will say that the question of where to put them...and the fact no one is using these... raises questions about the feature that should probably be answered.


There was a speculative feature for putting defaults in function specs, by using a GROUP!:

foo: function [
    x [integer!]
    /amount
    y [integer!] (10)
][
    x + y
]

One key concept was that if it were in the spec, then some amount of automatic documentation could be provided from it. But also it meant you could save yourself mentioning the name parameter twice, in the spec to name it and the body to default it.

But what turned out to happen is that it gets pretty much lost in the noise, when you have string-based parameter documentation. It's easy to overlook, and raises questions of whether you want any relevant evaluations in the GROUP! to run once or every time.

x: 100
foo: function [
    x [integer!]
    /amount
    y [integer!]
        {The integer to add, hey where do we put that default now?}
        {Does it go before or after these documentation lines?}
            (10 * x) ;-- is that relative to the parameter or to the global?
][...]

Not only that, if you want to COMPOSE function specs you run into the problem of dealing with any GROUP!s in it. So not having GROUP!s have meaning keeps you from ending up with problems with escaping there.

UPDATE: Labeled compose has pretty much solved this problem if it comes up. So I don't think it's a concern anymore.

In the meantime, we've had the pretty infix default operator come along. It gives a nice enough and unambiguous experience. You can even do your defaulting inline if you like:

foo: function [
    x [integer!]
    /amount
    y [integer!]
][
    x + y: default [10]
]

There was no particularly efficient trick for storing the values or putting them in the frame...all it was doing was putting code in the body of the function, just like having a bunch of DEFAULT lines up front. The unimplemented automatic documentation feature raises a lot of questions...if it was calculated, do you want the calculated value or just what was in the GROUP! raw? :-/

Add it all up, and the GROUP!-to-default-a-parameter looks like a turkey. So should people just learn and love DEFAULT and its good friend MAYBE, and we get rid of this?


Pursuant to my update, here's a real world example to ponder from @codebybrett's READ-DEEP:

read-deep: function [
    {Return files and folders using recursive read strategy.}
    root [file! url! block!]
    /full
        {Include root path, retains full paths vs. returning relative paths.}
    /strategy
        {Allows Queue building to be overridden.}
    take [function!]
        {TAKE next item from queue, building the queue as necessary.}
][
    take: default [:read-deep-seq]
    ...
]

Let's imagine we want to move that default into the spec. Where?

read-deep: function [
    {Return files and folders using recursive read strategy.}
    root [file! url! block!]
    /full
        {Include root path, retains full paths vs. returning relative paths.}
    /strategy
        {Allows Queue building to be overridden.}
    take [function!] (:read-deep-seq)
        {TAKE next item from queue, building the queue as necessary.}
][
    ...
]

I sort of feel like it vanished into the noise. The code is shorter, but I don't feel it's clearer. Putting it on its own line is its own line seems a little weird:

read-deep: function [
    {Return files and folders using recursive read strategy.}
    root [file! url! block!]
    /full
        {Include root path, retains full paths vs. returning relative paths.}
    /strategy
        {Allows Queue building to be overridden.}
    take [function!]
        (:read-deep-seq)
        {TAKE next item from queue, building the queue as necessary.}
][
    ...
]

If one is writing a quick-and-dirty function with less documentation, it might be clearer...people might find it expedient. This is why it works okay for <local>s and <static>s

read-deep: function [root /full /strategy take (:read-deep-seq)] [
    ...
]

I actually am using it (though not in public code).
I find the documentation feature is better this way, than with defaults in the function.

1 Like

Do you have an opinion on where it should go? Before documentation comments? After?

Rebol2/R3-Alpha were agnostic about whether you put strings before or after the types:

foo: func [
    x {This was legal} [integer!]
    y [integer!] {So was this}
][...]

So far Ren-C has carried over that policy...though I'm not sure I like it being that flexible. I have a very strong preference for the types first.

UPDATE: I've changed my mind on this and now prefer descriptions first. But the fact that I could see it either way suggests that either way perhaps should stay legal.

The defaults I don't have a clear feeling about, because it looks noisy to me no matter what.

I, too have a preference for types first, but I am not sure whether there has to be a policy about it.

How about folding the default value into the type block?

foo: func [
    /set
    x [integer!] (1) "The x"
    y [integer! (1)] "The y"
][...]

So a thought I'm getting is that a SET-WORD! is a necessary part of the equation for defaulting. Let's put aside the other arguments I've already made regarding things getting lost with the help strings. Even this just doesn't look right:

foo: function [
    /some-refinement
    :hard-quoted-thing (first [bar mumble])
][...]

And with statics, absence of a SET-WORD! looks bad too:

foo: function [
    ...
  <static>
    data ([a b c])
]

Doesn't it come off much better when a SET-WORD! is used??

foo: function [
    ...
  <static>
    data: ([a b c])
]

This is giving me a revised opinion of the role of SET-WORD! in function specs--as being reserved for relating to assignment.

That doesn't bode particularly well for return: [integer! text!]...and may suggest it should be <return> [integer! text!]. One thing I will say is that it is a very common mistake for me to make to just say return [integer! text!] and have to figure out what I did wrong, while having the TAG! there is a pattern that is a less easy pattern to break.

So: I think the muddiness of defaulting refinement arguments within the specs (especially since they can't take on this SET-WORD! behavior) is at issue. Also, the sequence of defaulting refinement arguments--since they come from the function instance--would likely want to be after the statics in runtime order, since they are specific to the instantiation. Yet if they were part of the spec, they would appear before the statics. That ordering is hard to relate to.

All told, I think defaulting for non-locals and non-statics needs to get the axe, here. :-/

UPDATE: In light of new circumstances, and them not being taken out six months after me saying it, I no longer think this is so obvious. But maybe it's still not going to make it as a Beta/One feature.

So it's been 3 whole years since this was discussed, but something actually has changed about the nature of the implementation of functions to where this can actually be an efficiency benefit.

Basically, there is no difference now between locals and specialized-out arguments. This is because in the function specification block, I'm using the space where TYPESET! definitions for an unspecialized parameter would be to store specialized values.

In other words, the specializations and type definitions are folded into the same array...since the specialization value is type checked at specialization time the slot is available for the value itself. It's a cool trick.

And it means that we can actually set locals in the frame itself to initial values besides ~unset~.

This only works if the value is fixed. So for example:

>> a: 1020

>> foo: func [x <local> y (a)] [print [x y]]

>> foo "before"
before 1020

>> a: 304

>> foo "after"
after 1020

It would have to capture the value as-is. Anyway, it's not like this is a super huge priority I'm just pointing out that there is a new efficiency which comes from the unification of locals and specialization that wasn't there before, so this can actually offer a boost that DEFAULT in the body would not have.

But the value would have to be fixed in the frame, like a specialization.

And statics don't have any other option but to be initialized once, so it's consistent with that.

2 Likes