Brave SWITCH new .WORLD [?] - Skippable Predicates

Note: This post was originally written to use REFINEMENT! for the predicates. However, generalized TUPLE! has seemed more favorable, so that's what's used when talking about it now...even though generalized tuples do not exist at time of writing.

It's a bummer that all the Rebol functions like SWITCH or FIND use some kind of internal magic operator to decide if things are equal. We'd like that operator to be less opaque and more clearly documented and understandable, and it's in the long to-do-list... to try and figure out how it ties into IS-ness or MATCH-ness or whatever it is.

Still, no matter what the default is, we know we need a way to override it. Ren-C had added a /STRICT refinement to SWITCH to deal with this case:

>> switch 0.01 [1% [<pretty lax...>] 0.01 [<stricter>]]
<pretty lax...>

>> switch/strict 0.01 [1% [<pretty lax...>] 0.01 [<stricter>]]
<stricter>

But the name "strict" wasn't something super agreed upon. And it's just the tip of the iceberg, you kind of want to pass in more functions. That seems kind of ugly.

>> switch/compare 0.01 [1% [<pretty lax...>] 0.01 [<stricter>]] :==
<stricter>

Fancy New Experiment: Skippable Refinements that ARE the compare!

Dig this (and it will look even better when EQUAL? and = are strict by default, and IS is the lax operator...)

>> switch 0.01 [1% [<pretty lax...>] 0.01 [<stricter>]]
<pretty lax...>

>> switch 0.01 /strict-equal? [1% [<pretty lax...>] 0.01 [<stricter>]]
<stricter>

>> switch 0.01 /== [1% [<pretty lax...>] 0.01 [<stricter>]]
<stricter>

There's a skippable slot for a "refinement" (blank-headed PATH!) between the thing you're switching on and the cases block. And it looks up to an ACTION!.

It also accepts literal ACTION!s. You won't/can't type that in by hand...but that means if you're doing an APPLY there's a normal frame slot for your compare to go into. (It's one of the nice things about skippable parameters being knownst to the system in a way that variadic parameters are not.) If you need a SWITCH variation with a different interface you can make one out of this one.

Notice that the interpretation puts the switched-on expression on the left or in the first argument.

>> x: 10
== 10

>> switch x /greater? [20 [<nope>] 5 [<yep>]]
== <yep>

Before we go propagating the idea all over the system, why not give this a spin. Kick the tires a bit. Write some tests!

>> x: 10

>> switch x /(reduce pick [:greater? :lesser?] 1) [20 [<a>] 5 [<b>]]
== <b>

>> switch x /(reduce pick [:greater? :lesser?] 2) [20 [<a>] 5 [<b>]]
== <a>
1 Like

I feel like maybe this should be done with generalized TUPLE! instead.

switch obj/value /matches [...]

switch obj/value .matches [...]

Because the plan is that TUPLE!s will be inert, there's not much odds of you switching on one literally. But PATH!s you will want to switch on.

As the approach goes on, this will make it harder to do things like FIND on explicit tuples

do [find data .matches integer!]  ; yay, works

do [find css-stuff .class]  ; oops, tries to call CLASS as the predicate

But with generalized quoting, you'll be able to get around it:

do [find css-stuff '.class]

do [find css-stuff lit .class]

You just have to understand that TUPLE! has a purpose that is "effectively evaluative", kind of like you have to understand when you go to find WORD!s. find data my-word already needs a quote or lit. It comes with the territory.

I'm a little bummed because it kind of feels like these "are refinements", but I think that re-imagining it and saying tuples starting with dot act as predicates (by convention) probably has better bones.

1 Like

I find that tuple looks better, and wins when it comes to ergonomics.

2 Likes

Using something different than refinement could also have another advantage, that it could be interpreted as a kind of chain. If we assume SWITCH usually goes with IS, you might want to use = for strict equality. You also might want to use a more literate "equals". But there isn't any particularly nice way to say "unequals".

So what about just good old:

 switch x .not.equals [
     <whatever>
 ]

Then imagine if we've define IS as is: :equals/lax. So there's a refinement on equals, and you might also call it as:

 switch x .not.equals/lax [
     <whatever>
 ]

So that's an interesting potential advantage. Though the proposal I have for tuples is that paths take priority, so it would see that as the PATH! [.not.equals lax]. But if the chain is specializing that should still work, e.g. (=> NOT EQUALS) could produce a function that takes one argument and has a /LAX refinement.

(This is all pretty out there, and easier said than done...but it does tie into stuff I'd like to see be able to work. I'm just putting in another point of notational advantage for TUPLE! here.)

This also suggests the possibility that a GET-TUPLE! might have a purpose as a chaining generator, e.g.

 chain [:a | :b | :c]
      => get c.b.a 
      => :c.b.a

We also might consider if CHAIN should be reversed, which is something that came up before.

1 Like

Predicates are becoming more critical to think through, as Ren-C tries to pin down some formalisms for off-the-cuff Rebol2-isms.

For instance, consider how Rebol2 blurred lines by considering both literal datatype matches and values-of-that-type to match:

 rebol2> find ["A" 10 20] integer!
 == [10 20]

 rebol2> find reduce [string! integer! block!] integer!
 == [integer! block!]  ;...and, it looks like WORD!s :-/

In moving to using the @[bl o ck] type to represent datatypes, Ren-C is making this more formal:

>> integer!: @[integer]

>> find reduce [text! integer! block!] integer!
== [@[integer] @[block]]  ; distinct from WORD!s

By and large, the interpretation most natives (including PARSE) will be taking is that often when you are using such an @[bl o ck], you mean to specify a type. But not always. Your dialects are free to interpret these other ways.

So I feel that if you want to do a FIND where you're matching against a pattern, that's a job for a predicate. But where should it go? How should the parameters be mapped?

Consider that you can write match integer! 10 and get 10, but match integer! <tag> and get null. That means the datatype parameter comes first and the thing to test comes second. So this predicate looks a bit backwards:

 find /match ["A" 10 20] @[integer]

There's a routine called MATCHES which reverses the order:

>> 10 matches integer!
== 10

So we might use that instead:

find /matches ["A" 10 20] @[integer]

But would it look better if it were between ?

find ["A" 10 20] /matches @[integer]

Or does that only apply for things that are infix? It's hard to say.

But what I do think is that for a "fancy function" that does something other than an equality comparison on FIND, you should have to ask for it. We should make it easy to ask, and then people will likely appreciate the more deterministic and reliable behavior.

There's a technical problem I hadn't noticed until REFINEMENT! and PATH! unified (which we may discuss) but that is that PATH!s are not inert. This means it's hard to distinguish the above from:

 >> find [a b c] obj/thing-to-find   ; evaluative
 ** Error: predicate is not an ACTION!

e.g. the hard quote recognition of a PATH! sucks in things you might legitimately want to look up and find. :-/ You don't want to have to subvert this with find [a b c] (obj/thing-to-find) or other nonsense.

I'm still rather bullish on /a/b being both legal as something in the parts box and the "same-flavored" thing as /a. I'm also not confident that the evaluative advantage of an inert /a exceeds being able to get that with '/a. We may not know what the purpose of that leading slash could be, but it is presumably related to how binding is treated.

Yet I mentioned that I thought having TUPLE! be the inert form of PATH! was an interesting idea, as it would provide historical compatibility as well as open up new options. We don't have it yet, but its inert status would make it the clear better choice for this function.

My idea of tuples not being inert, but operating the same as paths but with constrained rules ("thing to the left of a dot is not a function") has such strong advantages I'm hard-pressed to justify tuple as being generically inert.

But if your tuple starts with an inert value, then making it inert offers compatibility advantages (e.g. 128.0.0.1). Then things like .foo.bar should also be inert, because they technically start with BLANK!.

So that part is good, but it still has the problem that the type alone can't dictate whether you want a predicate. If a TUPLE! can evaluate to a thing to be found like a WORD! would, you don't want all TUPLE!s to get sucked up by a skippable parameter that only sees a TUPLE!.

So you want a constrained type. If it's a TUPLE! (or PATH! or whatever) you're only interested in it as a predicate if it starts with BLANK!.

Of course constraining the instance of the value based on something more than just its raw type is a desirable feature. I've talked about constructing things like EVEN-INTEGER!. So if /foo is a REFINEMENT! and .foo is a PREDICATE! in this constrained-type universe, I guess that's all right.

The part I find a bit unfortunate about all of this is that it kind of seals the deal that BLANK!-headed paths (and hence "refinements") remain inert in the evaluator. Not that I knew exactly what else they were going to do, but I thought they might have some kind of meaning in namespacing (or something). This may be the final nail in that coffin.

But hey, many other wishes have come true. Can't have everything. But predicates are needed, I want to be able to say any .even? [1 7 5 9 10 3] and such, and have been bothered that these generic things aren't available...when they should be easy to make. (Notationally, any/predicate [1 7 5 9 10 3] :even? is just too awkward...though you would still be able to say that with a skippable parameter at another position!)

1 Like

Having given this the spin, I have found a couple of problems...

ACTION!s Being Hard Quoted Instead of Run

The /PREDICATE refinement not only takes TUPLE!, but it also is willing to take ACTION!. That's in case you try to supply it explicitly, these would be equivalent:

switch .greater? x [20 [<nope>] 5 [<yep>]] :greater?

switch/predicate x [20 [<nope>] 5 [<yep>]] :greater?

It didn't seem like it would conflict, because you don't see hard-quoted ACTION!s in people's code...

...unless they're using the API, or doing a COMPOSE. Internally this happens a lot:

REBVAL *reversed = rebValue(Lib(REDUCE), Lib(REVERSE), some_block);

(The Lib() macro does a fast fetch of builtin functions without word lookup.)

What was trying to be written above was the effect of reduce reverse some-block. But it's acting like do compose [(:reduce) (:reverse) (:some-block)].

The slot where it looks for the predicate TUPLE!s now has the REVERSE action in it...which we're intending to run to negate the variable. But it's picked up as a predicate. :frowning:

I don't think the solution is to have separate /PREDICATE and /PREDICATE-ACTION refinements on every function! Somehow any shorthands need to work with actions.

I Think I Want Leading Dot TUPLE! to Mean Something Else

It's difficult to read method code when you are mentioning a field in an object that was declared far away. This is true in many languages... so C++ frequently has people write this->x, and the problem is exacerbated in Rust code which is crawling with self->x.

What if you could just say .field, and that would be assumed to mean something along the lines of self.field ?

What I'm thinking is that in Rebol's binding model, you'd have a way that .field -- even though in memory it has a heart-byte that is a WORD! (single immutable cell) -- that could hold a binding for an implied object, not a binding for FIELD.

This concept has gotten me intrigued to where the predicate use seems far less advantageous.

Going to Drop Special Predicate Notation for Now

Given the various issues I've encountered, I'm going to just make /PREDICATE take a normal action for now...with no shorthand.

With new APPLY, it's not too onerous to supply a predicate. And you can write a whole function in place...which is probably something you'd often want to do:

apply :switch [
    x [20 [<nope>] 5 [<yep>]]
    /predicate func [a b] [a > b]
]

We know that notation will have to be supported, regardless. So let's put that as a stake in the ground so the functions still have the predicate ability. And work on the shorthand notations as time permits.