Big Alien Proposal 🛸 "/WORD" Runs Functions

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 UNRUN or ^META access to get the action value literally.

>> /foo: func [x] [print ["X is" x]]
== ~#[frame! [x]]~  ; anti

>> foo 10
X is 10

>> ^foo
== ~#[frame! {foo} [x]]~

>> unrun foo
== #[frame! [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
== #[frame! {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 could motivate using OBJ/ACCESSOR to invoke the function, and have OBJ.ACCESSOR give it back as-is.

You could then tell where the function invocations were: XXX.YYY.ZZZ is never a function call. XXX/YYY/ZZZ always is, and you know YYY is a function. XXX.YYY/ZZZ is a function call, and you know ZZZ is a function. Readability is improved by a lot!

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 [(var: [a b c]) [d e], dup: 2]
== [a b c [d e] [d e]]

>> var
== [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.

4 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:

Or... we could say the self form is ./accessor - which would be kind of like how file paths work in Linux. When you just say plain accessor 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 /accessor free to be talking about the same word as accessor, just asking to invoke it.

It's an idea. But . is a WORD!, so if you were going to go this route it would have to be defined in the frame of a method. Or we relax it and say that .accessor can run functions like a WORD! could. But leaving out the slash seems suboptimal for this proposal.

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 antiforms. These are the things with "odd" behaviors (like unset variables, definitional errors, run-via-word-reference FRAME!s). The ^META step means they are easier to work with and pipe around...because they have been reified as quasiforms (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

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
1 Like

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 The First Attempt... BUT...

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

I Explored Having FUNC Not Return FRAME! Antiforms

Having Action be "FRAME!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 antiform.

That got ugly. The whole idea of antiforms 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 antiforms. And everything that processed or composed functions in any way had to either take antiforms or force you to ^META (or "UNRUN") your generated functions.

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

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

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

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

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 leading slashes don't mean function execution, 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.

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 an action 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.

Any Synthesis Of Ideas Here That's Not Terrible?

It should be remembered that antiform-based Actions are new, and their potential benefits are not completely understood yet.

We've seen great benefit from the antiform blocks, resolving the /ONLY debacle. And there are ideas behind antiform 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 FRAME!. It felt cleaner to shift this burden of things becoming antiforms onto the caller, because otherwise the burden was on decaying antiform 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 antiforms, and work from there.

3 Likes

After about two years of learning to work with antiform-based actions, they are reasonably understood (and understood as the antiform of FRAME!)

The idea of FUNC returning plain FRAME! and then having a refinement-style assignment promote (demote?) it to an antiform is bad.

>> foo: func [msg] [print ["This" name]]
== #[frame! [msg]]

>> foo
== #[frame! [msg]]

>> run foo "Sucks"
This Sucks

>> /foo: func [msg] [print ["This" msg]]
== ~#[frame! [msg]]~  ; anti

>> foo "Sucks As Well"
This Sucks As Well

It is way too easy to forget the slash.

But I've Been Up All Night Smoking Crack. So Hear Me Out...

What if FUNC returned an antiform FRAME! and you had to use the slash in order to say "yes, I meant to assign that?

>> func [msg] [print ["This" msg]]
== ~#[frame! [msg]]~  ; anti

>> foo: func [msg] [print ["This" msg]]
** Error: Cannot save antiform frame with foo: (use /foo:)

>> /foo: func [msg] [print ["This" msg]]
== ~#[frame! [msg]]~  ; anti

>> foo "Might Have Potential"
This Might Have Potential

/FOO: and /FOO could mean "antiform frames are ok to be assigned here.

  • You can use it in function specs, e.g. func [a] [...] would not take an antiform action, but func [/a] [...] would.

  • You could use it in enumerations, e.g. for-each [key /value] obj [...] would allow you to receive frame antiforms, but for-each [key value] obj [...] would not.

Crazy Talk :crazy_face: What If Refinements Were CHAIN! ?

Under the rule above, you'd have to make your methods in your objects look like this

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

Let's say that in order to call ACCESSOR you had to use a PATH!, e.g. OBJ/ACCESSOR. And then...for refinements...we use CHAIN! ... our new interstitial peer to TUPLE! and PATH! that uses colons.

This would make :word the new refinement (blank-headed chain):

obj: make object! [
    data: 1
    /accessor: func [arg [integer!] :refine1 [integer!] :refine2] [
        if refine1 [
            return data + arg + either refine2 [negate refine1] [refine1]
        ]
        return either refine2 [data + negate arg] [data + arg]
    ]
]

And if you want to use those refinements:

obj/accessor:refine1:refine2 10 20
  • By looking at it, you can tell that's a call to a function... it's only the weird exception of WORD! that were assigned with /word: where you look at word and can't tell in advance

    • compare with Redbol foo/baz/bar/mumble - You have no idea what's going on.

    • Ren-C made an improvement with foo.baz/bar/mumble where you know due to the use of refinements that foo.baz must be a function, but you're still out of luck if you just see foo.baz

    • This gives us foo/baz:bar:mumble, where we get a more intuitive sense of where the function is (it's /baz, the thing after the slash) and in the diminished case we'd still know foo/baz was a function call

  • Field selection via dot of actions could always not run the action even if it's an antiform, e.g. obj.accessor just gives you the frame antiform as is

    • The impact of these casually extracted antiforms would be limited to places that were rigged to accept the antiform frames

(append:dup [a b c] [d e] 2) is different, but is it bad?

It shifts :foo to be unrelated to "getting"...it would be inert, and used in things like APPLY.

And with slashes being so applicable to function application, it seems inevitable that // be the operator (though you could use :: if you want, these will be WORD!s now)

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

:dup would become REFINEMENT?. This would mean that leading colon is how you imply optionality, not slash.

>> [a b c]: pack [1 + 2 10 + 20]
** Error: not enough values in antiform pack for assignment

>> [a b :c]: pack [1 + 2 10 + 20]
== 3

>> reduce [a b]
== [3 30]

In these places, if you used a slash you'd be saying you'd accept action antiforms. You can use both a slash and optionality: /:c. And you can circle it too, @/:c, if you really want to !

Execution Suppression

Accomplishing execution suppression could be done with a terminal dot on a WORD!, or the usage of any TUPLE!. I always thought foo. as "I mean FOO. Period." had potential.

>> append [a b c] [d e]
== [a b c] [d e]

>> append.
== ~#[frame! [series value :dup :part :line]]~  ; anti

But now we'd have it so foo.bar would be enough to get the execution suppression without needing to say foo.bar. because even antiform frames won't run unless you say foo/bar :bangbang:

And there'd be far fewer occasions where you'd need to use foo. "just to be safe". Because you'd have a sense from wherever it was assigned if you didn't have to worry about it.

You wind up with a different look:

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

vs.

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

I can see how the period might suggest (apply append) [[a b c] [d e] :dup 2] if you didn't know what it meant. But if you don't know what things mean, then you can interpret things all kinds of ways.

It doesn't seem all that measurably worse--if it's worse at all. Looking past this one particular case, the benefits are enormous. And you can always say apply get $append [[a b c] [d e] :dup 2] if you want to be verbose about it. (This particular case of course has append // [...])

And We Preserve the /FOO to Run A Function or Frame

>> tester: make frame! append.
== #[frame! [series value :dup :part :line]]

>> tester.series: [a b c]
>> tester.dup: 2

>> /tester [d e]
== [a b c [d e] [d e]]

If it's already an action antiform, then the slash can just be commentary for the reader to say "if not obvious from the context, this is a function call"

:exploding_head:


I only just came up with this, but it's based on understanding the pros and cons of prior attempts, and the benefits are easy to articulate.

I am especially fond of being able to look at XXX/YYY and know that's going to run a function, and that XXX.YYY never will.

This critically narrows the scope of negative impacts of antiform FRAME! by making it only relevant to dispatch from WORD!

...and of course the security of consciously declaring when you're assigning something to a variable that can run arbitrary code from a mere word reference is very important.


By being forced to say foo.bar or foo/bar you can't have a situation of "run it if it's a zero arity function or just give me the value". But maybe that's what foo/bar/ could mean if it ever comes up.

2 Likes

When you look at the case of /foo: it makes sense that the slash is "above" the foo:

/(foo:)  ; this interpretation is correct

If it were the other way around, it would be like you were trying to set a function execution, which makes no sense.

(/foo):  ; this is not correct

The wrapping path is relaxing a constraint on what would otherwise be a stricter assignment (no antiform frames), and the assignment stays stuck together so you can pick it out of the path.

And /:a is the representation of a refinement that is allowed to be an antiform action!!

Since . is a WORD! now we know that /.: has a meaning... assign an antiform action to the dot operator. /: means run the FRAME! or antiform-frame resident in the : variable.

You'll need to use escaping to assign forms like ::: or // because you can't get the colon "on top" of them.

>> ($:::): 100

>> :::
== 100

>> /($//): does [print "A small price to pay"]
== ~#[frame! []]~  ; anti

>> //
A small price to pay

It's all clicking.

:star2:
:flying_saucer:
:alien:

1 Like

Looking at some practical examples of the consequences, of course every time you want to assign a function you now have to say /foo: func [...] [...] instead of just foo: func [...] [...]. While that may seem a little irritating when it's FUNC, it's actually helpful when you're using something else... like someone reading /foo: cascade [...] for the first time will know that CASCADE is a function generator. So it's more helpful than it seems, and it's an easy-to-hit key on the keyboard.

it definitely pushes refinements down in terms of how much they stand out:

for-each [abbrev expansion] data [
    replace:all sig unspaced [space abbrev space] unspaced [space expansion]
    replace:all sig unspaced [space abbrev newline] unspaced [space expansion]
]

response: copy:part lowercase ask ["Okay?" text!] 1
if response = "y" [break]

surname: across to ","
thru space [opt some space]
[firstnames: across to "("] (trim:head:tail firstnames)
title: between "(" ")"

Colon just doesn't "pop" as much as slash does.

But I think this is the correct tradeoff, because it means slash now pops as showing you where functions are being called or defined, which is more significant.

We're getting more bang for our character buck.
:dollar:

Emulation of historical Redbol gets tricky. But new techniques could address it, when and if it becomes a priority for...someone.

BUT WAIT, THERE'S MORE: AN INSANEO-STYLE BENEFIT!

  • path means execute function

  • chain in path means pick refinement

  • tuple in chain means provide argument there !!!

This gives us what everyone always wanted.

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

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

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

And it means we can do away with skippable parameter hacks (in fact, we can do away with skippable parameters altogether):

>> compose:only.<*> [(1 + 2) (<*> 10 + 20) (100 + 200)]
== [(1 + 2) (30) (100 + 200)]

(That's an actual legitimate use of an :ONLY refinement...)

It would be literal, so you'd have to use a GROUP! to evaluate. So if (a: '**)

>> a: <*>

>> compose:only.(a) [(1 + 2) (** 10 + 20) (100 + 200)]
== [(1 + 2) (30) (100 + 200)]

Stratospheric: CHAIN! as The Function Call Dialect

What if you offered the ability to pass the chain to the function so it could handle it itself?

>> all:not [1 > 2, 3 > 4, 5 > 6]
== ~true~  ; anti

Instead of being constrained to a finite number of refinements, you could interpret it how you wanted.

Maybe a version numbering scheme to make the same function have different behaviors, without needing more than one top level declaration... something:1 vs. something:2 as a shorthand for something:version.1 and something:version.2 ?

Hence compose:<*> could be the presumed way to give the limit of what you compose. You could even change the interpretation of parentheses or fences or blocks...

>> compose:[$] [a b c [1 + 2] [$ 10 + 20] (100 + 200)]
== [a b c [1 + 2] 30 (100 + 200)]

>> compose:(a) [a b c [1 + 2] (a 10 + 20) (100 + 200)]
== [a b c [1 + 2] 30 (100 + 200)]

Sky is no longer the limit.

This Is Looking Like A Done Deal

Not only is this going to give greater readability, greater security/sanity, a more coherent model for things I've bumped up against... but it's features I now don't think I can live without.

I will be beginning the change shortly.

2 Likes

This is the first time I’ve seen this thread, but I really like it! It feels like a great improvement on the basic Rebol notion of refinements.

(And, for once, I completely agree with the syntax. It reminds me of Potion, which I’ve always liked.)

2 Likes

I like it. It’s really not hard to understand. Might take a little getting used to, but I think the benefits are clear.

I’m on board the spaceship.

1 Like

2 posts were split to a new topic: Function Composition via PATH!

Responses To Potential Criticisms


append:dup.2:line is less readable than append/dup.2/line because : visually does not separate the words so strongly as /.

Yes, I will admit it doesn't stand out as strongly. But there are priorities.

To me, seeing XXX.YYY and knowing that will not run a function vs. XXX/YYY and knowing it will run a function (and that YYY is the function) is a higher priority.

Along similar lines, when I introduced generic TUPLE! I had some doubts about pushing things down and losing some of the "heft" of slash:

foo/some-thing

foo.some-thing

I thought "the dash now looks stronger than the dot", to where you might try to grok that as:

(foo.some)-thing

But the mind is fluid. You read things how you are used to them. While I found this a little upsetting at first, that upset quickly gave way as I re-learned. I don't even think it took a week. I now think nothing of it, and read it naturally. (For that matter, we are used to this in filenames, such as my-file.txt not being my-(file.txt))

My bet is that this will just be another such thing, to where the benefits will vastly outweigh the downsides.


Rebol's ambiguity problems are not solved with optional slash. Only with a mandatory one--which would be a lot of syntax, and reduce the flexibility.

Only WORD! are left as the fuzzy case. And I believe that it will be annotated enough at the boundaries that you will know what you are getting into.

foo: func [block /baz <local> bar mumble] [
    baz 1 2 3
    /bar: frotz 5 6 7
    bar 8 9 10
    mumble: splat 11 12 13
    append block mumble
    print "You know this print isn't an argument to mumble"
]

It's unavoidable that the "keywords" in Rebol have this character, and you won't want the slashes on them. But I am thinking this is "as good as it gets" in terms of letting you mitigate the burden of the ambiguity, and write clear and safe code.

Note that we have even more safety offered now with APPLY, which you might use whenever you're getting functions you aren't completely confident in the arity of:

foo: func [block /baz <local> bar mumble] [
    baz // [1 2 3]
    /bar: frotz 5 6 7
    bar // [8 9 10]
    mumble: splat 11 12 13
    append block mumble
    print "You know this print isn't an argument to mumble"

    ; ... and you know mumble isn't an argument to block!
]

You're losing readability and making the language even more filled with syntax of all kinds.

Consider the difference between these two things.

Traditional Redbol:

old-append: :lib/append
old-orange: lib/orange

vs. /WORD is active proposal:

 /old-append: lib.append
 old-orange: lib.orange

These are the same number of characters. But the second not only looks better, it has many nice properties:

  • It keeps you from having to use an ugly prefix colon when you're fetching APPEND as an active function out of LIB by value... using a tuple (dot) as APPEND.LIB is enough to ensure you won't run a function

  • The prefix slash on /OLD-APPEND shows your acknowledgment that what you're getting can be active (if you just said OLD-APPEND: it would be an error)

  • You are shielded from executing a parameter-consuming function from LIB called orange by using the tuple of LIB.ORANGE

  • If orange in lib happens to be an antiform active function, it shields you from assigning that to old-orange because you didn't say /OLD-ORANGE:

I'm really skeptical of the good faith of someone who has used Rebol/Red at all and won't acknowledge the superiority of the second form.

1 Like

I have a little bit of a qualm here, if /foo is a notation that in some places means "thing that a function can be assigned" and in other places means "invocation of a function".

It's only a small qualm. e.g. in for-each [key /value] we know that KEY isn't getting evaluated. In function specs like func [a] we know A isn't getting evaluated.

We don't want to shuffle it so that foo/ is the notation for invocation, because that breaks the basic premise of obj/foo having the slash before the thing that runs.

So resolving this would have to put terminal slashes on all the receiving sites.

foo/: func [obj action/] [
   let arg1: 10
   for-each [key value/] obj [
       if not action? value. [print "Not an action!"]
   ]
   let arg2: 20
   obj/method arg1 arg2
   action arg1 obj.field
]

I don't like that. It's ugly, and I think the leading slashes are much more natural:

/foo: func [obj /action] [
   let arg1: 10
   for-each [key /value] obj [
       if not action? value. [print [key "is not an action!"]]
   ]
   let arg2: 20
   obj/method arg1 arg2
   action arg1 obj.field
]

I'm not convinced the first form really offers much benefit at all in practice, and it certainly doesn't offer enough to make things look that bad.

I have no problems with this — the situation is perfectly symmetrical. It’s no different to how Lisps let you write things like

(defun (f x y z) body)

to assign f, and then

(f x y z)

to use it. Either way, the notation (f x y z) means ‘function f with parameters x, y, z’.

1 Like