In the interest of shipping something: Rebol2-style `<local>`s only?

I pretty much consider being able to declare variables at the point they are first used to be a non-negotiable feature.

This is why I was a big fan of FUNCT when I first saw it. It looked for SET-WORD!s in your function and "gathered" them up implicitly as being local to that function.

 funct [arg1 arg2] [  ; R3-Alpha "funct"
     local1: 10
     local2: 20
     return (local1 + arg1) * (local2 + arg2)
 ]
 => func [arg1 arg2 /local local1 local2] [  ; equivalent R3-Alpha code
     local1: 10
     local2: 20
     return (local1 + arg1) * (local2 + arg2)
 ]

I was one of the proponents for it taking over the relatively-useless role of FUNCTION, which was previously a fairly lame contraction like this:

function [arg1 arg2] [local1 local2] [...body...]   ; Rebol2 "function"
=> func [arg1 arg2 /local local1 local2] [...body...]  ; equivalent Rebol2 code

As you write more sophisticated code, you start to realize the weakness of automatically assuming SET-WORD!s are local. There are many uses of SET-WORD!s that are not implied as being local variables, and if dialect authors are to be free with using them as labels or other purposes then there will be many more.

I've written about the problems - Please Review if you haven't already.

I've experimented with LET as a replacement, but I'm not happy yet.

Right now, LET works mechanically about the same as SET-WORD!s did. let x: 10 behaves in a FUNC or FUNCTION body just as x: 10 did in a FUNCT(ION).

It's an improvement in the sense that it sacrifices a single word--instead of the whole class of values of SET-WORD!--for carrying the meaning of declaring a local.

There are questions about how this can interact with PARSE or other dialects, which had expanded their syntax e.g. to allow copy x: [some "a"] instead of just copy x [some "a"]. Should that now enable copy let x: [some "a"]? Who is responsible for these dialect syntax exceptions, where any point of providing a variable to assign might want to also declare that variable?

Beyond that... I have a nagging feeling that the way it works is wrong...injecting variables into the function definition and being scanned for. I feel like it should work more like a USE does, and be dynamically binding the code that comes after it when it is encountered. If you want to add variables to a function's definition you should always be able to do that with <local>. But something tells me that LET should be a different beast...that uses a syntax trick to create a wave of binding that affects the statements after it instead of forcing your hand in making a code block the way USE does.

Can we punt on it?

Rebol2 got a fair way with just /local. I think it would have been safer if it had unbound any SET-WORD!s in the body that were not explicitly declared or imported.

One of the main reasons this would be a pain would be OBJECT!s that would want methodized access to things without explicitly <with>ing them. But Ren-C has METHOD to take care of this (and establishing the relationship to the object addresses other design holes, e.g. not having to deep copy and rebind all the functions in objects on each instance creation).

What if we went to a situation where you used <local> for now, and all SET-WORD!s were unbound that weren't covered by either the args or <local> or imported by <with> or on an object supplied by <in> (or implicitly via METHOD)? We could limit the "junk" which might accrue by having it so that <local>s which do not have corresponding SET-WORD!s in the body raise an error.

I'm not floating the idea because I don't believe in being able to declare locals at their point of first usage. I'm suggesting it because I believe in the idea too much to see it done incorrectly, and what I've seen so far feels wrong.

Thoughts? I do like the idea of FUNC and FUNCTION being synonyms, and would like to stay the course with that direction.

Sounds fine to me. I trust your intuition. I think you're saying you don't know what the answer is yet, and putting that on the back-burner for now sounds completely reasonable.

1 Like

I tend to use FUNC for the most part anyhow as I feel the naming of locals is an important part of understanding the function as it's being written.

I'm a bit ambivalent on METHOD as I think having functions be local to their object be the default, though I understand this is not always the case (see the classic example of object usage in the Ten Steps doc). I sense that the desired non-copying behaviour of functions came from the usage of OBJECT! that MODULE! now covers. Could use more data points on that.

My feeling on functions in general is that they are effectively blocks with a spec, and that FUNC should reflect the native spec dialect of ACTION! (i.e. func: [spec body][make action! reduce [spec body]])—that seems to be a little out of whack at the moment. This is with an eye to having some form of representation of function ([spec]:[body] or [spec]|[body] are spitballs with the ']:[' sequence being the key designation) where it's implied that the body behaves like BLOCK! in terms of obtaining context.

I'm not sure I get how LET works—is it a nothing pass-through word that is just used as a marker for locals gathering or is does it have meaning within FRAME! ? For me, it seems foreign.

Writing a bunch of functions that communicate state with each other via global variables without requiring them all to be put inside an object seems like a reasonable desire, and is/was a common pattern in R2.

This is what <with> does, and I think being explicit about it isn't too much of a problem. You only have to use it if you intend to write to the variable:

 global: _

 writer: func [x <with> global] [
     global: x
 ]

reader: func [] [
    return global
]

Yes; it is an "invisible" (no return result). It takes a skippable WORD!, so that if the thing that follows it is a plain word it will be consumed silently.

let x  ; will just remain as VOID!, no error raised

But a SET-WORD! or SET-BLOCK! or whatever will not be consumed as an argument, hence it is not subject to the invisibility:

>> print ["The let is invisible, but value is" let x: 10]
The let is invisible, but value is 10

I don't think it's particularly mysterious, but there are issues. e.g. whether to enforce not using a LET'd variable before the LET, and how that might work. (See also "hoisting" in JavaScript for difference between VAR and LET)

 foo: func [] [
    x: 10
    print ["Should this be legal?" x]
    let x: 20
    print ["Or only after the LET?" x]
]

There's the question of if dialects are responsible for adding support for LET. I mention the early change that allowed SET-WORD! in COPY and SET for PARSE...e.g. parse data [copy x: [some "a"]]. Does it need to support [copy let x: [some "a"]] now? Does every dialect need to be involved?

I'd considered other ideas for LET's behavior, e.g.

>> let x
== x  ; the variable itself

Which would permit the likes of parse data [copy (let x) [some "a"]], modulo our debates over plain GROUP! injections.

But like I say: the collecting SET-WORD!s is a fairly unsustainable tactic. And a "scanned for in the body" construct has the same problem of inadvertent locals:

 outer: function [x] [
     let inner: function [y] [
         let z: x + y  ; Both OUTER and INNER pick up this Z in body walking
         return z
     ]
     return inner
 ]

If LET had more of a runtime character... like USE... then only those bodies that actually needed the variable would get it. This is what I mean when I say I'm looking for a pleasing answer that hasn't really emerged. That unused Z in OUTER is symptomatic of a design flaw I want to see a way out of.