METHOD and the argument against PROCEDURE


#1

I’ve an idea that METHOD be a left-quoting version of FUNCTION…looking for a SET-WORD! or SET-PATH! on its left, and doing the assignment. The reason it would do this would be so that it had access to the word to read the binding out of it. That means it can look to see what fields were in that object, to solve a problem today with using FUNCTION in objects:

obj: make object! [
    x: 10
    f: function [] [
        x: 10 ;-- would assign a local, since it's gathered
        y: 20 ;-- would also assign a local
    ]
    f2: function [<in> obj] [ ;-- today's workaround
        x: 10 ;-- would assign obj/x
        y: 10 ;-- would assign a local
    ]
    m: method [] [
        x: 10 ;-- would assign obj/x, since it's in binding of `m:`
        y: 20 ;-- would assign a local, not in binding of `m:`
    ]
]

This seems like a pretty darn cool idea to me. The left quoting gives METHOD the missing piece of the binding it needs, so you don’t need the workaround.

Cool as it is, it makes me wonder about the naming situation of having a PROCEDURE form. If method needed a parallel to that, what would you call a method that doesn’t return a value? PROCMETHOD? :-/

This leads me to feel that maybe PROCEDURE’s semantics (0-arity “return”, kill any result that might drop out) should be something you can indicate in either kind of spec easily. And with the new VOID! type, we might have a coherent answer. So…

Is there any really good reason why f: function [return: <void>] [...] couldn’t be the signal to act like PROCEDURE does today, drop LEAVE, and just make RETURN not take an argument when that’s that signature?

When definitional returns were being created, I was a bit worried about the idea that you might accidentally use a 0-arity RETURN with an argument. But we are starting to need a test for “residual” execution anyway, to catch cases where you’ve got non-invisible content following something that RETURNs or throws, else we can’t catch the “dark corner” of return if x [...] else [y]. So why not take a cue from C and just have a 0-arity RETURN in these cases?

People can still define PROCEDURE if they like it:

 procedure: func [spec body] [
     function compose [return: <void> (spec)]
         compose [leave: :return (body)]
 ]

To me it seems worth it to standardize so that METHOD can get the feature of auto-suppressing the result falling out of the bottom without needing another name, and it keeps from having to come up with two names for everything that wants to be function-like but do that.

There’s technically no real reason why having this ability for FUNCTION needs to affect PROCEDURE. It just may be more of a smooth transition to using METHOD to not teach people that a different action generator is how you get no-result. Also, we could still say that return: <void> gives you a LEAVE instead of an arity-0 return if we wanted to. It’s perhaps no more of a non-sequitur than it was with PROCEDURE. That’s a separate decision… anyone have an opinion?


Letting Go of Enfix Path Dreams (but fixing it with "Magic")
#2

I can’t recall ever using procedure. A quick count of the old scripts at rebol.org shows that I am not alone (16 references in total).
method is a nice idea indeed.
Good stuff :slight_smile:


#3

I didn’t catch the reason why we had to have procedure, just had to change all my non returning functions to procedures.


#4

The idea of PROCEDURE was that it didn’t have a result, and it would automatically stop unintended results from “falling out” of a function, thus helping avoid unwanted expectations by callers.

Let’s say I might have code like:

emit: function [data] [
    append accumulator data
]

Then say people use it, but then eventually one day someone would notice that it returned the accumulator. They might then take this for granted.

But you might change the emit function, feeling you are completely in your right to do so:

emit: function [data] [
   write-log ["Time to emit was:" delta-time [
       append accumulator data
   ]]
]

So those callers who had been using your unintended result would now be screwed. So getting people in the habit of using PROCEDURE when they didn’t intend to return a result seemed like a good thing.

just had to change all my non returning functions to procedures.

Obviously I wasn’t stopping all of the cases by erroring on null results. I couldn’t proactively error on the EMIT example above, as it returned a seemingly-reasonable value from the APPEND. But if write-log then didn’t return a value on one of its branches, it was likely that it didn’t intend to return a value on any of them. So it was semi-random enforcement: if you ever caught a function ending in PRINT or something, odds were extremely high it contractually never intended to return a value…and could use that moment to help you annotate it with that fact.

This was assumed to be enough to get people in the habit of thinking about whether they were writing something returning a value or not. And I think that’s a good idea.

But I was more or less forced to revert that when NULL took over the role of BLANK!. I realized that NULL was just too common and too interesting a return result to do this to.

Yet now, with VOID! coming back and not being able to be assigned via SET-WORD! or SET-PATH!.. that seems like error enough…good enough to let the calls error instead of hassling the author of the function.

Again: my concept here is that return: <void> have the semantics of PROCEDURE. In today’s code, that means any value that tries to falls out of the bottom of a function with that annotation would get wiped out and replaced with a VOID!–not NULL. That’s different from return: [void!]:

>> foo: function [return: [void!]] [1 + 2] ;-- typecheck void, arity-1 RETURN
>> foo
** Type Error: you said you'd return void but didn't

>> bar: function [return: <void>] [1 + 2] ;-- force to void, arity-0 RETURN
>> bar
;-- appears to give no result in the console, since that's how VOID! is treated

You use return: <void> or return: [integer! void!] or return: [<opt> any-value!] type annotation, depending on what you intend…they’re different.

So your work replacing things with PROCEDURE isn’t wasted, it just points out places that you would need to change to function [return: <void>]. At your liesure, as there will be a compatibility PROCEDURE for a while.

The compatibility PROCEDURE may not exist indefinitely, though, which is part of the question here.


#5

Well, let us know when you comitt the change, and if you would provide examples. I think it makes sense to set the options in the spec rather than have many different variations of the same thing.


#6

It may even solve another big question, of how the whole process of derived binding gets started…

I’ve explained derived binding before. I mention that what happens is that there’s this previously-unused pointer in an ACTION! cell…which is now allowed to point at an OBJECT!. The idea is that at the moment of derivation, e.g. make parent-object […], the process of copying the cells from the parent object to the child object looks to see if any an ACTION! value in the parent was bound to the parent (or any object less-derived than the parent, but in the same inheritance chain). If it is, then the copied ACTION! gets its binding pointer updated to the new derived object. Execution of that ACTION! instance can then weave in the binding pointer with running the body so it doesn’t have to copy it to have the derived references forward.

But what I left out was how ACTION!s get their binding pointers set in the first place, to kick off this process. The uneasy thing I did was just assume it wouldn’t hurt if you had ACTION!s with null pointers to just stick the pointer of the object on it during the make…

but it can hurt, because I just mentioned that having one of these pointers means you’re adding the cost of doing derived binding checking–even if there’s nothing in there it applies to. And it felt really ad hoc anyway. Now MAKE OBJECT! can just ignore functions with null bindings…they aren’t methods! Their bindings meant what they said, and stay fixed.

parent: make object! [
    x: 10
    print-x-alpha: func [] [print x]
    print-x-beta: method [] [print x]
]

child: make parent [x: 20]

>> child/print-x-alpha
== 10

>> child/print-x-beta
== 20

But it’s not like METHOD has any “magic powers”. You could do the same with your own abstractions, or bind the ACTION! manually.

parent: make object! [
    x: 10
    print-x-gamma: func [] [print x]
    bind :print-x-gamma (context of 'x) ;-- see note on SELF
]

child: make parent [x: 20]

>> child/print-x-gamma
== 20

So you get both your method features…binding in the object, and the forwarding in derivation, from METHOD…and you can build new METHOD-like things yourself if you get ideas for them.

Looks to me like another likely victory point for Ren-C. Nice, indeed. :slight_smile:


(Note on “SELF”: I’m still leaning to try and figure out how to layer it in such a way that a “low-level” routine like MAKE OBJECT! doesn’t claim the word SELF, just as MAKE ACTION! doesn’t know about RETURN. So it would be higher level things like CONSTRUCT which would introduce that. It’s still percolating. But if it existed, it would be a synonym for getting the context of any members of the object, most likely itself (context of 'self) in case the object was otherwise empty! This same technique is used by RETURN, it ties itself to an unwind of the FRAME! it gets from (context of 'return).)


#7

Committed, though I don’t know what example you’d need beyond what’s in the commit message. It’s just PROC and PROCEDURE behavior, triggered in the spec instead of by separate names…and gets rid of LEAVE by assuming you can deal with the fact that sometimes RETURN is arity-0 and sometimes arity-1, depending…

Yes… well the spec dialect stuff has been evolving and though it has some fuzzy areas, I think it’s a lot better than the old /EXTERN and /WITH and such. Putting things in the spec language can make it a little bit weirder to process.

In fact, making the compatibility shim is a good example of how you have to sort of process the block even on simple things. But it’s also really neat:

procmaker: function [
    return: [action!]
    generator [action!] spec [block!] body [block!]
][
    generator collect [
        pending: [return: <void>]
        try-inject-return: func [item [<opt> any-value!]] [
            if pending and (not text? :item) [
                keep was pending: _
            ]
        ]
        for-each item spec [
            try-inject-return :item
            keep/only :item
        ]
        try-inject-return () ;-- in case spec was empty or all TEXT!
        keep [leave:] ;-- define local
    ] compose [
        leave: :return ;-- `return: <void>` makes RETURN 0-arity
        (body)
    ]
]
proc: specialize 'procmaker [generator: :func]
procedure: specialize 'procmaker [generator: :function]
unset 'procmaker

Being able to make these changes with such fluidity and have the system work, is really a testament to the flexibility of the system!


#8

METHOD is now added:

method: enfix func [
    {FUNCTION variant that creates an ACTION! implicitly bound in a context}
    return: [action!]
    :member [set-word! set-path!]
    spec [block!]
    body [block!]
    <local> context action
][
    context: binding of member else [
        fail [member "must be bound to an ANY-CONTEXT! to use METHOD"]
    ]
    set member bind (function compose [(spec) <in> (context)] body) context
]

Straightforward and elegant:

  • Extracts the binding of the SET-WORD! or SET-PATH! it quoted on the left. Note that MAKE OBJECT! applies these bindings to top level words in its block before the code runs.

  • Delegates to FUNCTION to make the action, but tacks its own contribution onto the spec…the context to bind in. (Note: Any <local> will out-prioritize this; which probably makes sense. But with the order of binding, any other <in>s would be overridden by the method. This can be reviewed as experience with it is gathered.)

  • BINDs the newly created ACTION! value itself to the context. This is critical to derived binding; since the body of the method does not change on derivation, it needs some way to make each derived object “uniquely stamp” their instance. The stamp is updated during derivation–if the derivation is applicable. Here all we’re doing is kicking off the process by saying “associate this ACTION! value with this instance of the base object”.

  • SETs the SET-WORD! on the left to this new ACTION!. Since the set-word was quoted and passed into METHOD, it’s METHOD’s job to do the assignment if it wants an assignment.

The result of SET is just the value passed in, so that’s given back in case the value is being used… so x: y: method [...] [...] will give X and Y the same values (though the binding of X: wouldn’t be taken into account, only the Y: that gets quoted).

And making it all even more nice is the stellar performance curve of creating lots of derived objects, due to derived binding!


How to avoid getting a METH habit
#9

So, I often have constructs like this

 obj: make object! [
        password: "abcd"
        username: "Graham"
        m: method [] [
            password: :password ; will it get this from obj?  or produce an error?
            y: 20 ;-- would assign a local, not in binding of `m:`
        ]
    ]

So, it does grab the correct value

obj: make object! [ 
    password: "abcd" user: "graham" 
   m: method [][password: :password y: 10 join-of password form y]
]

>> obj/m
== "abcd10"

#10

Yes, that is what it does.