25
\$\begingroup\$

I have made the bold claim that a longstanding problem in Rebol is "now solved"...that of "definitional returns".

But of course, such claims need some peer review, and there's always some new trick to learn or improvement to make. So I thought I'd paste some code here. Even though it doesn't tell the full story of what I mean when I would say something like "solved"...it can be critiqued anyway.

The best document overviewing what the heck a definitional return is is on a site that's down right now, but a copy is on the Internet Archive. It's really for a Rebol audience, and at the risk of turning this into a blog I'll just link to another answer and say:

Rebol's model of being able to implement customized control constructs depends on something called definitional scoping. There is an invisible (though queryable) property that various symbols get called a "binding" that gets set at "definition" time.

This invisible binding connects the symbol to its context...allowing it to act as code and be executed in remote environments without getting confused. e.g. just because you're writing a routine that has a symbol FOO: defined in it and someone passes in a block of code that references a FOO, there won't be confusion on which is which. The ability to pass code around in this way lets one write one's own loop constructs or do other things that make tools that seem like they came "in the box" as much as anything else.

That works fine for variables, and for most functions. But RETURN is a tricky one, because there was only one global function that performed the action. Even though definitional scoping correctly preserves the binding to that return in both the authoring context and the context passed to, that doesn't help because the person meant "return from me" not "return from you".

Below is a primitive "FUNC" function constructor--with a finessed implementation as native code now merged to master in the Ren/C fork of Rebol3. It is based on the idea that there is no such thing as a global RETURN... so the native MAKE for FUNCTION! is unaware of the concept. It's up to the generator or generators to define the return you want.

Commented below. Note that it uses Ren/C improvements such as UNLESS being not set if the body is not run, to "opt-out" of the COMPOSE:

func: make function! [[
    {Defines a function with given spec and body, and definitional RETURN}
    spec [block!] {Help string (opt) followed by arg words (opt type, string)}
    body [block!] {The body block of the function}

    ; Ren/C's MAKE FUNCTION! uses SET-WORD to indicate "pure locals"
    no-return:
][
    make function! compose/deep [
        [
            ; SPEC

            ; We need to figure out two things:
            ;
            ; 1. Is the function <no-return> (e.g. no definitional return
            ;    function definition or CATCH needed in the body?)
            ;
            ; 2. If the function is *not* <no-return>, do we need to add
            ;    a RETURN: "true local" to the spec or is it already there?
            ;
            ; Since our COMPOSE/DEEP is splicing series, we will need two
            ; splices to build the new spec.  One splice is of the passed
            ; in spec (always) and the second for a RETURN: (...maybe)

            (
                no-return: any [

                    ; One way of having no return is if there is a literal
                    ; <no-return> tag in the spec being passed in.  This
                    ; tag is FUNC-specific... all MAKE FUNCTION!s are
                    ; "no return" and it would error if we passed in the
                    ; tag.  However, we can't just take it out because we
                    ; haven't copied the passed-in spec.  We only pay for
                    ; that copy in the event that transparent is found.
                    ; Note that <no-return> is expected to be very rare,
                    ; though useful when used (needed for COLLECT, USE)

                    ; UPDATE: The MAKE FUNCTION! generator has been updated to
                    ; tolerate ANY-STRING! being left residual in the spec, as
                    ; STRING! was tolerated before.  That was motivated by
                    ; realizing this could just say `find spec <no-return>
                    ; ...but it's the generator's choice to remove or not

                    all [
                        no-return: find spec <no-return>
                        spec: copy spec
                        remove at spec index-of no-return
                    ]

                    ; Another way of being told to be transparent is if there
                    ; is a RETURN, /RETURN, :RETURN or other word that is
                    ; *not* a set-word!.  The reason for exempting RETURN:
                    ; and going ahead anyway is that there's little harm in
                    ; having a preloaded value for a "true local" as it is
                    ; not overwriting any passed-in quantity... and it can
                    ; even serve as documentation and compatibility with Red
                    ; (when used under a certain idiom).
                    ;
                    ; We get our no-return decision and leave the spec
                    ; at the position of the set word.

                    all [
                        spec: any [
                            find spec 'return
                            tail spec ;-- let tail be fail vs. none...
                        ]
                        not tail? spec ;-- ...so still have spec intact!
                        not set-word? spec/1
                    ]
                ]

                ; We want the spec left at the position of a possible RETURN
                ; if that's what it is be positioned on a possible RETURN:, so
                ; we splice from the head position, yet leave spec where it is

                head spec
            )

            ; Now we have two reasons to not add the RETURN: set-word... if
            ; it's already there, or if we are transparent...

            (unless any [
                no-return
                set-word? spec/1
            ][
                quote return:
            ])
        ] (
            ; BODY

            ; Now for the body.  Compared to doing the spec analysis work,
            ; it's a lot easier to understand.  We either want just the
            ; plain body if we were transparent, or to pair up a
            ; definitional return with a named throw + catch if not.
            ;
            ; Named THROW/CATCH is fairly well understood at this point, but
            ; the interesting bit is that if we are definitional then the
            ; word RETURN (that we know is local) has its binding retrieved.
            ; That binding is the name of the throw and catch, and in the
            ; case of a function it will be the FUNCTION! itself (new
            ; feature, previously it returned TRUE for function locals).
            ; For CLOSURE! it will be the unique object associated with
            ; that invocation...as before.

            either no-return compose/deep [[[(body)]]] compose/deep [[[
                return: make function! [
                    ["Returns a value from a function." value [any-value!]]
                    [throw/name :value bind-of 'return]
                ]
                catch/name [
                    (body)
                ] bind-of 'return
            ]]]
        )
    ]
]]

Quick snapshot of the above in action...and of course, there's nothing special about using the name RETURN. You could call it whatever you like, give it any arity you liked, put the parts together different ways...it's a pattern for solving "return-like problems" :-)

>> foo: func [x] [
    print "yes"
    return (10 + x)
    print "no"
]
== make function! [[x return:][
    return: make function! [
        ["Returns a value from a function." value [any-value!]]
        [throw/name :value bind-of 'return]]
    catch/name [
        print "yes"
        return (10 + x)
        print "no"
    ] bind-of 'return
]]

>> foo 20
yes
== 30

...another example...

>> bar: func [] [
    help return
    print [newline "It even has help. :-)"]
]
== make function! [[return:][
    return: make function! [
        ["Returns a value from a function." value [any-value!]] 
        [throw/name :value bind-of 'return]]
    catch/name [
        help return
        print [newline "It even has help. :-)"]
    ] bind-of 'return
]]

>> bar
USAGE:
        RETURN value

DESCRIPTION:
        Returns a value from a function.
        RETURN is a function value.

ARGUMENTS:
        value (any-value!)

It even has help. :-)

This is a pure user-mode variant of the generator. To say definitional return is "solved" refers to the combination of the schematic along with a pure native simulation of the above code, and the lack of the need to build a keyword into the language (e.g. RETURN, which would run against the "no built-in keywords" goal).

Here is a very preliminary data point on their relative speeds at time of writing:

; Mezzanine definitional FUNC (exactly as above)
>> delta-time [loop 1000 [foo: func-mezz [a] [return a * 2] loop 1000 [foo 5]]]
== 0:00:03.46531

; Native definitional FUNC (simulates effects w/o boilerplate)
>> delta-time [loop 1000 [foo: func-native [a] [return a * 2] loop 1000 [foo 5]]]
== 0:00:00.800268

There's plenty of optimization to go on the latter number, but perhaps there are some ideas to throw in on the top one above...


UPDATE - In case all the commenting is in the way of reading it clearly or offering tweaks, here's a version without comments:

func: make function! [[spec [block!] body [block!] no-return:][
    make function! compose/deep [
        [
            (
                transparent: any [
                    all [
                        no-return: find spec <no-return>
                        spec: copy spec
                        remove at spec index-of no-return
                    ]
                    all [
                        spec: any [
                            find spec 'return
                            tail spec
                        ]
                        not tail? spec
                        not set-word? spec/1
                    ]
                ]
                head spec
            )
            (unless any [no-return set-word? spec/1][quote return:])
        ] (
            either no-return compose/deep [[[(body)]]] compose/deep [[[
                return: make function! [
                    [value [any-value!]]
                    [throw/name :value bind-of 'return]
                ]
                catch/name [(body)] bind-of 'return
            ]]]
        )
    ]
]]
\$\endgroup\$
  • \$\begingroup\$ (Note: If any readers on CodeReview haven't seen that kind of flexibility before and want to learn more, see Rebol and Red chat...) \$\endgroup\$ – HostileFork Nov 1 '15 at 22:08
  • \$\begingroup\$ This question has been around for a long time... BTW I do think // or ;; could be comments... \$\endgroup\$ – FreezePhoenix Sep 20 '18 at 13:35

Your Answer

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

Browse other questions tagged or ask your own question.