Squaring the circle of LENGTH? and LENGTH-OF

The stated goal of Ren-C was experimentation, to try things that may-or-may-not be good ideas... push them around and get experience with them, and possibly revert ideas that turn out to be bad (or create new ideas).

There a long and drawn-out description on the Trello card for getting rid of the name LENGTH?. This is one of the operations for which people were most unhappy with calling it LENGTH-OF. So even though LENGTH is very "nouny" (and the name of several refinement parameters storing lengths, e.g. /PART LENGTH), it was defaulted to a synonym of LENGTH-OF.

But...aesthetics matter. For instance: I am turned off by the look of foo == bar and its deceptive supposed complement foo !== bar (vs. foo != bar which is actually the complement of foo = bar...good luck with that, C programmers). That gives rise to musings on = and IS.

So it should not be surprising that I have suffered a fair amount of cognitive dissonance on things like LENGTH?/LENGTH-OF/LENGTH. We may be used to HEAD and TAIL but not LENGTH, kind of how we're used to FOREACH but not MAPEACH (which doesn't make FORECH good, nor HEAD or TAIL). Still, correcting it pains me, to look at what might be called a kind of -OF pollution, which is throwing extra words in places they do not belong.

I was writing some example code and had this:

foo: func [n /accum a] [
    case [
       not accum [a: 0 | recurse: true]
       n = 0 [return a]
    ]
    frame: context-of 'n
    n: n - 1
    a: a + 2
    redo frame
]

And while I found myself liking the elegance of it on the whole, I kind of couldn't get past how in Red, this would say frame: context? 'n and it would look better. The -OF feels like a "wart".

But the Trello card on LENGTH-OF lays out the facts. e.g. HEAD means give the head of a series, and HEAD? means ask for a LOGIC! of if the series is at its head. So if you say context? system that seems like a yes or no question ("is system a context?") and hence context? 42 should be false.

This got me to wondering if there was some thinking-outside-the-box option which might be a symbolic modifier of the word. context* foo, context& foo, anything that wasn't taken already that might look better than -OF (those don't, really).

In any case, I guess the point is that I am feeling unsettled with both the Ren-C "-OF warts" and Red's continuation of the non-LOGIC! convention for ?.

Just being out of the box - perhaps the "-" is the "wart"?

frame: context of 'n

and

head: head of s

I admit that the "of" looks superfluous in the second example compared with prior experience, but not if one wants "head" as a variable.

It is also meant to point to the dual meanings of the word "head", each meaning valid for different contextual notions. Mixing the same symbols from different contextual notions in the same expression is not something that has been cracked in Rebol yet, imo. I'm still hanging out for the "relative expression" promise to be fulfilled :slight_smile:

3 Likes

This spin on OF is certainly better looking--and not as far flung! I like the way you put it, that it's - that's the wart.

Mechanically we now have soft quoting on left infix, so you could do:

x: (pick [length context] i) of 'n

It is "weird", but this is actually how "actions" work (like APPEND), they're really symbol dispatches on the word.

I'll point out that with specialization and OneFunction, if you were in a situation where you needed a single arity function you can always make one:

takes-single-arity: func [f [function!]] [
    print (f "test")
]
takes-single-arity (specialize 'of [property: 'length])

So if you ever needed LENGTH-OF you can make it.

Unfamiliar at first, but it strikes me as learnable.

I say we push on this, because as I say, I'm just not happy with LENGTH? or LENGTH or LENGTH-OF...and already I feel more compelled to put up with some level of weirdness about this kind of strategy vs. going with any of those.

1 Like

It didn't occur to me at first, but this concept already existed in R3-Alpha's REFLECT. It had the same characteristic of taking a symbol and then doing a type-based dispatch to get it.

(Even though LENGTH and HEAD were "actions" and not "reflectors", this spirit of dispatch was already how they worked. Each type had its own dispatch which received the symbol for a WORD! to react to in order to retrieve the property. REFLECT just introduced another level in the dispatch which produces a kind of "reflection namespace", so you wouldn't be taking away a global name for it. words obj must have seemed over the top, so reflect obj 'words meant WORDS itself wasn't an action, but then people wanted less typing so you got words-of.)

Not many people knew of REFLECT's existence, because they only used the specializations like SPEC-OF, BODY-OF, WORDS-OF, VALUES-OF... which were in the base definitions:

Very few properties were extracted this way, and the mechanisms were somewhat sketchy (par for the course with the sketchiness of most of the port and action code). However, it was an existing mechanic that would need to be addressed one way or another.

Thus OF is just an infix left-quoting convenience version of REFLECT.

You are right to say it is not "solved". But pointing out that what we're asking for here is a "relative expression" effect certainly bolsters the argument that attacking the problem is well within the scope of the language mission.

I only had to use head of (or more frequently length of) for a short time before becoming sold. One has to consider the consequences of not figuring out how to do this...because the verbification of LENGTH caused a lot of devils in the details:

charset: function [
    "Makes a bitset of chars for the parse function."
    chars [string! block! binary! char! integer!]
    /length "Preallocate this many bits"
    len [integer!] "Must be > 0"
][
    ;-- CHARSET function historically has a refinement called /LENGTH, that
    ;-- is used to preallocate bits.  Yet the LENGTH? function has been
    ;-- changed to use just the word LENGTH.  We could change this to
    ;-- /CAPACITY SIZE or something similar, but keep it working for now.
    ;--
    length_CHARSET: length      ; refinement passed in
    unset 'length               ; helps avoid overlooking the ambiguity

    either length_CHARSET [append make bitset! len chars] [make bitset! chars]
]

...and those devils are still there for reused verbs like /ALL and aren't going away. But when LENGTH crossed the verbification line it seemed more drastic and hard to predict.

To try this vision, I twisted the code around and hacked it up for our common examples. It involved holding my nose a bit and digging in the bodies of code outside the evaluator. Definitely a hack...but at least a hack that piggy-backs on existing unsolved problems, without really introducing any new ones.

While jumping in with this half-baked might be more akin to the kinds of half-thought-through behaviors I usually would ascribe to ... other developers ... :stuck_out_tongue: I say we give it a chance. People can still use specializations if they like (I point out they'd already been effectively doing this with things like WORDS-OF, so it's not that mind-blowing to have TYPE-OF be a reflector specialization).

By and large, I think the infixed OF has been a big win.

There are some questions it brings up of how you get a list of reflectors for a type. This isn't a particularly new problem, and it parallels questions like "if I pass TO an INTEGER! how do I know which types it can accept as input"...generic actions hide information. This was one of the arguments for why TO-INTEGER was useful, because of exposure of the interface. One might argue that LENGTH-OF might be a similar hack.

The question of how reflectors are extended is pretty much exactly like the question of what R3-Alpha called "actions" are extended. One problem I just noticed is that since NEXT is implemented currently as a specialization of SKIP, there's a question of how that might get wedged in as next of series. Further how might parameterizations work? How would I say [skip 3] of series, for instance? Should I want to say that?

But the elegance of length of series can't be denied, and I don't think the total count of "design puzzles to solve in Rebol" went up all that much. Certainly balanced out on how much we hated length x or length? x or length-of x. Just wanted to mention the specialization issue, because I was pondering if NEXT might someday become NEXT OF SERIES to free up the variable "next" for common use.

So I was thinking about the definitions of MINIMIMUM / MAXIMUM and MIN / MAX, and realized that this was yet another case where you quite probably want variables called MIN and MAX.

For this reason, Ren-C had at one point renamed these to MINIMUM-OF/MIN-OF and MAXIMUM-OF/MAX-OF.

But this is another case where it would look better without the wart:

max: max-of a b

max: max of a b

I've come to really see being able to read code which reuses a word like max in such a way and not freak out as being part of the Relative-Expression bit of [R]elative [E]xpression-[B]ased [O]bject [L]anguage You're relative to the OF, on its left side...

We break the rules others won't, and get something in the bargain!

Looking at this old question, I realized something...

The Earth Has Moved Since 2017 :globe_showing_asia_australia: Virtual Binding

Functions like OF can actually receive a binding environment.

And... while this might sound a little bit loopy... OF can simply tack -OF onto the symbol you pass it, and look that up in the environment it runs in.

So basically, you extend the reflectors the same way you'd write any other function. Just end the function name in -of.

whatever-of: func [a b] [
    print ["WHATEVER!" mold a mold b]
]

>> whatever of [1 2 3] $10
WHATEVER! [1 2 3] $10

Then it becomes a personal choice. You can use whatever-of or whatever of as you so choose. And if you want help, you can ask it on whatever-of and know what its applicable types are...

Is it a little bit kludgey? Sure, but what good is virtual binding if we don't use it!

I'm going to give it a shot. There's probably some easy optimizations for linking symbols like WHATEVER to be able to navigate to the symbol for WHATEVER-OF rapidly so you don't have to do any hashing to find it... at least for builtin symbols (e.g. put them sequentially in the SymId table).

UPDATE: It works! And the the sequential symbol optimization works too!

2 Likes

So this really takes a load off my mind. OF becomes extensible, and now we can apply it other places it makes sense.

I've fretted over historical Rebol putting question marks on things that didn't return logical true or false, just so they wouldn't conflict with variable names. And just generally stealing name space. Examples like HEAD above have other cases... like FIRST which makes a pretty good variable name frequently.

And then there's functions that aren't single arity trying to steal global names. But this works now! With MAX-OF defined, you can say:

>> a: 300
>> b: 1000

>> max: max of (a + 4) (b + 20)
== 1020

>> max
== 1020

With the generic retriggering possible, we open up the floor to a lot more things. How about our classic MAP-EACH problem?

What if EACH does the same thing... tacks -EACH onto whatever's on the left:

>> map each 'x [1 2 3] [x * 10]
== [10 20 30]

>> for each 'x [1 2 3] [print ["X is" x], x * 10]
X is 1
X is 2
X is 3
== 30

>> for 'x [1 thru 3] [print x]
1
2
3

So FOR EACH and MAP EACH can be completely disconnected from what FOR and EACH do in isolation! You're simply using EACH to methodize to special FOR-EACH and MAP-EACH functions with their own definitions.

This avoids some of the pretzel twists I was doing where EACH was a generator that had to feed to the generic FOR. That forced us to write:

>> for 'x each [1 2 3] [print ["X is" x], x * 10]
X is 1
X is 2
X is 3
== 30

Not only is that a little bit awkward to read, it also means that EACH had to make a generator function... adding overhead that FOR-EACH can avoid making.

Other Creative Ideas?

I'm sure there's tons of nutty directions to go with this concept:

if x is integer [  ; => integer? x
   print "How weird do you want to get?"
]

if y is even [  ; => even? x
   print "Pretty crazy!
]

That's not likely to be a good use of IS, because the way infix works you can't put complex expressions on the left without using parentheses. So this is a poor direction to go for the core. But hey, people might have ideas in their scripts for similar things.

Be on the lookout for ways to use this! I'm going to look into this FOR-EACH and MAP-EACH thing, it might be worth sacrificing the EACH word for this, vs. the old idea:

>> foo: each [1 2]

>> foo
== 1

>> foo
== 2

>> foo
** Error: End of enumeration reached

>> try foo
== ~null~

EACH was a kind of nice name for that generator, but having to pick another name in order to be able to write for each 'x and map each 'x while leaving the definitions of non-each FOR and MAP completely open is a tradeoff I'm willing to make.

It might actually read better as:

foo: generates each [1 2]

Then define GENERATES-EACH... ! This way plain GENERATES could be a dialected interpretation instead of an itemwise one:

bar: generates [1 to 100]

EACH could make arbitrary decisions beyond just tacking -EACH onto things. Who knows what might be useful:

>> [* 10] each [1 2]
== [10 20]
1 Like

Something else to note here is that OF can be written to apply refinements to the function it is calling:

whatever-of: func [a b :backwards] [
    if backwards [swap $a $b]
    print ["WHATEVER!" mold a mold b]
]

>> whatever of [1 2 3] $10
WHATEVER! [1 2 3] $10

>> whatever:backwards of [1 2 3] $10
WHATEVER! $10 [1 2 3]

This gives you a linguistic rearrangement. Because it would otherwise be:

>> whatever-of:backwards [1 2 3] $10
WHATEVER! $10 [1 2 3]

What sort of possibilities does that open up? I don't know. (I haven't figured out how to take advantage of refinements on infix either, but we can do that now too...)

1 Like