The Grand Leading-Slash "Safety, or Burden?" Question

Overall, I have been tremendously happy with how the ideas of the Big Alien Proposal have worked out.

That started from the concept that when slashes appear, they either come before a function they run, or after a function they suppress execution for.

foo.bar
; ^-- foo is an entity from which BAR is being selected.  BAR is not
; allowed to be an antiform frame, so this syntax cannot invoke a
; function call (though it can invoke an 'accessor', e.g. a "getter"
; which is 0-arity).

foo/bar
; ^-- foo is an entity from which BAR (an a FRAME! or antiform FRAME!)
; is being selected and then invoked.  This will generate an error if
; bar is not a frame or antiform frame.

foo.bar/
; ^-- bar is a field which is an antiform FRAME!, whose execution is
; being suppressed.  This expression will return an antiform frame, or
; an error if not an antiform frame.

foo
; ^-- conventional WORD! reference, will run an antiform frame as an
; action invocation or fetch other values as-is

/foo
; ^-- invocation reference, will run an antiform frame (or plain frame)
; as an action invocation and give errors on other types

foo/
; ^-- action suppression, will give you back an antiform frame as-is
; and error on other types.

(If you're curious about why /foo will run plain FRAME! as well as antiform, while foo/ will not return an antiform frame for plain FRAME!, this is based on the idea that it's better to be conservative when fetching values so that you won't get surprised by getting a plain frame back from foo/~ which gives everything back as-is.)


I've written elsewhere how pleased I am that the way you suppress a function's execution is by throwing up a "barrier" with a separating slash that makes it clear arguments are not being gathered at the callsite. That's really slick.

For this idea to work, something else had to be used for refinements. That meant invention of the CHAIN! datatype has opened a lot of interesting doors, and I find it's quite learnable to see things like trim:auto:tail instead of trim/auto/tail.

I actually prefer it! What some might think of a disadvantage of being "less noticeable" turns into an advantage... trim:auto really could have been a function called trim-auto just as easily. Why would you want a slash to make the fact that it has a refinement "pop"? The slashes to make function calls or suppression pop are much better applied.

So that's all good. :smile_cat: No regrets!

But... Leading-Slash For Functions Rule Hasn't 100% Gel'd

Another part of the proposal was that in order to get tighter control on what was a function or not, you would be required to assign functions using a leading-slash kind of SET-WORD!.

>> foo: func [a b] [return a + b]
** Error: FOO: can't be used to assign antiform FRAME!, use /FOO:

>> /foo: func [a b] [return a + b]
== ~#[frame! "foo" [a b]]]~  ; anti

It hasn't fully settled with me after working with it for some time.

As I mentioned above, colons for refinements was easy to adapt to...and now that I'm adapted, I prefer it.

But I'm still typing test: cascade [add/ even?/]. I love the trailing slashes (and this will be even better when the whole cascade can be done with just even?/add/). But I'm kind of cursing under my breath the thought of having typed test: and having to backspace over it so it says /test:. And then I go "hrmph."

When I'm reading code, I probably appreciate it more than I find it to be "messy". It gives you a better compass. The eye can scan and comprehend much better... it's of particular value when you're not using an obvious function generator like FUNC, but something else. This cues readers to go "oh, I guess that's a function generator".

Yet still... it's a burden in a way the other changes are not. It's the only change that increases the character count.

What's At Stake By Not Enforcing This?

Ren-C has a powerful story about how antiforms can't be put in blocks, which means you can write this kind of code and it "just works":

block2: collect [
    for-each 'item block1 [keep item]
]

assert [equal? block1 block2]

When you compare it to Rebol2/R3-Alpha/Red, it's one of those vastly superior situations. You aren't getting tricked into receiving an ITEM in the FOR-EACH that would generate an unset variable error, or conflate with the state that gets returned when an item can't be picked from a block, or accidentally run a function. It's a solid solution.

But that's only for blocks. What about other places, like objects?

If we don't put barriers on how action antiforms get assigned to variables, we get the problem all over again:

for-each [key value] obj [
    if integer? value [  ; oops, what if VALUE is an action antiform!
        print "Found an integer"
    ]
]

There's no way in this case to say "variables can't hold antiforms". Logic is an antiform. Words holding antiform frames are actions.

Getting this under control with slashes is the kind of thing I've been trying to do for a long time, I've just never had the syntax. Leading slashes felt like it could be the key:

for-each [key value] obj [...]  ; value can't be frame antiform

for-each [key /value] obj [...]  ; value must be frame antiform

for-each [key ~/value] obj [...]  ; value may be frame antiform

But if these rules are applied everywhere, what you have to do gets more complex:

set $x does [print "Is this an error?"]

set $/x does [print "Do you have to do this?"]

>> var: $x
== x  ; bound

set var does [print "If this errors, how to make VAR into bound /x?"]

set:active var does [print "Do you use refinements?"] (or just SET:ANY ?)

Nothing is free. And the already more complicated world where x: is a CHAIN! instead of a fundamental different type of word has its own issues, that these all pile on top of.

There's Likely Not Enough Value In Optional Slash

If /foo: func [...] [...] will enforce that the thing you're assigning is an antiform action, but foo: func [...] [...] still works... I have a feeling that the complexity it takes to offer the feature doesn't give a sufficient payoff to be worth it.

You have everyone paying the tax of dealing with complicated path structures and bookkeeping--vs. being able to just SET and GET words and tuples at will... and then you're not even giving any additional guarantees in the source.

This makes me feel like it really is an all-in or not-at-all situation.

Long Story Short: I'm Still Weighing It

I'm not ready to make a verdict.

The techniques for working with these new CHAIN! and PATH! situations are still being learned. Most of my hesitance isn't from the looks or typing an extra character, but from frustrations in that...and maybe that frustration will lessen as I work on it more.

2 Likes

If it can't settle with me, it will certainly put others off, and lose a major aspect of the clean feeling of the language.

Having done a whole lot of soul-searching, I feel like the answer has to be: it's optional (though possibly specifiable as required via a per-module "strict" mode).

It's nice to have a shorthand for foo: ensure action! ... and so I don't think the feature should be thrown out. But when you're writing foo: func [...] [...] you already know it's an action, and being forced to say you do makes the code look junky.

Fortunately, users now have the convenience of terminal slash assuring them the thing they are suppressing evaluation of is an action:

log: print/

log "Nice to know we are sure PRINT was an action"

So things are incrementally better. But just by virtue of the way the language works, there's always going to be uncertainties (it's a function, but how do you know it's the right arity function, or takes the right types?)

There may be places where it fits. The idea that function parameters have to be leading slash in order to be passed as executable actions is the sort of thing that may provide some sanity. (I've wondered if actions passed to functions should turn into plain FRAME! by default...)

Still more thinking to do, but I've concluded it can't be forcibly required in all use cases. Maybe if there's some kind of "strict" setting on a module.

1 Like

Maybe there are some functions that are marked as "generators", and others that are not... such that if you are receiving an antiform action back from a non-generator, then that's when you need the slash?

foo: func [...] [...]  ; generator, no slash needed

/thing: select obj item  ; SELECT not a generator, needs slash?

It would be much rarer to see the slashes in these cases, but they would be the actual meaningful places to see them.

This could be handled by a "hot potato" bit on values.

Maybe the way you are considered a hot potato is if you don't explicitly mention ACTION! in the return spec. e.g. if you just say you return [any-value?] then the return of an antiform frame will require the slash. But if you say [any-value? action!] then you're exempt. And if you just say [action!] then you're exempt of course.

Kind of an interesting compromise idea, although most hidden-bit ideas have been scrapped. It's almost like there would be two FRAME! antiforms... a stable one and an unstable one... and generators would return stable forms but non-generators unstable ones, that error unless they find an "aware" receiving site. :thinking: It's just an inkling of an idea at the moment.

Years of fretting... years of thinking... and...

:crab:

:double_exclamation_mark: I'VE GOT THE ANSWER :double_exclamation_mark:

Now that there's a new conception of meta-variables, this all comes together. When you say (^x: ...) you're asking to do a lifted-assignment, and when you say (^x) you're asking to do unlift when you fetch, without execution.

So now...


>> obj: make object! [x: 1020, y: func [x y] [probe x, probe y]]

>> for-each [key value] obj [
       probe value
       if integer? value [print "INTEGER!"] else [print "something else"]
   ]
1020
INTEGER!
** Error: FOR-EACH can't assign antiform action to VALUE, use ^VALUE

>> for-each [key ^value] obj [  ; ask value to be meta-represented
       probe value  ; ordinary fetch meta variable (e.g. quoted or quasiform)
       probe ^value  ; fetch unlifted (compensates for meta-representation)
       if integer? ^value [print "INTEGER!"] else [print "something else"]
   ]
'1020
1020
INTEGER!
~&[frame [x y]]~
\~&[frame [x y]]~\  ; antiform
something else

:double_exclamation_mark: THIS IS AS GOOD AS IT GETS :double_exclamation_mark:

It takes a bit of liberty in the sense that it's forcing you use meta-representative variables for something that doesn't require it (a plain variable can of course hold an antiform action, that's critical to the implementation of the system).

But it's simply watching your back. 99% of the time you don't want to be in a situation where you're running a function that you see in an enumeration. (This case of an arity-2 function would just start consuming the branches and things after them... a disaster.)

And it's symmetrical: if you start seeing ^value as sort of being like "the name of the variable" then you would logically line up that your references should match the definition, and use the same name.

Wow.

2 Likes

Thanks Brian—
That’s a beautiful design which allows us to elegantly handle some of the new parts in the box.

This Is The Right Idea, Needs An Operator (^)

We have this problem elsewhere, e.g. the "Surprising Ghost" problem... where a function that returns GHOST! only sometimes can cause disruptions to the structure of code.

In this case, returning an ACTION! only sometimes can be a pain.

The solution for surprising ghosts was that the evaluator would look at a function call, and if it returned a ghost always... allow it. Otherwise it would not vanish, but if you really wanted it to vanish you could use an operator:

>> 1 + 2 eval [comment "HI"]
== ~,~  ; anti (ghost)

>> 1 + 2 ^ eval [comment "Hi"]
== 1 + 2

So with the "Surprising Actions" problem, we need a similar rule:

  • If a function returns an action! always in its type signature, let it.

  • If a function returns an action! sometimes in its type signature, default to a PANIC if it returns an action!

  • Allow use of the ^ operator to override the panic and produce an ACTION!

So if you write:

x: select obj 'action-field

Then if you really mean it that you want X to be able to become an action, you have to write:

x: ^ select obj 'action-field

We can make an exception for metarepresentation assignments:

^x: select obj 'action-field

But then I think we have to use the same rule as for ghosts when we fetch the representation... e.g. ^x will produce a panic if you're holding an action, but ^(x) will not.

This can probably be limited to assignments, e.g. return ^x doesn't have any particularly great reason to be cautious, while y: ^x does... so you'd need to write y: ^(x). Basically the problem we're trying to solve is the assignment of actions to variables that will run the action, and you need some way of saying "yes, I know what I'm doing here".

This is a satisfying solution. It means we don't have to get too wild with the combinatorics of assignments. But I do think that /foo: ... meaning "I know this is an action assignment, so assert that" is very helpful especially to those reading code, when it's not obvious.

What Are The Implications For foo/~ ?

So if I write (bar: foo/) then foo/ as the "fetch and ensure it's an ACTION!" operation counts as one of the "actions always" operations that doesn't need a rubber stamp.

But what about foo/~, which has been proposed to be "get as is, don't run actions, arbitrary antiforms are ok". That returns actions only sometimes.

I kind of feel like making you write (bar: ^ foo/~) may be worth it, because it reinforces "hey, you might be setting this to an action"... if you were using foo/~ to think you were getting unsets or something. That gives us the necessary operation division: (^ foo/~) is "get me anything including actions" and plain (foo/~) is used for "get me something that may be trash".

It's a little bit weird but I think it makes sense. And again, this particular evaluator rule is probably only necessary when you're dealing with assignments.

How Does This Interact With The FOR-EACH ^VALUE Idea?

So here, we have a bit of an issue, that the operator doesn't go on the value if we wanted the "keep it as a non-meta form, but rubber-stamp ACTION! values"

for-each [key ^ value] obj [.
    ...
]

I don't think I'm that interested in worrying about the concerns of people who want to iterate over objects that contain some actions, some not, but don't want to use the meta-representational protocol. They don't want to do what they think they want to do.

I might be sympathetic to someone who is iterating over something where they know they're all actions:

for-each [key /action] obj-of-all-actions [.
    ...
]

There it makes sense to me that you know what you're doing. But if it's sometimes an action, and sometimes isn't, then I don't buy it that you're not better off with it being meta-represented.

Something that I might accept is the idea that /foo: either condition [action/] [~] is legal. If you assign trash to a variable that you think is an action, then you get correctly interrupted when you try to use it (vs. a null or something that would keep going). That would give you an option if you needed a state in your object to represent "no action here", that you could still enumerate using /action.

Promising Stuff...

Not easy to get it all implemented, but I'm working as fast as I can.