AUGMENT: Add Parameters and Refinements After-The-Fact

:boxing_glove: Don't call it a comeback... :boxing_glove:

AUGMENT is a new addition to the function composition toolbox. It serves a single purpose: to create a variation of a function that has more parameters and refinements, but acts exactly the same.

>> foo-x: func [x [integer!]] [
       print ["x is" x]
   ]

>> foo-xy: augment :foo-x [y [integer!]]

>> foo-x 10
x is 10

>> foo-xy 10
** Error: foo-xy is missing its y argument

>> foo-xy 10 20
x is 10

You might ask: "What good is that, since the original function has no idea the parameter is there?" This is where our friends like ADAPT and ENCLOSE come in.

Let's try that again. First, with an ADAPT:

>> foo-xy: adapt (augment :foo-x [y [integer!]]) [
       print ["y is" y]
   ]

>> foo-xy 10 20
y is 20
x is 10

And here's an ENCLOSE example:

>> foo-xy2: enclose (augment :foo-x [y [integer!]]) func [f [frame!]] [
       let y: f.y
       print ["y is" y]
       do f
       print ["y is still" y]
   ]

>> foo-xy2 10 20
y is 20
x is 10
y is still 20

This didn't drop out of the sky...

It is the result of long-term-thinking, and design choices with an eye toward doing this someday. One of the recent strategic moves that really made it feasible was changing refinements to be their own arguments. If it weren't for that, if your function was foo: func [x /y] [...] and you tried bar: augment 'foo [z], the parameter list would look like you had written bar: func [x /y z] [...]. As soon as a function got a refinement, you'd have no way to add a normal parameter...because everything would become an argument to the last refinement.

The very first motivating scenario which got me thinking about this was when I was lobbying for removing the /DEFAULT refinement from SWITCH. I felt strongly about needing a generalized solution based on NULL results from branching constructs. But I wanted it to be easy to make a compatibility version.

And now, it is easy:

 switch-d: enclose (augment :switch [
     /default "Default case if no others are found"
        [block!]
 ]) func [f [frame!]] [
     let def: f.default  ; see NOTE on why it's not `do f else (f/default)`
     do f else (def)
 ]

It works the way you'd expect:

>> switch-d 1 [1 [print "one" 1020]]
one
== 1020

>> switch-d/default 1 [1 [print "one" 1020]] [print "defaulting!" 304]
one
== 1020

>> switch-d/default 2 [1 [print "one" 1020]] [print "defaulting!" 304]
defaulting!
304

I'm not totally thrilled with the way the meta information for HELP is being inherited. But the somewhat hackish way it is done is working well enough to get us started. Note how the description for /DEFAULT was incorporated:

>> help switch-d
USAGE:
    SWITCH-D value :predicate cases /all /default

DESCRIPTION:
    Selects a choice and evaluates the block that follows it.
    SWITCH-D is an ACTION!

RETURNS: [<opt> any-value!]
    Last case evaluation, or null if no cases matched

ARGUMENTS:
    value [<opt> any-value!]
        Target value
    :predicate [refinement! action! <skip>]
        Binary switch-processing action (default is /EQUAL?)
    cases [block!]
        Block of cases (comparison lists followed by block branches)

REFINEMENTS:
    /all
        Evaluate all matches (not just first one)
    /default [block!]
        Default case if no others are found

This is a very new mechanism that is going to need testing. But it's going to make many things easier--not just in implementing things like Redbol, but also in being able to create skins with warnings about deprecated refinements (and what to do instead)...while removing those refinements from the natives themselves.

Try it out!


NOTE: Although I'm now of the belief that function arguments must outlive their calls, I don't believe this implies that do f should not invalidate the caller's handle on that f frame. It is an effective transfer of ownership of that frame to the function; and you need the feedback that you cannot expect another DO to work again. Hence the default must be cached.

2 Likes

Wow. Ren-C is becoming like the Queen of a Rebol chess board-- it can move any number of spaces in any direction.

Very cool feature to rollout for a Friday afternoon. Definitely a Keanu "Whoa!!"
So high!

1 Like

I would say these features on a technological level aren't more "impressive" than things you can do in...say...Haskell which are rather sophisticated, e.g. say Monads or Lens. (Also, things like that use a methodology that is structured enough that you can compile them; we aren't headed that way.)

...BUT... I think this could bring function composition techniques to the masses in a visceral way. I'll point out that these are just building blocks, so if people want to mash them up they can.

How about this? (Note: requires the indefinite lifetime commit to work.)

extender: func [original spec body] [
    let body-copy: copy/deep body
    enclose (augment :original spec) func [f [frame!]] [
        let execute: does [do f]  ; should work as just `does f`, will fix...
        bind body-copy 'execute
        bind body-copy copy f
        do body-copy
    ]
]

Now the body has access to all the locals of the original function, without having to go through f/whatever...because we bound into the frame for you. It makes a (shallow) copy of the frame so you don't have to worry about caching--the original arguments are available for the entire enclosure. And at the point you want to run the inner function, you just say "execute".

switch-d: extender :switch [
     /default [block!]
][
     execute else (default)
]

A new abstraction that hides the details, and you could wire up others as you wish...

>> switch-d 1 [1 [print "Amazed yet?"]]
Amazed yet?

>> switch-d/default 1 [1 [print "Minecraft" 1020]] [print "Of Programming" 304]
Minecraft
== 1020

>> switch-d/default 2 [1 [print "Minecraft" 1020]] [print "Of Programming" 304]
Of Programming
== 304
1 Like

Yessss, great. I will have to find uses for this.

1 Like