Survey of Redefining Datatype WORD!s

Here's a quick survey of what happens when you redefine integer!: tag!

R3-Alpha: Abstract, But Inaccurate

R3-Alpha remembers what you wrote in the spec.

r3-alpha>> integer!: tag!  ; INTEGER! now means TAG!

r3-alpha>> foo: func [x [integer!]] []
r3-alpha>> help foo
USAGE:
    FOO x

DESCRIPTION:
    (undocumented)
    FOO is a function value.

ARGUMENTS:
    x (integer!)

But showing what you said is disconnected from the behavior; as it built a concrete typeset by looking up the words you gave it:

r3-alpha>> foo 10
** Script error: foo does not allow integer! for its x argument

So it expects tags now, regardless of the integer! you see in HELP. Changing it back won't make a difference...because it already captured that concrete typeset:

r3-alpha>> integer!: lib/integer!

r3-alpha>> foo 10
** Script error: foo does not allow integer! for its x argument

Rebol2: Concrete

Rebol2 does not remember what you said at source level...it generates the type list from the typeset it actually uses for the function:

rebol2>> integer!: tag!

rebol2>> foo: func [x [integer!]] []

rebol2>> help foo
USAGE:
    FOO x

DESCRIPTION:
    (undocumented)
     FOO is a function value.

ARGUMENTS:
     x -- (Type: tag)

There we see it says Type: tag...no mention of the integer. So your help has the benefit of being truthful, while being rather concrete. Any flowery names or aliases you use will be gone.

Red: Crash, but basically it's the R3-Alpha approach

Just typing integer!: tag! will crash Red. That happens in both the stable and up-to-the-minute version.

But we can get the gist of what would happen if they fixed it by making another type.

red>> test!: integer!

red>> foo: func [x [test!]] []

red>> foo 10

red>> help foo
USAGE:
     FOO x

DESCRIPTION: 
     FOO is a function! value.

ARGUMENTS:
     x            [test!] 

If you change the definition of TEST!, this will not be seen by FOO, as it's only using [test!] as a "picture" of the specification. It concretized its parameter as whatever typeset TEST! made at the time the function was created:

red>> test!: tag!

red>> foo <nochange>
*** Script Error: foo does not allow tag for its x argument

Ren-C: Presently in same boat as R3-Alpha (hence sort of Red too)

It seems misleading to be capturing information for HELP which doesn't actually correspond to a "real thing". The type word winds up being completely disconnected from what the function does.

Could we (should we) react to changes in the source level construct?

It's fundamentally dangerous to let the user change the types of things and have the new types be what existing type checks use. If you have a native that only expects cells that have the integer layout and you let some other type of cell pass to that native, it will crash. User functions shouldn't crash the interpreter, but arbitrarily bad things could happen anyway if they get types they don't expect.

We might be able to lock words that are used in type descriptions... I mentioned this before as a possibility:

>> test!: integer!

>> foo: func [x [test!]] []

>> test!: tag!
** Error: Cannot modify TEST! word, locked for use in a type spec

This could address the potential instability issues of passing unexpected types. Though it would have to be a "semantically deep lock". For instance, if it were legal to say:

 my-types!: [integer! tag!]
 foo: func [x [my-types!]] []

...if the way types were interpreted was such that it would pull in types that were grouped in a block like that, then it would have to reach through and lock INTEGER! and TAG! too. This same "deep lock" notion would need to apply to any functions that might be used.

Also, it wouldn't necessarily assist with making the HELP coherent when reading it. Because when you're just looking at a word you don't know what context it is coming from. We could imagine that the user context might start out with no functions defined, and so its INTEGER! would be free to set to some other definition...but the lib context would have its INTEGER! locked down by the native definitions...

Is The Rebol2 Concrete Idea A Better Bet?

I gather that having HELP return a customized/arbitrary block was seen as a step toward having an expanded notion of type expression.

But as with most R3-Alpha "advancements" over Rebol2, it was a token gesture, and implementing any of it was left as an exercise to the reader. :roll_eyes:

Where I'd been leaning was the "deep lock" concept of hardening the definitions, and treating the block as a "type dialect". There could be some some amount of optimization (e.g. if you saw a simple list like [integer! tag!] to compress that as a bitset, and cache it). But anything more complex would have some cost...probably not much less than if you wrote the check yourself, but with the advantage of being expressed in the function spec.

But the "type dialect" idea hasn't seemed that promising. I actually tried to turn MATCH into a type dialect with tricks like [block!/2] to represent "block of length 2". This problem is a universe unto itself... the code for it was confusing, and writing it out looks better:

>> arg: [a b]
>> 2 = length of try match block! arg
== #[true]

>> arg: <not a block>
>> 2 = length of try match block! arg  ; the try is necessary here
== #[false]

>> 2 = length of match block! arg  ; try because LENGTH OF errors on NULL
** Error: NULL isn't valid for REFLECT, except for TYPE OF ()

So given that we're not doing any compile-time magic, I think type checks are better expressed as something like Eiffel preconditions. The language gives a lot of tools for that. And maybe we should do better at capturing that source to be part of the public interface.

Anyway...I still have more questions than answers...as usual. But thought I'd write this up as it's a fairly concrete survey of the state of things.

2 Likes