Syntax for Typechecked Values

One of the things you can do with Generalized Accessors, is that assignments to any value can be typechecked. You get this "for free" because accessors pair a function with a variable slot, and that function can use typechecking:

Writing the accessor boilerplate is laborious...

checked: accessor lambda [^:value [your! types! here!] <static> actual] [
    either value [actual: value.] [actual.]  ; terminal dot allows unset vars
]

If you want to be truly generic and support storing actions in the variable, you need slashes:

checked: accessor lambda [^:/value [your! types! here!] <static> actual] [
    either value [/actual: value.] [actual.]
]

Not only is that a nuisance to get typechecking, it's also not native code, so you're running that EITHER and its branches.

So we could make it a feature of ACCESSOR, maybe just when you give it a block...and it could do a particularly cheap and special form. I think this is actually the right way to go about it... it would put a "pairing" cell in the variable's location, which would pair up the stored value with a PARAMETER! (which does preoptimized calculations on the typeset:

checked: accessor [your! types! here!]
checked: initial-value

That's all fine and good but seems we need a shorter way to say it...that doesn't repeat the variable name and doesn't have to say ACCESSOR.

We want whatever this is to still get collected as a SET-WORD! (if it is one), so this probably has to be something like:

checked: typed [your! types! here!] initial-value

I don't hate it. Alternate ideas?

Interaction With Syntax For Locals

This would now be able to work:

foo: func [x [text!] <local> y [integer!]] [...]

This would need to be lenient in terms of letting the Y be unset prior to its first assignment. So it can contain nothing at the outset, but any future assignments have to assign integers.

But this would get a bit of a problem if we want to mix it with a form that lets you assign the local:

foo: func [x [text!] <local> y: [integer!]] [...]

That would give you Y with the BLOCK! [integer!] in it. You'd have to say:

foo: func [x [text!] <local> y: typed [integer!] 10] [...]

We could keep the historical "groups to initialize" idea

foo: func [x [text!] <local> y (10)] [...]  ; not typechecked

foo: func [x [text!] <local> y [integer!] (10)] [...]  ; typechecked

And maybe the idea of making a place to put the type constraints vindicates that syntax for this context.

Typechecking will slow down the code. But it would be running through the same checking mechanisms that functions use. As that code got better, so would this.

There could be some sort of "enable typechecking only in debug mode" property of functions where you could turn it on or off.

I don’t hate it either. But I do have an alternate thought: why not use a sigil? You were wondering what to do with ~~, and this looks like a good opportunity to use it:

checked: initial-value ~~ [your! types! here!]

It doesn’t look that bad to me. Or it could be reversed:

checked: [your! types! here!] ~~ initial-value

(Even better would be ::, which has already been used for the purpose in Haskell. But sadly that’s no longer available.)

Mechanically whatever this is has to be in the position after the SET-WORD!

Setting things to the accessor is a special out-of-band operation, that by definition can't be done through the assignment of a normal value:

set-accessor $checked func [...] [...]

So the trick in ACCESSOR is to get the left hand side literally and assign it:

accessor: enfix func [
    return: [~]
    var [set-word! set-tuple!]  ; support SET-GROUP! at some point
    action [action?]
][
    set-accessor var get $action
]

Currently I'm having it return nothing. But if it wanted to be "consistent" with what drops out of other SET-WORD!s it would run the accessor and give back whatever the accessor wants to say the value is now.

Actually, these situations that do "weird" things should probably be giving back antiform TAG!. So you still get the ornery response, but with a clarifying message about the weird thing that happened.

accessor: enfix func [
    return: [hole?]
    var [set-word! set-tuple!]
    action [action?]
][
    set-accessor var get $action
    return ~<set-accessor succeeded>~  ; mentioning SET-ACCESSOR is useful
]

Anyway... you only get that one unit of lookback to find the variable to bless with this property. So we can't do:

Whatever happens needs the form:

checked: xxx ...

xxx takes 3 arguments: one from the left, two from the right.

So we could put the types after the value.

checked: xxx initial-value [your! types! here!]

And we could require a sigil of some kind with that...as a literal throwaway parameter to help you find the separation point:

checked: xxx initial-value :: [your! types! here!]

But so long as the XXX is there, I don't see the value in it.

~~ and :: are still available, and we could do:

checked: :: [your! types! here!] initial-value

checked: ~~ [your! types! here!] initial-value

But I don't particularly like the :: after the :

We do control the gathering, so could theoretically make it look for non-SET-WORD! patterns, and the :: sigil can't be reassigned, letting the :: imply assignment:

checked :: [your! types! here!] initial-value

But that's one of those irregularities that doesn't really buy enough to make it worth it.

TYPED is a Rebol-y enough thing that you can abbreviate to T or something if you like.

T: typed.

checked: T [your! types! here!] initial-value

But usually there's higher return factorings you can do if you're repeating yourself a lot.

Ah, I didn’t realise it had to be next to the SET-WORD!. I guess checked: ~~ type! value is acceptable, though I don’t love it.

But… I just had another idea: now that we have CHAIN!, why not co-opt the SET-WORD! itself?

checked:your!:types!:here!: initial-value

Or perhaps, if this is syntactically valid (which I hope it is):

checked:[your! types! here!]: initial-value

I actually really like this idea, for a couple of reasons:

  • Within Ren-C, one could say that typechecking is to values as refinements are to functions — a small modification to their behaviour. From this point of view, it makes sense to make their syntax consistent.
  • Outside Ren-C, colons are very widely used for type signatures. This is consistent with that too.

On the other hand, this syntax could be used for something already. Or simply impossible to fit into current Ren-C. I don’t know.

EDIT: Another thought on this — it could be extended to GET-WORD!s too, if that syntax isn’t already taken. :value:type! could mean, ‘get value and assert it has type type!’. Could be useful to have for some tricky situations.

That would be possible. It looks a bit like a function call notation being a CHAIN!, but by ending in : it is differentiated.

Syntax coloring could make it clear (through bolding the whole thing) that it was an assignment.

It would help with the locals problem...

foo: func [x [text!] <local> y:[integer!]: 10] [...]`

Though it deviates from the argument spec which just uses space in the dialect, it's a different section.

We can try it. But TYPED could also be available if one wanted it (or wanted to build some variation that was similar-to, but not exactly the same).

1 Like

Well, it could be made an option in the argument spec too, for those who like the syntax (or dislike square brackets):

foo: func [x:text! <local> y:integer!: 10] […]

I like the lightness of it.

1 Like

In terms of "Object Field Gathering Rules", this means taking the old rule:

"look for length 2 CHAIN! with WORD! at head and terminal BLANK!"  ("SET-WORD?")

And turns it into:

"look for any length CHAIN! with WORD! at head and BLANK! at tail"

For this to be useful, the meaning of all WORD!-headed, BLANK!-tailed CHAIN!s (conceptually SET-CHAIN!, though that's not a fundamental type, could be a constraint e.g. SET-CHAIN?)...must have the contents of the chain pertain to some constraint or property of that WORD! at the head.

(As is always the case, these are evaluator rules. In your dialect, means whatever you want.)

Curiously, you can change the typing of your variables midstream in code if you want.

 var:integer!: 10
 var: 20
 ; var: "hello" would be an error

 var:[integer! text!]: var  ; should there be a syntax for retype, but keep value?
 var: 30
 var: "goodbye"
1 Like

It may be that TYPED not take an initial value by default, but typecheck the value in its current state if not unset...

var:integer!: 10
var: 20
; var: "hello" would be an error

var: typed [integer! text!]  ; == 20 ?
assert [var = 20]
var: 30
var: "goodbye"

Also better than a CHAIN! when you have line breaks and want to explain things:

 state: typed [
     integer!    ; if state is integer, we are exiting (UNIX exit status)
     tag!        ; a TAG! is used for non-terminal states
     issue!      ; issue is used for terminal states
     ...
 ]

If that goes on for a while, you might be better to not have a dangling thing to assign at the end bracket, but do a separate assignment.

1 Like

So there are two senses of type checking, the checking of what you get into that function, and whether you want that to be applied to changes to the frame variable once you get it.

(The practice of using function arguments destructively is part of Rebol's pursuit-of-"efficiency" history, although it makes debugging harder.)

Rebol has only had the first form (checking arguments) with no checking after that.

Now that we'll be having the ability to do the checking after, this could be a notational way of distingiushing the two, saying you actually want modifications to keep the type of the argument in that set. :man_shrugging:

2 Likes

Hmm… yeah, that seems reasonable to me.

It looks like Red has looked into "typed contexts", and discussed some of the syntax issues there:

Seems that their lack of CHAIN! to unify the type with the declaration forces them to choose between various bad options.