Big Alien Proposal 🛸 "/REFINEMENTS" Run Functions

UPDATE...This was tried out, and while interesting things were learned it ultimately didn't "feel right" (in particular the competition of /slash for refinement and /slash: for function definition was more annoying than anticipated, and SET-PATH!s introduce a lot of complexity in places it wasn't needed.) See further down the thread. It's informing new designs being tried out.


So far, /leading /slash /notation has just been evaluator inert, lining up with Rebol2 and Red and R3-Alpha:

redbol>> /foo
== /foo

That inertness doesn't seem to get leveraged much. And in Ren-C it's a particularly weak choice, since the evaluator has generic quoting to get you the literal result:

ren-c>> '/foo
== /foo

Also, leading slash is an actual PATH!...encompassing arbitrary patterns like /lib.append/dup/part. So I've always been wondering if there was some interesting evaluator behavior for it, like...

  • ...asking to pick from "global scope": if your function has an argument called ALL then /ALL might get you the definition outside your function? (Something like ::foo in C++)

  • ...maybe a shorthand for self/foo for picking members out of objects inside of methods?

Yet nothing has ever really stuck. But @IngoHohmann pointed out that there's a basic thing that leading slashes might do for us which may have been overlooked...

...simply running functions.

"But WORD!s Run Functions, Why Should /FOO Do That?"

There are many reasons, but the biggest one is...


I feel pretty much 100% certain it is time that we switched to a world where not all WORD!s holding ACTION!s will run them.


It's too cumbersome when writing generic code to worry that a value you got "from somewhere" and put into a SET-WORD! has to be handled with special operators:

 >> var: select obj 'item
 >> if integer? var [print "INT"]
 Muhaha the next thing at your callsite was [print "INT"]  ; eek, VAR was action

 >> var: first block
 >> if integer? var [print "INT"]
 HAH! Did you think blocks were safe?  Not at all: [print "INT"]  ; in blocks too!

:man_facepalming:

Whether you think of it in terms of "security"--or simply bugs and chaos--this persistent tax on Redbol code authors has lacked a palatable solution. Putting a GET-XXX! on every access is ugly, and easy to forget. What we've ended up with is a mishmash...where people are constantly forced to choose between deciding if the brokenness is likely enough to cause a problem that it's worth it to make the code ugly.

(When code ages, it's like it develops some sort of pox--as leading colons are added on an ad-hoc basis, then no one really knows if they're safe to remove.)

I propose that only specially marked assignments would automatically run a function through a word reference, requiring a ^META access to get the action value literally.

>> /foo: func [x] [print ["X is" x]]

>> foo 10
X is 10

>> ^foo
== #[action! {foo} [x]]

If a function is assigned through a plain SET-WORD!, then that would be inert by default...but able to take advantage of this new leading-slash execution.

>> foo: func [x] [print ["X is" x]]

>> foo
== #[action! {foo} [x]]

>> /foo 10
X is 10

Compliance Isn't Actually That Ugly!

If you look at the definition of an object, then annotating the member functions isn't really so bad:

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

This also gives us some extra ammo: it can explain why we would use OBJ/ACCESSOR to invoke the function, and why OBJ.ACCESSOR can act as an error.

That can clean up examples like this:

 >> error: trap [...]
 >> if integer? error.arg1 [print "INT"]
 You forgot to worry about TUPLE! too! [print "INT"]  ; aaaargh...

All we have to do is say that TUPLE! accesses like that can't run methods. It would have to be error/arg1 to run it... which would also confirm that it was actually defined as a method. (Otherwise you'd run it with /error.arg1 if it was just a random non-method field that happened to be a function.)

This would be a systemic solution to historical annoyances.

It Can Be A Nice Dialecting Pattern

It's useful in dialects where plain WORD! references are taken for another meaning. For instance, UPARSE by default assumes a word means a combinator, so if you want to run a function that uses parse rules to gather its arguments you need something else:

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

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

Initially I tried this with terminal slashes, as negate/ and add/, but that doesn't look as good (and separates the functions from their arguments).

What Do We Lose?

Because I was trying to think of a meaningful evaluator behavior for leading-slash values, I didn't do much with them. But eventually I decided to use them in New Apply:

APPLY II: The Revenge!

They're nice because they break up the space:

>> apply :append [[a b c] [d e] /dup 2]
== [a b c [d e] [d e]]

But SET-WORD! is reasonable at this, and commas can make it more visually separate if needed:

>> apply :append [[a b c] [d e] dup: 2]
== [a b c [d e] [d e]]

>> apply :append [[a b c] [d e], dup: 2]
== [a b c [d e] [d e]]

Of course whatever we put here is being overloaded. If you want a SET-WORD! for assignment purposes, you'd have to put it in a group:

>> apply :append [(abc: [a b c]) [d e], dup: 2]
== [a b c [d e] [d e]]

>> abc
== [a b c]

So we could think of this similarly. If you wanted to use a refinement-style path here, you just do it in a group:

>> apply :append [(/reverse [a b c]) [d e], /dup 2]
== [c b a [d e] [d e]]

APPLY is a dialect, and there are always going to be some tradeoffs made. There's only so many parts.

It's probably best to leave APPLY as it is. I don't think we're going to be in the midst of some epidemic where suddenly every function invocation is done through a leading slash and it's going to be contentious. There will also be ways of running a function through REEVAL or maybe a dedicated RUN function that won't use the slash...

So nothing needs to be lost, really.

The Big Win is that the Obvious Code is the Correct Code

I've done some tentative implementation on all this, and all together, it seems pretty solid

Really all you're doing is paying the cost of an extra (easy-to-type) character to say that a word is intended to execute a function without needing to explicitly be told to.

There will be ways to subvert it, as of course you could do this:

>> /func: enfix lambda [left [set-word!] spec body] [
       do compose [/(as word! left): lib/func (spec) (body)]
   ]

>> cheat: func [] [print "Breakin the law, breakin the law..."]

>> cheat
Breakin the law, breakin the law...

But we wouldn't make you do it that laboriously, if you're making something where words need to be associated with functions that run automatically. And Redbol would do it through some evaluator parameterization as opposed to a mechanism like that.

However, the general expectation would be that most people would embrace the slash, as a useful piece of information...that makes everything work more coherently.

3 Likes

Looks awesome to me. I appreciate the leveraging of prior art and making it consistent throughout the design.

2 Likes

Agreed there is a persistent tax on code.

I think a great new perspective and I like that, makes you think and dream.

I wonder if the proposal just shifts the problem so you will still have it, just in a different way. If it reduces the burden then that's good.

The slash becomes a marker for type I guess but not always. Perhaps a beginner will pre-slash all words wherever they want to call a function - I wonder what that would look like.

Obvious code is the correct code is definitely a big win.

In thinking how the utility of leading slashes may have been overlooked for other usage I wondered if it could relate to binding "this word searches for it's binding...somewhere else..." - but I've got nothing useful to say on that.. :upside_down_face:

Thinking how the proposal associates type, my next vague idea was whether words could use a dictionary that specifies expectations more globally that the code to which the dictionary relates is prepared for functions or not. Again I've got nothing useful to say on that further :smirk:

Ignoring my vague notions, I think it's a potentially very worthwhile idea.

2 Likes

It's not "oh no, terrible idea"...so I'll take it. :slight_smile:

I have thought about how this would affect teaching the language.

If you think of it like learning there are "normal" parameters, and then learning "quoted" parameters is part of the whole power to make things bend.... then you might imagine a certain style of teaching where people think for a while that things like APPEND are somehow "keywords" and then they have to use slashes for their "functions".

But then...you reveal... that /FOO: is the magic "keyword-making tool", and they see it as a choice.

Perhaps people would start thinking of these as being in different categories--and using a mix of both. ("I use FOO: for functions, and /FOO: for keywords") But that starts to make you wonder about what to do when they slip up... and what they thought was going to act as a keyword falls through the cracks as a discarded value.

(I've written a bit of theory on how we might do a better job of "noticing discarded values", and there actually are probably a few new tools for us here... but... nothing imminent.)

Anyway... who knows. Maybe this idea of "there are functions you call with a notation, and there are keywords" isn't such a bad idea. It's how other languages work. :man_shrugging: Perhaps some users would find comfort in drawing a distinction about which words they "activated".

Ultimately Redbols have brought in the dangerous opportunity that any word you are looking at can be redefined, and might call a function.

Yet we have to write code, and meta-code, and meta-meta-code, etc. in such an environment.

So I would say your intuition is correct--this is like noise shaping in engineering--where you try to push the system-intrinsic noise into places it won't matter (like moving sound noise power to where the human ear won't pick it up). We cannot remove the complexity, only push it other places.

But I feel that this pushes the concerns up the spectrum to the meta and meta-meta code, which already had to be careful anyway. And I don't think any of that code gets too much more complex...just a bit different.

I think it will give people a better grounding to be able to use the language enough to even care about what's in play at the meta and meta-meta levels...!

1 Like

A post was merged into an existing topic: It's Time To JOIN Together

One piece of feedback that came back to me was that this feature (picking out of "self") sounded compelling.

And I notice that if we decided that .data was a way of picking a data element out of a "current" object, that we'd need /accessor for running methods out of the "current" object. :neutral_face:

Hmmm.

That still fits in with the idea of /foo: for making word-active functions, so it doesn't undermine that part. And it still fits in with /foo running functions, too. It's just potentially contentious with "run this function with plain binding"--whatever plain binding is. :roll_eyes:

Anyway...there is still terminal slash. And of course we could make RUN for invoking inert functions.

>> foo: func [x] [print ["X is" x]]

>> foo
== #[action! {foo} [x]]

>> foo/ 10
X is 10

>> run foo 10
X is 10

If we believe leading slash needs to be preserved for such purposes, they might be common enough to warrant using SET-WORD! in APPLY.

One brainstorm thought would be that if you're not in an object, then /foo gets you the function "as imported in the current module". And then you could use a number of slashes to escape that, like inside an object you would say //foo or ..foo to back out. This depends very much on the binding model to say you have that available, and likely wreaks havoc on composed code.

Anyway, the way to get a stronger position is to keep looking at things from lots of angles. So keep thinking!

I'm going ahead with the /foo: for word-activated functions regardless, as that holds whether /foo is reserved for running methods out of self or not. There's a lot to get worked out--in particular this is creating the first big rift in Redbol emulation, so parameterized evaluators have to get worked on.

1 Like

I always felt like there was an elegant symmetry to:

:word  => get
 word  => evaluate
 word: => set 

The evaluation of a primitive being itself and the evaluation of an action being it's result. Unless I'm forgetting fringe cases, wouldn't this in practice make word! and get-word! effectively the same at that point? I mean if this is where we are at then why not go full Leeroy Jenkins on it and force all value accesses through get-word! and restrict word!s only to evaluating actions. Of course I could just be brain farting at 2 in the morning.

It's worth asking, but... the entire purpose of the language is to sort of be able to "animate" declarative data so that it executes, usually with an English-like aesthetic.

:xxx isn't a particularly natural thing to see or type--we don't put colons before things in writing. It would make things much uglier.

More a gimmick... and the 90% similiarity between WORD! and GET-WORD! mean it's not that useful. The difference is mostly just there to bite people who are trying to write generic code when they forget.

There's a lot more elegance with simply WORD! for getting, and SET-WORD! for setting. And if there's some reason WORD! wasn't good enough for getting, then the value is probably "ornery" in some way...so when you fetch it, then ideally it should go through a transformation that makes it less ornery.

This is why there are ^META words...

>> x: 10

>> ^x
== '10   ; quoted 10

The added quote level defuses things at "quote level -1", which are called isotopes. These are the things with "odd" behaviors (like unset variables, definitional errors, running actions). The ^META step means they are easier to work with and pipe around...because they have been reified as plain items at quote level 0 (so they can be put in blocks and are otherwise normal). Then you "unmeta" them to return them to the form that's "too hot to get".

But this is for when code needs to be sophisticated...and my aim is to empower that sophisticated code. So if someone would say "I'm not that worried about things like picking a function out of a block, because I never put functions in blocks"... they need to think about what life is like for the people writing libraries. If good generic libraries aren't pleasant and fluid to write--with that property of correct by default that I mention... the end users won't have a very interesting ecology to write code in.

2 Likes

The appealing thing here is the idea that inside a method implementation for an object, you could have another method in that object with a name like "append", and then /append would be assumed to mean self/append while plain append could be left to its global meaning.

Or... we could say the self form is ./append - which would be kind of like how file paths work in Linux. When you just say plain append it uses the "active environment" (e.g. search paths) to determine the meaning. The ./ prefix says you want to be running things out of the current location.

That would leave /append free to be talking about the same word as append, just asking to invoke it.

Remember this is all highly speculative, and based on binding ideas that may be contradictory to whatever model is found to eventually make sense.

@iceflow19 - you might want to look at:

Rebol And Scopes: Well, Why Not?

1 Like

I can see the benefits of this proposal especially from a security point of view. If we are thinking of using RUN then I am not convinced that / sounds like a run symbol.
> is an extra key press, but it looks much more like a run action to me. We may have to consider a different prompt symbol.

>> >foo 10
X is 10

A fair thought...I'd say if there's anything so far bothering me with the prototype is that refinements are slashy, and the conflation starts making me think that those should be executable actions.

But how would we be representing these things?

obj>accessor
obj>accessor/refinement
obj.subobj>accessor/refinement
obj.data

This involves coming up with ways to wedge that > (or whatever) into the existing reasonably-well-thought-out-and-working TUPLE! and PATH! mechanics. What structure would these things be building?

There might be a RUN-WORD! like >foo, and perhaps we could make an argument that RUN-WORD!s are allowed in PATH!s (at the head only, maybe?) and get something like:

obj.>accessor/refinement

But... the thing that the slash idea has going for it is just that it seemed fine to say that obj/accessor ran a method before. And a plain word could run an action as well. So when you tie that together with the idea of obj.data for picking a field, it makes the slash seem like an acceptable "method" indicator. A global slash is just a method on the global context or module itself.

Appreciate the thought, good to keep the ideas coming...

I managed to get a booting system where you have to use /foo: assignments to get functions to execute when dispatched by WORD!.

TL;DR - I don't think I like it...BUT...

It's valuable exploration, to look at the options and think about alternative designs.

It Exercises Having FUNC Not Return ACTION! Isotopes

Having "ACTION!s at quote level -1"... that cannot be put in blocks, and can represent a non-literal sort of usage of actions... is very new. Like a-couple-weeks-ago new.

And in the first cut at implementing them, consider what would need to be true for this to work as expected:

foo: func [x] [print [x]]

In this world, for the word FOO to run the function, that meant the return product of FUNC would have to be a function isotope.

That got ugly. The whole idea of isotopes is that most routines don't touch them, and error when they encounter them. But this meant every generator (FUNC, LAMBDA, ->, ADAPT, SPECIALIZE, CHAIN, ENCLOSE...) had to make isotopes. And everything that processed or composed functions in any way had to either take isotopes or force you to ^META your generated functions.

I tried throwing in random decaying from isotopes to normal actions, but it felt uncomfortable.

That's because I imagined no one would want to write the "clean" answer:

foo: unmeta func [x] [print [x]]

And probably wouldn't be much better with a specialized native like RUNS that only isotopified actions (in the spirit of how SPREAD only isotopifies blocks):

foo: runs func [x] [print [x]]

I suggested even stealing something light like arrow:

foo: -> func [x] [print [x]]

But the search for minimal also dovetailed with the desire for lib/append to reasonably imply /append was a function. So that's part of what birthed the comparatively-palatable idea of slipping it into the assignment with a single easy-to-type character:

/foo: func [x] [print [x]]

So it's good to try that out. Not having weird decay operations everywhere feels more stable.

But...

Incongruity With Refinements Feels Worse Than I Thought

We've already discussed the overlap with APPLY, where nice happy things were being overloaded, like:

 apply ^append [series value /dup 3]

But here, take a look at trying to move the CASE refinement name out of the way so that CASE the function can be used inside a function:

/alter: func [
    return: [logic!]
    series [any-series! port! bitset!] {(modified)}
    value
    /case "Case-sensitive comparison"
][
    case_ALTER: case
    /case: ^lib.case
    ...
]

I'm not bothered by the number of slashes. I'm bothered by them meaning such unrelated things, yet being used so close to each other in such a core construct. The arrow proposal or even RUNS is less problematic:

alter: -> func [
    return: [logic!]
    series [any-series! port! bitset!] {(modified)}
    value
    /case "Case-sensitive comparison"
][
    case_ALTER: case
    case: runs ^lib.case
    ...
]

Other Than That, Slash Kinda Helped Code Readability

If you ignore the refinement debacle, there are some perks. If we tie together slashiness with "runs a function", then I've already pointed out how this feels pretty natural:

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

But additionally, there are lots of "function generators" out there, with even more in the future. If you see something that says:

something: make-processor alpha beta gamma

Knowing if that SOMETHING is a function or not is a fairly important thing. It means you have to think about how you handle it carefully, even if just passing it around to somewhere else. So if you see:

/something: make-processor alpha beta gamma

You have to know the function-or-not at the callsite. Given that you have to know it when you reference it, then it seems to make some sense to declare it.

But I feel uneasy about treading on refinement's turf. And among things in Rebol that seem actually kind of decent, the refinements in function specs actually do feel kind of "good".

Forgetting The Slash Was Certainly Annoying

We don't have any new-users who are only habituated to a world where you have to put in the slash. So there's just going to be people who are used to not needing it, forgetting it.

This does bring about a shift from the "over-execution" of functions when you forget a GET-WORD!, to an "under-execution" of functions when you forget a slash.

There may be safety mechanisms to notice during a DO if you have a completely unused value midstream in an evaluation, where you'd have to ELIDE it if you really "meant to do that".

>> foo: func [x] [print ["Not word-active, value is" x]]

>> do [foo 10]
** Error: Unused word-fetch in evaluator FOO, use ELIDE if purposeful

>> do [elide foo 10]
== 10  ; used, because it's the result

But such mechanisms wouldn't apply to REDUCE, which doesn't know whether you're going to wind up using the result or not:

>> reduce [foo 10]
== [#[action! {foo} [x]] 10]

On the plus side, it's easier to see what's going wrong there when you look at the reduced block.

What About An "I Meant To Do That" For Non-Slash FUNC Assigns?

One vague idea I had was that if the return type of something is only ACTION!, then that might be taken to mean the likelihood you want to assign it to something active is higher than usual. (If something can return ANY-VALUE! then you almost certainly want the thing it's assigned to being inert, because a slash assign would be an error if it was anything other than an ACTION!.)

So you could prove you-meant-to-do-that with any function that returned something other than just ACTION!:

>> foo: does [print "Hello"]
** Error: DOES only returns ACTION!, use /FOO or pass through something else

>> foo: all [does [print "Hello"]]  ; something generic that returns ANY-VALUE!
== #[action! []]

>> foo: inert does [print "Hello"]  ; maybe something specific for the purpose
== #[action! []]

It's a bit weird to get the evaluator involved with such heuristics. But practically speaking, it seems this would take care of most of the problem cases...and would help call out the oddity to readers.

The specialized construct should probably be enfix, quote the left hand side, and make sure the left hand side was not a slashed item.

>> /foo: inert does [print "Hello"]
** Error: INERT used with /FOO: on left hand side, wouldn't be inert

"What About Backwards: SET-WORD!s Make Isotope By Default?"

I observed you could actually do this the other way, e.g. assignments of ACTION! to word are assumed to convert to isotopes unless you suppress it. Maybe you suppress with something along the lines of .foo

>> block: reduce [does [print "Boo!"]]

>> block.1  ; tuple access is inert
== #[action! []]

>> foo: block.1  ; imagine assignment isotopifies
== #[action! []]  ; isotope

>> if action? foo [print "It's an action."]
Boo!
** Error: ...

>> .foo: block.1
>> if action? foo [print "It's an action."]
It's an action.

There is a compatibility advantage there... but it feels wrong... like... LIT-WORD!-decay-wrong. :grimacing:

This has the same kind of problem as with the GET-WORD!s. You'd find some people saying "oh, it'll never be an ACTION!..."--maybe right or wrong--and not using the dotted form. Then other people would put dots in front of absolutely every assignment, just to be safe. It's a lighter pox because it's only on the declarations, but still very much a pox.

What the slashing has going for it is that it has a clarifying effect--you know more about the source by seeing the slash there...and you don't get that "I know what it is" benefit from these dots.

But having to work with SET-PATH!s definitely introduced grating bits. Anything that searched for SET-WORD! (like MAKE OBJECT! in the block you pass it) had to search for things that were SET-WORD-LIKE. Some already shaky things got shakier.

Any Synthesis Of Ideas Here That's Not Terrible?

It should be remembered that isotopic actions are new, and their potential benefits are not completely understood yet.

We've seen great benefit from the isotopic blocks, resolving the /ONLY debacle. And there are ideas behind isotopic actions and typesets and such, for solving similar problems of wanting non-literal-value semantics.

The part I liked about this prototype was making things like FUNC and SPECIALIZE and ADAPT and everything else just return regular ACTION!. It felt cleaner to shift this burden of things becoming isotopic onto the caller, because otherwise the burden was on decaying isotopic actions to regular actions. That burden showed up when fulfilling action parameters, or inside COMPOSE or REDUCE, etc.

But looking at the two strategies side-by-side, I have to say that it's probably better to lean into the "generator functions" producing isotopes, and work from there.

3 Likes