Question

Inspired by this awesome post I felt like playing around with condition handling a bit more.

I think I got it all figured out for warning and error (didn't check message in detail yet), but out of pure interest at this point I wondered

  1. How the same paradigm could be used for alternative conditions (i.e. conditions simply inheriting from condition, not message, warning or error)

  2. When, if at all, the use of such alternative conditions would make any sense in the first place (and/or when signalCondition() would actually be used)

  3. How I can get a custom restart handler to "go back to the environment where the actual expression was evaluated" so it can "dynamically" work with/modify the original expression(s) (in frames further up the calling stack) in order to accomplish full flexibilty for condition handling


Example

Default condition constructor:

condition <- function(subclass, message, call = sys.call(-1), ...) {
  structure(
    class = c(subclass, "condition"),
    list(message = message, call = call),
    ...
  )
}

Custom alternative condition constructor:

custom_condition <- function(subclass, message, call = sys.call(-1), ...) {
  cond <- condition(subclass, message, call = call, ...)
  message(cond)
}

Example function that issues two alternative conditions at some point:

foo <- function() {
    for (ii in 1:3) {
        message(paste0("Run #: ", ii))
        if (ii == 1) {
            custom_condition(subclass="conditionFooExample",
                message="I'm just an example condition")
        } else if (ii == 3) {
            custom_condition(subclass="conditionFooExample2",
                message="I'm just another example condition")
        }
        ii
    }
}

Running without any wrappers such as tryCatch(), withCallingHandlers(), withRestarts() or the like:

foo()
Run #: 1
I'm just an example condition
Run #: 2
Run #: 3
I'm just another example condition

As I see it, not really useful as I would have gotten the same result when simply issuing the messages directly via message() in foo() instead of creating customs conditions via custom_condition().

This happens when wrapping it with tryCatch():

tryCatch(
    foo(),
    conditionFooExample=function(cond) {
        cond
    },
    conditionFooExample2=function(cond) {
        cond
    }
)
Run #: 1
<conditionFooExample in foo(): I'm just an example condition>

As we see, the first condition leads to kind of a "graceful exit". As I see it, as is right now, not really useful either because it resembles the behavior of warnings.

So I thought if alternative conditions are to be useful at all, then this whole process would probably need to involve custom condition handlers (e.g. for logging purposes: "write something back to a DB and continue") and something similar to invokeRestart("muffleWarning") (see ?condition and look for muffleWarning) in order to "really continue" (as opposed to "gracefully exiting").

So I gues this would then look something like this (PSEUDO CODE)?

withRestarts(
    tryCatch(
        foo(),
        conditionFooExample=function(cond) {
            cond      ## possibly 'message(cond)' instead?
            ## Do something meaningful before invoking a restart
            invokeRestart("conditionFooExample")
        },
        conditionFooExample2=function(cond) {
            cond      ## possibly 'message(cond)' instead?
            ## Do something meaningful before invoking a restart
            invokeRestart("conditionFooExample2")
        }
    ),
    conditionFooExample=function() {
        ## (Maybe) do something more before continuing

        ## Then continue:
        invokeRestart("muffleCondition")
    },
    conditionFooExample2=function() {
        ## (Maybe) do something more before continuing

        ## Then continue:
        invokeRestart("muffleCondition")
    }
)

Of course, there is no such thing as invokeRestart("muffleCondition") and I'm also not sure if this "architecture" would make any sense.

However, this is where question 3 came up: how do these restart handlers actually work, i.e. how do they get from their frame "one frame up" again?

Last not least I asked myself what would happened if I used signalCondition(cond) instead of message(cond) in custom_condition()

custom_condition <- function(subclass, message, call = sys.call(-1), ...) {
  cond <- condition(subclass, message, call = call, ...)
  signalCondition(cond)
}

foo()
Run #: 1
Run #: 2
Run #: 3

So, this seems even more "unnecessary" that before as the function simply terminates.

This is how it would look with tryCatch():

tryCatch(
    foo(),
    conditionFooExample=function(cond) {
        cond
    },
    conditionFooExample2=function(cond) {
        cond
    }
)

Run #: 1
<conditionFooExample in foo(): I'm just an example condition>

Well, the behavior itself does make sense, but I could have gotten the same result with an error condition.

Was it helpful?

Solution

You can get a hint about how restarts work from looking at the code to warning.

Here's a function providing two different restarts

f = function() {
    message("before")
    withRestarts({
        message("during")
        cond <- condition("restartable", "a restartable condition")
        signalCondition(cond)
    }, restart1 = function() {
        message("restarting 1")
    }, restart2 = function() {
        message("restarting 2")
    })
    message("after")
}

You can then choose to deal with your condition in different ways, e.g., not at all

> f()
before
during
after

catching the condition and terminating one way or another (your restarts are not available with tryCatch)

> tryCatch(f(), restartable=function(...) "did my best")
before
during
[1] "did my best"
> tryCatch(f(), restartable=function(cond) stop(conditionMessage(cond)))
before
during
Error in value[[3L]](cond) : a restartable condition

or using a calling handler to recover and try again

> withCallingHandlers(f(), restartable=function(...) invokeRestart("restart1"))
before
during
restarting 1
after
> withCallingHandlers(f(), restartable=function(...) invokeRestart("restart2"))
before
during
restarting 2
after

Not really sure if this helps or not; it would be really super if these facilities were used more extensively, especially signalling errors with subclasses.

OTHER TIPS

OK, I think I got the "how to make handlers 'dynamical'" figured out now.

The trick is to use sys.frame() if you want to "dynamically" change values in the frame where the original expression (argument expr) of withCallingHandlers() is evaluated.

To see what's going on, you can call sys.calls() and sys.call() inside your handler.

Note that it is not necessary to actually re-evaluate the original expression inside the handler after you made your changes! withCallingHandlers() takes care of that automatically (i.e. we could have omitted value <- eval(sys.call(-pos)) in the example below).


Example

Default condition constructor:

condition <- function(subclass, message, call=sys.call(-1), ...) {
  structure(
    class=c(subclass, "condition"),
    list(message=message, call=call),
    ...
  )
}

Custom condition signaler:

custom_triggerCondition <- function(subclass, message, call=sys.call(-1), ...) {
  cond <- condition(subclass, message, call=call, ...)
  signalCondition(cond)
}

Example function in which custom signaler is used:

myLog <- function(x) {
    if (is.numeric(x) && x == 5) {
        custom_triggerCondition(subclass="MyTriggerCondition", 
            message="This is an example alternative condition"
        )
    }
    log(x)
}
myLog(2)
[1] 0.6931472
myLog(5)
[1] 1.609438

Note how MyTriggerCondition differs from a message: unless we provide a handler, nothing happens and/or is signaled.

Let's see how things look when the conditions for signaling MyTriggerCondition are not met:

## No condition signaled //
res <- withCallingHandlers(
    {
        myLog(x=2)
    },
    ## Handler for message condition class 'MyTriggerCondition' // 
    MyTriggerCondition=function(cond) {
        ## Omitted for the sake of compactness. 
        ## See next call to 'withCallingHandlers()' for actual code
        return(NULL)
    }
)

This gives us:

res
[1] 0.6931472

Let's see how things look when the conditions for signaling MyTriggerCondition are met:

## Condition 'MyTriggerCondition' signaled //
res <- withCallingHandlers(
    {
        myLog(x=5)
    },
    ## Handler for message condition class 'MyTriggerCondition' // 
    MyTriggerCondition=function(cond) {
        message("handler> System calls:")
        syscalls <- sys.calls()
        print(syscalls)
        message("-------------------------------------------------------------")
        message("handler> This is what happened:")
        message(conditionMessage(cond))
        message("handler> Condition class:")
        print(class(cond))
        message("handler> Now I'm handling the condition by changing 'x' to 10:")
        pos <- length(syscalls) - 2
        message(c(
                paste0("handler> Need to go back '", pos, "'\n"),
                "handler> frames for the desired system call:")
        )
        message(paste0("handler> Getting system call [-", pos, "]:"))
        syscall <- sys.call(-pos)
        print(syscall)
        message(paste0("handler> Evaluating system call [-", pos, "]:"))
        value <- eval(sys.call(-pos))
        message("handler> Evaluation result:")
        print(value)
        message(paste0("handler> Getting system frame [-", pos, "]:"))
        sysframe <- sys.frame(-pos)
        print(sysframe)
        message(paste0("handler> Listing content of system frame [-", pos, "]:"))
        print(ls(sysframe))
        message(paste0("handler> Getting old value of 'x' in system frame [-", pos, "]:"))
        print(sysframe$x)
        message(paste0("handler> Overwriting content of system frame [-", pos, "]:"))
        message("handler> 'x' is set to 10")
        sysframe$x <- 10
        message(paste0("handler> Inspect overwriting result in system frame [-", pos, "]:"))
        print(sysframe$x)
        message(paste0("handler> Re-evaluating system call [-", pos, "] (locally):"))
        value <- eval(sys.call(-pos))
        message("handler> Re-evaluation result (locally):")
        print(value)
        message(c(
            "handler> IMPORTANT:\n",
            "handler> Note how the expression evaluated is still\n",
            paste0("handler> myLog(x=5): ", value, "when re-evaluated 'locally'\n"),
            "handler> inside the handler function.\n",
            "handler> But also note that the **actual** return value\n",
            "handler> of the handler or 'withCallingHandlers()'\n",
            "handler> will be myLog(x=10): 2.302585 as the handler\n",
            "handler> changed the value of 'x'!"
            )
        )
        return(value)
    }
)

Let's run this:

handler> System calls:
[[1]]
withCallingHandlers({
    myLog(x = 5)
}, MyTriggerCondition = function(cond) {
    message("handler> System calls:")
    syscalls <- sys.calls()
    print(syscalls)
    message("-------------------------------------------------------------")
    message("handler> This is what happened:")
    message(conditionMessage(cond))
    message("handler> Condition class:")
    print(class(cond))
    message("handler> Now I'm handling the condition by changing 'x' to 10:")
    pos <- length(syscalls) - 2
    message(c(paste0("handler> Need to go back '", pos, "'\n"), 
        "handler> frames for the desired system call:"))
    message(paste0("handler> Getting system call [-", pos, "]:"))
    syscall <- sys.call(-pos)
    print(syscall)
    message(paste0("handler> Evaluating system call [-", pos, 
        "]:"))
    value <- eval(sys.call(-pos))
    message("handler> Evaluation result:")
    print(value)
    message(paste0("handler> Getting system frame [-", pos, "]:"))
    sysframe <- sys.frame(-pos)
    print(sysframe)
    message(paste0("handler> Listing content of system frame [-", 
        pos, "]:"))
    print(ls(sysframe))
    message(paste0("handler> Getting old value of 'x' in system frame [-", 
        pos, "]:"))
    print(sysframe$x)
    message(paste0("handler> Overwriting content of system frame [-", 
        pos, "]:"))
    message("handler> 'x' is set to 10")
    sysframe$x <- 10
    message(paste0("handler> Inspect overwriting result in system frame [-", 
        pos, "]:"))
    print(sysframe$x)
    message(paste0("handler> Re-evaluating system call [-", pos, 
        "] (locally):"))
    value <- eval(sys.call(-pos))
    message("handler> Re-evaluation result (locally):")
    print(value)
    message(c("handler> IMPORTANT:\n", "handler> Note how the expression evaluated is still\n", 
        paste0("handler> myLog(x=5): ", value, "when re-evaluated 'locally'\n"), 
        "handler> inside the handler function.\n", "handler> But also note that the **actual** return value\n", 
        "handler> of the handler or 'withCallingHandlers()'\n", 
        "handler> will be myLog(x=10): 2.302585 as the handler\n", 
        "handler> changed the value of 'x'!"))
    return(value)
})

[[2]]
myLog(x = 5)

[[3]]
custom_triggerCondition(subclass = "MyTriggerCondition", message = "This is an example alternative condition")

[[4]]
signalCondition(cond)

[[5]]
(function (cond) 
{
    message("handler> System calls:")
    syscalls <- sys.calls()
    print(syscalls)
    message("-------------------------------------------------------------")
    message("handler> This is what happened:")
    message(conditionMessage(cond))
    message("handler> Condition class:")
    print(class(cond))
    message("handler> Now I'm handling the condition by changing 'x' to 10:")
    pos <- length(syscalls) - 2
    message(c(paste0("handler> Need to go back '", pos, "'\n"), 
        "handler> frames for the desired system call:"))
    message(paste0("handler> Getting system call [-", pos, "]:"))
    syscall <- sys.call(-pos)
    print(syscall)
    message(paste0("handler> Evaluating system call [-", pos, 
        "]:"))
    value <- eval(sys.call(-pos))
    message("handler> Evaluation result:")
    print(value)
    message(paste0("handler> Getting system frame [-", pos, "]:"))
    sysframe <- sys.frame(-pos)
    print(sysframe)
    message(paste0("handler> Listing content of system frame [-", 
        pos, "]:"))
    print(ls(sysframe))
    message(paste0("handler> Getting old value of 'x' in system frame [-", 
        pos, "]:"))
    print(sysframe$x)
    message(paste0("handler> Overwriting content of system frame [-", 
        pos, "]:"))
    message("handler> 'x' is set to 10")
    sysframe$x <- 10
    message(paste0("handler> Inspect overwriting result in system frame [-", 
        pos, "]:"))
    print(sysframe$x)
    message(paste0("handler> Re-evaluating system call [-", pos, 
        "] (locally):"))
    value <- eval(sys.call(-pos))
    message("handler> Re-evaluation result (locally):")
    print(value)
    message(c("handler> IMPORTANT:\n", "handler> Note how the expression evaluated is still\n", 
        paste0("handler> myLog(x=5): ", value, "when re-evaluated 'locally'\n"), 
        "handler> inside the handler function.\n", "handler> But also note that the **actual** return value\n", 
        "handler> of the handler or 'withCallingHandlers()'\n", 
        "handler> will be myLog(x=10): 2.302585 as the handler\n", 
        "handler> changed the value of 'x'!"))
    return(value)
})(list(message = "This is an example alternative condition", 
    call = myLog(x = 5)))

-------------------------------------------------------------
handler> This is what happened:
This is an example alternative condition
handler> Condition class:
[1] "MyTriggerCondition" "condition"         
handler> Now I'm handling the condition by changing 'x' to 10:
handler> Need to go back '3'
handler> frames for the desired system call:
handler> Getting system call [-3]:
myLog(x = 5)
handler> Evaluating system call [-3]:
handler> Evaluation result:
[1] 1.609438
handler> Getting system frame [-3]:
<environment: 0x000000001eb17c98>
handler> Listing content of system frame [-3]:
[1] "x"
handler> Getting old value of 'x' in system frame [-3]:
[1] 5
handler> Overwriting content of system frame [-3]:
handler> 'x' is set to 10
handler> Inspect overwriting result in system frame [-3]:
[1] 10
handler> Re-evaluating system call [-3] (locally):
handler> Re-evaluation result (locally):
[1] 1.609438
handler> IMPORTANT:
handler> Note how the expression evaluated is still
handler> myLog(x=5): 1.6094379124341when re-evaluated 'locally'
handler> inside the handler function.
handler> But also note that the **actual** return value
handler> of the handler or 'withCallingHandlers()'
handler> will be myLog(x=10): 2.302585 as the handler
handler> changed the value of 'x'!

The value of res now is:

res
[1] 2.302585
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top