I have implemented Pure and Refined: Simplifying Refinements to One or Zero Args. It's so much better in so many ways...usermode and internal...that it's essentially a bug that Rebol ever did it any other way.
Despite being a significant change, it's pretty easy to update client code to. The majority of refinements don't actually take arguments in the first place--so there's nothing you need to do for those. If a refinement took an argument but didn't give a datatype for it, you now have to...it's the type block's presence that indicates it needs an argument at all. But maybe rather than just slapping [any-value!] on, it would be a good time to actually annotate with what the expected types are.
Because there's a goal to be able to emulate Rebol2/Red style in usermode, I decided that before committing the change I would update the Redbol emulation. This would process a function defined in the historical way to a new spec and body for the new rules. (Though note that Redbol will be an extension, and fundamental transformations like this would be rewritten as natives--in whole or in part.)
Nothing too fancy...it transforms the ANY-WORD! refinement arguments into SET-WORD!s, and then when the body runs it moves the value from the refinement into the local and sets the refinement's variable to true or false. A more complex one that allowed multiple refinement arguments would have to be variadic...I'm interested in the workings of that but it's low priority considering everything else.
Technique-wise, what I really like is how smooth it's getting to add set-ness/get-ness/lit-ness with COMPOSE:
insert body compose/deep [
(argument): :(refinement)
if not blank? :(refinement) [(refinement): true]
]
REFINEMENT is a PATH! in this case (as /foo is a path). While such paths with blanks at the head are inert, their GET-PATH! and SET-PATH! forms are active, so /foo: acts just like foo: and :/foo acts just like :foo. Scenarios like this one motivate that.
Another thing that I'm getting comfortable with is the no-op status of NULL when used with things like the value to APPEND or the value to KEEP. Historically I had some mixed feelings about it, but as NULL has become the true "non-thing" parallel to what NONE! was trying to signal, it would be a real waste to not be able to use its non-thing-ness. And when you're doing operations like a REPLACE in a block and want to actually replace with nothing, a BLANK! won't do. When you take that to its logical conclusion you realize that something like if keep match text! value [...] is a good thing. VOID! is there to fill in the gaps if you want to have an "ornery" error-triggering value.
Anyway, this is just an example of how the state of everyday coding is evolving for the better. It's a non-trivial function to write, but can be written without feeling in the dark about edge cases.
This includes the other parts of the transformation, like dealing with /extern
on function, or making it so you can mutate the body by tweaking the <const>
marker out of the spec for the body
parameter. It's actually working in practice, and I just ran the Rebol2-ish script to build hostilefork.com
with it!
rewrite-spec-and-body: function [
spec "(modified)" [block!]
body "(modified)" [block!]
][
; R3-Alpha didn't implement the Rebol2 `func [[throw catch] x y][...]`
; but it didn't error on the block in the first position. It just
; ignored it. For now, do the same in the emulation.
;
if block? first spec [take spec] ; skip Rebol2's [throw]
spool-descriptions-and-locals: does [
while [match [text! set-word!] first spec] [
spec: my next
]
]
while [not tail? spec] [
refinement: try match path! spec/1
; Refinements with multiple arguments are no longer allowed, and
; there weren't many of those so it's not a big deal. But there
; are *many* instances of the non-refinement usage of /LOCAL.
; These translate in Ren-C to the <local> tag.
;
if refinement = lit /local [
change spec <local>
refinement: _
]
spec: my next
if not refinement [continue]
if tail? spec [break]
spool-descriptions-and-locals
if tail? spec [break]
if not argument: match [word! lit-word! get-word!] spec/1 [
continue ; refinement didn't take args, so leave it alone
]
take spec ; don't want argument between refinement + type block
if not tail? spec [spool-descriptions-and-locals]
; may be at tail, if so need the [any-value!] injection
if types: match block! first spec [ ; explicit arg types
spec: my next
]
else [
insert/only spec [any-value!] ; old refinement-arg default
]
append spec as set-word! argument ; SET-WORD! in specs are locals
; Take the value of the refinement and assign it to the argument
; name that was in the spec. Then set refinement to true/blank.
;
; (Rebol2 missing refinements are #[none], or #[true] if present
; Red missing refinements are #[false], or #[true] if present
; Rebol2 and Red arguments to unused refinements are #[none]
; Since there's no agreement, Redbol goes with the Rebol2 way,
; since NONE! is closer to Ren-C's BLANK! for unused refinements.)
insert body compose/deep [
(argument): :(refinement)
if not blank? :(refinement) [(refinement): true]
]
if tail? spec [break]
spool-descriptions-and-locals
if tail? spec [break]
if extra: match any-word! first spec [
fail [
{Refinement} refinement {can't take more than one}
{argument in the Redbol emulation, so} extra {must be}
{done some other way. (We should be *able* to do}
{it via variadics, but woul be much more involved.)}
]
]
]
spec: head spec ; At tail, so seek head for any debugging!
; We don't go to an effort to provide a non-definitional return. But
; add support for an EXIT that's a synonym for returning void.
;
insert body [
exit: specialize 'return [set/any (lit value:) void]
]
append spec [<local> exit] ; FUNC needs it (function doesn't...)
]
; If a Ren-C function suspects it is running code that may happen more than
; once (e.g. a loop or function body) it marks that parameter `<const>`.
; That prevents casual mutations.
;
; !!! See notes in RESKINNED for why an ADAPT must be used (for now)
func-nonconst: reskinned [
body [block!] ; no <const> tag
] adapt :func []
function-nonconst: reskinned [
body [block!] ; no <const> tag
] adapt :function []
redbol-func: function [
return: [action!]
spec [block!]
body [block!]
][
spec: copy spec
body: copy body
rewrite-spec-and-body spec body
return func-nonconst spec body
]
redbol-function: function [
return: [action!]
spec [block!]
body [block!]
/with [object! block! map!] ; from R3-Alpha, not adopted by Red
/extern [block!] ; from R3-Alpha, adopted by Red
][
if block? with [with: make object! with]
spec: copy spec
body: copy body
rewrite-spec-and-body spec body
; The shift in Ren-C is to remove the refinements from FUNCTION, and
; put everything into the spec dialect...marked with <tags>
;
if with [
append spec compose [<in> (with)] ; <in> replaces /WITH
]
if extern [
append spec compose [<with> ((extern))] ; <with> replaces /EXTERN
]
return function-nonconst spec body
]