Weird Idea: LIB/APPEND Runs, but LIB.APPEND Doesn't?

Historical Redbol did not have generic TUPLE!. So that meant it used paths for everything...whether you were specifying a refinement to a function, or doing a member selection out of an object, or coordinate out of a pair, or...whatever.

So far what Ren-C has done is to shift it such that tuples are for member selection, and pathing is for refinements only.

A new twist is that a terminal-slash in a path (e.g. the last element is a BLANK!) means "APPLY the action". This is especially helpful when wanting to run a non-isotopic action in the new "activated actions" model:

>> block: reduce [reify :print]

>> block.1
== #[action! {print} [line]]

>> block.1/ ["The terminal slash comes in handy!"]
The terminal slash comes in handy!

So if you have a non-isotopic action in your hand, the slash can run it. But under the current rules, your arguments and refinements need to go in a BLOCK!.

Terminal Slashes Could Provide Safety...

With the above, the pattern you would use to say you were going to run something like APPEND/DUP out of LIB would be this:

>> lib.append/dup [a b c] [d e] 2
== [a b c [d e] [d e]]

But this means that you're on the hook for simple tuple references running, which you may not have expected:

>> if integer? obj.something [print "What if SOMETHING is an ACTION! isotope?"]
Boo!
** Error: Some error coming from OBJ.SOMETHING running

One way of stopping this would be to say that if you were going to call a function that was a member of an object, you'd have to use a terminal slash, otherwise you'd get an error.

>> if integer? obj.something [print "Clearer error if it's an ACTION! isotope"]
** Error: OBJ.SOMETHING is an ACTION! isotope, use OBJ.SOMETHING/ if intended

That would make field selection on the whole feel "safer". But at the cost of looking at a lot of terminal slashes, and nowadays that also forces you to use APPLY and pass your arguments in a BLOCK!.

More Succinct: Allow PATH!s to Pick The Action, Too

If something like lib.append/dup is clearly an action (due to the use of refinements) it wouldn't need the terminal slash, like lib.append/dup/. You already know an action is being run.

So what if you said lib/append instead of lib.append/ ?

Of course, under such a rule, lib/append becomes conflated with a situation where LIB is a function and you're applying the /append refinement. This does break away from a rule like "slashes always mean refinements".

That kind of sucks...BUT...it's still far less conflated than it was before. Remember, historical Redbol used slashes for EVERYTHING.

The new rule could be "slashes always mean from here on out, what we're talking about is function invocation". So it's either picking a function to run, or narrowing it down by means of a refinement.

The advantage here system-wide would be that you could use field selection without worrying about it, as foo.bar, meaning "I want that field and I don't want any functions to run". If there's no slash, there's no invocation.

I almost feel like the weird exception is worth it, because it would save a lot of bulletproofing that would otherwise be required on objects. But it's also clearly a bit bent.

The Other Direction: Terminal Dot as Invocation Suppressor

There's a possibility for saying "this reference is not an invocation", which could apply to WORD!s and to TUPLE!s alike. That's the terminal dot.

>> x: append.
** Error: APPEND. reference ends in dot but it's an ACTION! isotope

It has one downside of kind of being close to being a comma.

But the bigger reason I don't like this is just the mental tax that comes into effect. It's the same as how I didn't like the creeping desire for correctness, meaning people putting colons in front of things in a fairly ad-hoc way. We'd like the obvious code to just work.

I think the LIB/APPEND compromise may be worthwhile to get this correctness-by-default situation.

Possible Objection: Value/Function Invariance?

We might say that forcing you to distinguish at the callsite whether you are calling a function or not, prevents you from taking something that was a plain value previously, and substituting it with a function that calculates that value.

Under this principle, it's a feature that you "don't know" if obj.something is an action isotope or not...and making you commit to which it is by saying obj/something is bad.

It's kind of a narrow case--since you have to know at the callsite if it's a function that consumes arguments. So it could only be argued for arity-0 functions. And if the function doesn't return the same value every time, you're subject to some semantic questions.

I think that the answer here is that "accessor" functions (getter/setter) wouldn't count in this, if they existed. They would use the tuple syntax but stay "behind the scenes".

1 Like

Another way to look at it might be to say . is the value accessor, and / the function accessor.

So

lib.append
lib.append/dup

both give you the function value.

lib/append
lib/append/dup

both run the function.

For clarity the terminal / could be moved to starting / ...

append/ => /append

Hmm that last one would break refinements.

1 Like

It's a nice idea otherwise, to say the slashes begin the function call...and are optional for words, but non-optional once picking out of objects. It actually is somewhat nice-looking for calling functions... to the point I could imagine some people preferring to prefix it always in order to know what's a function and what's a value:

/append block var /clear data /frotz mumble bar /foobar grotz

(I had a thought that the language might be initially taught to always manually point out the functions, and then introduce the "voodoo" of isotopic actions as a "power user" feature... so perhaps even have a way of loading the standard libraries where no actions were isotopes...)

Remember there are no "refinements" any more, just PATH!s. And the only thing this is actually contentious with when PATH!s are acting as refinements, is the new APPLY syntax:

apply ^append [series value /dup 3]

We've previously wondered if that should be done with SET-WORD!s instead:

apply ^append [series value dup: 3]

But the inert refinements divided the space more pleasingly. With commas, this isn't quite as big an issue:

apply ^append [series value, dup: 3]

Remembering we can get a quoted refinement when we need one:

>> '/foo
== /foo

Seems there's some definite upside to this idea... I say it's definitely worth thinking about seriously.

We don't avoid the dual interpretation of lib itself maybe being a function with /append and /dup as refinements here, but we at least get a little more of an argument for why the conflation occurs.

/lib.append is better looking than lib.append/ for anyone who wants to avoid ambiguity.

1 Like

One potentially large positive upside is you might be able to limit the number of places where action isotopes need to be used.

e.g. object methods wouldn't have to be isotopes, if a path-bearing reference was what was used to run them.

if action? obj.field [  ; won't run it, returns inertly (errors on isotope)
   obj/field "I know you're an action!"  ; same # of characters to run it
]

If isotopes were a "weird thing" you used at module scope, then it might give more weight to the idea of saying that things like FUNC make plain ACTION!, and you have to do something special to turn them into an isotope...if that's what you want.

Also, we could have a more palatable syntax for the activations:

/foo: func [] [print "This thing will run on its own when you say FOO"]

foo: func [] [print "Requires you to say /FOO or else you get the ACTION! value"]

One might suggest doing it backwards for greater compatibility, and say that if you assign an ACTION! to a WORD it becomes isotopic unless you put a slash on it, in which case it would require a slash to call:

foo: func [] [print "This thing will run on its own when you say FOO"]

/foo: func [] [print "Requires you to say /FOO or else you get the ACTION! value"]

But I don't like that as much, because it means people who aren't thinking about whether something returns a function or not will wind up with activated values. :frowning:

(Something that's bothering me in the new model is how many places I'm having to put in automatic isotopic decay... I'm feeling it's obscuring the mechanics. So if the generators could produce non-isotopic functions, that would be a big help in removing the need for ACTION! isotopes to auto-decay.)

This could also mean that ACTION! could be evaluator-inert...with the isotope status merely being added by the /foo: notation.

>> obj1: make object! [foo: func [] [print "Plain"]]
== make object! [
     foo: #[action! []]
]

>> obj1: make object! [/foo: func [] [print "Isotopic"]]
== make object! [
     /foo: #[action! []]
]

It Could Limit The Scope of Isotopic Actions

We might even say that there is no way to actually produce an isotopic action as the result of an expression.

It could exist solely as a state of a variable. We might disallow GET-ting it in its isotopic form, and the only evidence you'd have that it exists is that things like ^append would give you back a plain ACTION!. So it must have been an isotope of some kind... but the only way you can put it in any other variable as an isotope is by assigning that variable with /foo:

The way this would be "subverted" in Redbol would simply be that its evaluator would be willing to run plain ACTION!s through words.

(Note all of these are just brainstorming at the moment about the possible implications. It may be that allowing the isotopes as expression results would be important...though if it could be avoided, that would be very simplifying.)

The Peace of Mind Element Shouldn't Be Underestimated

I know that Redbols are terribly insecure, and that's "just how it is". But the chance of running arbitrary code from every single line you write is fundamentally unsettling, so being able to pull that back by some large factor is a benefit.

So if we can pin it down to where...

  • ... you know that any plain WORD!s that execute are from modules you have deliberately imported
  • ... you have control over your own function parameters, by virtue of knowing none of them can execute automatically (at least when the function starts, you can tweak it after tht)

Then I think we buy a lot with the next step...

  • ...any random object or block you get passed that you pick at with TUPLE! won't run arbitrary code either

It's a huge step up, and when you add all the bits together...I think it's well worth making the APPLY syntax a little less distinctive.

1 Like

Or there could be a different word for creating deactivated actions, e.g.

foo: func [] [print "This thing will run on its own when you say FOO"]

foo: lambda [] [print "Requires you to say /FOO or else you get the ACTION! value"]

function, action, lambda, funcval, metafunc, ... ?

Which may even be seen as an asset, it gives us the look of objects as functions.

Don't think that would work out, because there are simply too many things that generate functions (think ADAPT, SPECIALIZE, AUGMENT, CHAIN, METHOD...) to give them all different names.


The fundamental issue I'm trying to address is that it's important to be able to write stuff like:

x: (some arbitrary expression)
switch type of x [...]
if x [...]

That should be reasonable and safe (allowing for giving errors with correct locality on things like voids or unset states). If it's not safe, then trying to write any kind of "interesting" code becomes a fairly harrowing experience.

I find it particularly harrowing because of the awareness that what "looks good" might work for a simple example, but won't be correct down the line. So you wind up in weird justifications of leaving some parts "clean-but-wrong"--in the hopes that situations won't actually arise in practice that break it.

You can imagine people fighting over this in a particular bit of code. (e.g. someone in Rebol2 who is GET-WORD! obsessed finding a bug and then going through turning every reference into a GET-WORD!. The original author says "...but it's not needed in all those places" and they say "...well what about the bug, how many more of those are there? how do you know it won't become an issue when code reorganizes in the future?")

Pushing the burden to annotating the "live" function word assignments may seem ugly to the uninitiated, but it can solve this problem near-perfectly. I feel like one easy-to-type character that you don't have to stretch for is reasonable. It would help people get their bearings.

>> foo: x -> [print ["X is" x]]
>> /foo 10
X is 10
>> foo
== #[action! {foo} [x]]

>> /foo: x -> [print ["X is" x]]
>> foo 10
X is 10

To see why it's important you only need to think of one little step of abstraction away:

 abstraction: func [] [
     return does [print "Boo!"]
 ]

Once you start thinking about that, you don't take the annotation of the live reference for granted.

This really offers a way of thinking about it where there's a "default" (not executing without the slash) and then you can twist it by convention... a bit like introducing quoting parameter conventions to someone who learns a normal function parameter first.

Leading-Slash-Executes Is A Better Story (and Better Look)

This improves things like action combinators in UPARSE...which were trailing slash before:

>> parse [1] [negate/ integer!]
== -1

>> parse [1 2] [add/ integer! integer!]
== 3

...but look nicer with leading-slash, that kind of "puts the function and its arguments on the same side of the divider" (vs. walling off the function from its arguments):

>> parse [1] [/negate integer!]
== -1

>> parse [1 2] [/add integer! integer!]
== 3

I think tying this in with the "LIB/APPEND executes, LIB.APPEND doesn't" puts together a story that holds together for solving a lot of historically "harrowing" problems.

Minor Gripe: Refinement Notation Inconsistency

When you write a function spec and it says:

 sorter: func [series [any-series!] /comparator [action!]] [...]

The combination of the slash with the fact that it's an action might suggest that it would run by default. That wouldn't be the meaning here.

But that's dialects for you. :-/ Here the slash is a callback to being a component of a function invocation, nothing to do with actually being a function.

Would This Be Overrideable?

Redbol would override it...but it would be done at the level of an actually-hooked evaluator that would run the ACTION!s regardless of isotopic status. In order to make Redbol functions callable by non-Redbol code, all SET-WORD! would have to turn

There'd clearly need to be some way to say SET-TRIGGERED-ACTION on a word, vs. making people write a function that ran do compose [/(word): (action)]. So a FUNC-like construct would be possible using something like that. But if function isotopes can be relegated to an implementation detail and never created by expressions in practice, I'll reiterate that would be a substantial benefit...

2 Likes

Well...this runs up against our desire to be able to do things like pass isotopes of actions to things like FIND, as a special "I don't mean this as a literal value" signal.

Lots of moving parts, here. :jigsaw: :truck:

Actually, it does seem reasonable...in that if you write:

obj: make object! [
    data: 1
    /accessor: does [return data + 1]
]

The idea here is we are saying you'd run the function as obj/accessor, with obj.accessor giving you an invalid isotope error. You'd say ^obj.accessor or ^obj/accessor to get the action (presumably if you used the latter, it would guarantee you got an ACTION! isotope back...while the former could return anything. So perhaps the former should be prohibited?)

In any case, looks pretty coherent. And goes a long way toward the "obvious code is correct" goal.

If you don't use the slash, we would treat it as just a value:

obj: make object! [
    data: 1
    accessor: does [return data + 1]
]

So here, obj.accessor would get you the ACTION!, as is.

But should it not raise any alarms if you say obj/accessor and try to run it? Do you have to say /obj.accessor instead?

I feel like that would help point out that you were running something that wasn't actually intended as a "method". It would help catch cases where people simply forgot to put the / on their /accessor: (which could be important if you were binding code into the object, and expected the methods to be word!-active)


I actually just started trying putting the slashes on the member functions of things, and it's a readability benefit... it helps you see more clearly what's a function and what isn't!

1 Like