Skinning REPLACE with Red's String PARSE Hack

Red made an unusual choice with their implementation of REPLACE when you use a pattern that's a block with a string. Instead of trying to stringify the block, they assume it's a PARSE rule:

red>> replace/case/all "aAbbabAAAa" ["Ab" | "Aa"] "-"
== "a-babAA-"

It seems like a bad idea to make the choice to do this based on the input type. What if you actually want to do this when the input is a BLOCK!? I'd prefer something like parse-replace (or if we ever allow the same word to be a function and a module, maybe parse.replace).

But honestly, I think that the answer is just to keep pushing on UPARSE itself to be slick enough that you wouldn't feel the need to reach for a shorthand like this. It's pretty easy to write as is:

>> parse/case "aAbbabAAAa" [
       try some thru [change ["Ab" | "Aa"] ("-")]
       accept <input>
   ]
== "a-babAA-"

That's probably reaching the limits of how short UPARSE can do an equivalent. Yet it's more powerful if you want to deviate or customize it, so I would reach for this more often than a limited REPLACE.

Still, Ren-C Does Backflips And Lets You Have It Your Way

So you should be able to adapt REPLACE to have Red's behavior if you want it.

Super easy. Barely an inconvenience!

replace: enclose :lib.replace func [
     f [frame!]
     <local> head tail rule
][
    if not all [
        match [text! binary!] f.target
        block? f.pattern
    ][
        return do f  ; use normal REPLACE semantics
    ]

    rule: if activation? :f.replacement '[  ; function generates replacement
        head: <here>
        change [f.pattern, tail: <here>] (
            apply/relax :f.replacement [const head, const tail]
        )
    ] else '[  ; replacement can be used as-is
        change f.pattern (f.replacement)
    ]

    apply :parse [/case f.case, f.target [
        while [thru rule] (
            if not f.all [return f.target]
        )
        to <end>
    ]]
    return f.target
]

It worked the first time I ran it!

There's so much interesting stuff going on here that it's hard to list it all. I can quickly hit some high points.

  • You don't have to repeat the interface of REPLACE. This is an ENCLOSE, so it just passes the frame built for LIB.REPLACE to the wrapper and lets it choose whether to run that frame as-is (or modified), or do its entirely own thing.

  • It uses the kickass new arity-2 WHILE combinator to great effect...simply iterating over the replacement rule.

  • Modern kickass APPLY for PARSE lets you put arguments in any order, and does refinements by name... here we put /CASE first because it's clearer... then passing the series and the rule.

  • It doesn't just run isotopic actions if they are passed, but it also optionally passes them the head and tail of where in the input is matched. If the function is arity-1, it just receives the head. If it's arity-0, it doesn't receive either. (This is due to APPLY's /RELAX that tolerates too many arguments.)

I wrote a little demo of the fancier function invocation:

>> data: "(real)1020(powerful)0304(magic)"

>> collect [
       replace/all data [between "(" ")"] func [head tail] [
            let item: copy/part head tail
            keep item
            if item = "(powerful)" [item: copy "(ren-c)"]
            return uppercase item
        ]
    ]
== ["(real)" "(powerful)" "(magic)"]

>> data
== "(REAL)1020(REN-C)0304(MAGIC)"

Not only that, but the references to the head and tail of the match are CONST...which prevents the replacement function from messing up the in-progress iteration of the series where the replace is happening. It only achieves modification by means of what result it synthesizes.

How about that?

cc: @Brett, @rgchris, @johnk

1 Like