rdesign-by-contractassertthat

Storing input value to check wether postcondition holds true when applying Design-by-Contract


I make use of the assertthat package quite often to check postconditions in functions. When reading more about the idea of Design by Contract I stumbled upon the idea to make checks of output in comparison to input values.

The most simple example is the following:

toggle <- function(x)!x

One can immediately state that x == !old_x must always be true. (old_x stands for the value of x before evaluation.)

(Of course this example is oversimplified and the postcondition does not add more useful information for humans or computers. A more useful example is on the bottom of the question..)

So I can extend my toggle function as follows to check that condition with every call:

toggle <- function(x){
  old_x <- x
  x <- !x
  assertthat::assert_that(x == !old_x)
  return(x)
}

This works of course but I wondered if there's another way to access the value of old_x without explicitely store it (or the result) under a new name. And without splitting the postcondition-checking code to the top and bottom of the function. Something along the line of how R evaluates function calls..

One attempt I can think of is to use sys.call and eval.parent to access to the old values:

toggle <- function(x){
  x <- !x
  .x <- eval.parent(as.list(sys.call())[[2]])
  assertthat::assert_that(x == !.x)
  return(x)
}

This works, but I still need to assign a new variable .x and also the subsetting with [[2]] is not flexible yet. However writing it like assertthat::assert_that(x == !eval.parent(as.list(sys.call())[[2]]) does not work and playing around with the search levels of sys.call(-1 ..) did not help.


Another (a bit more useful) example where the postcondition adds some information:

increment_if_smaller_than_2 <- function(x){
  old_x <- x
  x <- ifelse(x < 2, x <- x + 1, x)
  assertthat::assert_that(all(x >= old_x))
  return(x)
}

Any hints?


Solution

  • You can access the old parameter-values by accessing it via the parent environment. For this solution to work, you need to introduce new variable(s) for the return-result, i.e. retval, to prevent re-assignments to method-params. IMHO this isn't a serious drawback, since it's good programming-style not to overwrite method-parameters anyway. You could i.e. do the following:

    test <- function(.a) {
      retval <- 2 * .a
      assertthat::assert_that(abs(retval) >= abs(.a))
      return(retval)
    }
    
    a <- 42
    test(a)
    # [1] 84
    

    If you would like to take it a step further and submit the assertion-function dynamically you could do that as follows:

    test_with_assertion <- function(.a, assertion) {
      retval <- 2 * .a
      assertthat::assert_that(assertion(retval, eval.parent(.a)))
      return(retval)
    }
    
    a <- 42
    test_with_assertion(a, function(new_value, old_value) 
      abs(new_value) >= abs(eval.parent(old_value)) )
    # [1] 84
    

    Does this do, what you intended to do?