The same four laws test ordinary double vectors and an S7
ReadDepth class. For an introduction to interfaces and
traits, see Behavioral Contracts
on S7.
Many algorithms need only a length, a way to slice, and access to
values. Both ordinary double vectors and ReadDepth objects
implement those operations. The class validator keeps positions and
depths aligned, while the interface describes the behavior consumers
need.
vec_length <- new_generic("vec_length", "x")
vec_slice <- new_generic("vec_slice", "x", function(x, i) S7_dispatch())
vec_values <- new_generic("vec_values", "x")
VectorLike <- new_interface(
"VectorLike",
generics = list(
length = interface_requirement(vec_length, returns = class_integer),
slice = interface_requirement(vec_slice, args = list(i = class_integer)),
values = interface_requirement(vec_values, returns = class_double)
)
)
ReadDepth <- new_class(
"ReadDepth",
properties = list(position = class_integer, depth = class_double),
validator = function(self) {
if (length(self@position) != length(self@depth)) {
"@position and @depth must have the same length"
}
}
)
method(vec_length, ReadDepth) <- function(x) length(x@depth)
method(vec_slice, ReadDepth) <- function(x, i) {
ReadDepth(position = x@position[i], depth = x@depth[i])
}
method(vec_values, ReadDepth) <- function(x) x@depth
method(vec_length, class_double) <- function(x) length(x)
method(vec_slice, class_double) <- function(x, i) x[i]
method(vec_values, class_double) <- function(x) x
coverage <- ReadDepth(position = 1:5, depth = c(12, 15, 9, 20, 17))
implements(coverage, VectorLike)
#> [1] TRUE
implements(class_double, VectorLike)
#> [1] TRUEA function can depend on this small protocol without knowing how the object is represented internally.
A protocol author can publish a function returning a named list of laws. Implementation authors supply a constructor; the laws compare each result with the reference values passed to that constructor.
The domain here is unnamed vectors of finite doubles between -10 and
10, with positive, in-range integer indices. gen_double()
expands its bounds with size and shrinks toward zero. Empty vectors and
selections are included; indices may repeat or appear out of order. The
generator mixes ordered subsequences, permutations without replacement,
and repeated selections. Missing values, names, and negative indices are
outside this example.
gen_bind() constructs the object and an index generator
from the reference values. When those values shrink, it rebuilds both,
preserving object validity and index bounds. Bounds come from the
reference data rather than the method being tested. Inside the contract
mask, base::length() keeps the reference calculation
separate from the interface’s length alias.
vector_laws <- function(make, element = gen_double(-10, 10)) {
values <- gen_vector(element, max = 6L)
cases <- gen_bind(values, function(values) {
indices <- if (length(values) == 0L) {
gen_constant(integer())
} else {
gen_choice(
gen_subsequence(seq_along(values)),
gen_sample(seq_along(values)),
gen_vector(gen_element(seq_along(values)), max = 6L)
)
}
gen_product(
x = gen_constant(make(values)),
values = gen_constant(values),
i = indices
)
})
list(
values = new_law("values preserve constructor input", list(input = cases),
function(input) with(VectorLike, {
identical(vec_values(input$x), input$values)
})),
length = new_law("length agrees with constructor input", list(input = cases),
function(input) with(VectorLike, {
identical(vec_length(input$x), base::length(input$values))
})),
slice_values = new_law("slicing preserves selected values and order", list(input = cases),
function(input) with(VectorLike, {
identical(vec_values(vec_slice(input$x, input$i)), input$values[input$i])
}),
classify = function(input) c(
if (length(input$values) == 0L) "empty" else "nonempty",
if (anyDuplicated(input$i) > 0L) "repeated",
if (is.unsorted(input$i)) "reordered",
if (!is.unsorted(input$i, strictly = TRUE)) "subsequence",
if (length(input$i) == length(input$values) && !anyDuplicated(input$i)) "permutation",
if (any(input$values != trunc(input$values))) "fractional"
),
min_coverage = c(empty = 0.05, nonempty = 0.5, repeated = 0.05,
reordered = 0.1, subsequence = 0.2, permutation = 0.2,
fractional = 0.5)
),
slice_length = new_law("slice length matches the index count", list(input = cases),
function(input) with(VectorLike, {
identical(vec_length(vec_slice(input$x, input$i)), base::length(input$i))
}))
)
}lapply() runs the same four laws against both
representations.
implementations <- list(
numeric = identity,
read_depth = function(values) ReadDepth(position = seq_along(values), depth = values)
)
vector_results <- lapply(implementations, function(make) {
lapply(vector_laws(make), check_law, tests = 100L, seed = 1L)
})
sapply(vector_results, function(results) {
vapply(results, function(result) result@status, character(1))
})
#> numeric read_depth
#> values "passed" "passed"
#> length "passed" "passed"
#> slice_values "passed" "passed"
#> slice_length "passed" "passed"The slicing law classifies inputs as empty or nonempty, records
fractional values, and labels repeated, reordered, subsequence, and
permutation selections. Labels describe the indices themselves, so they
can overlap: an empty selection is a subsequence, and a full ordered
selection is also a permutation. Each label counts once per accepted
case. Its min_coverage requirements use proportions:
reordered = 0.1 asks for reordered indices in at least 10%
of cases.
gen_sample() fixes cardinality and shrinks toward
earlier source positions, swapping selected positions to preserve
uniqueness. gen_subsequence() grows length with size and
keeps source order during shrinking. Both sample positions; equal values
at different positions may still appear together. Sampling with
replacement uses gen_vector() and
gen_element().
vector_results$numeric$slice_values@coverage
#> label count proportion minimum met
#> 1 empty 16 0.16 0.05 TRUE
#> 2 nonempty 84 0.84 0.50 TRUE
#> 3 repeated 17 0.17 0.05 TRUE
#> 4 reordered 36 0.36 0.10 TRUE
#> 5 subsequence 57 0.57 0.20 TRUE
#> 6 permutation 57 0.57 0.20 TRUE
#> 7 fractional 84 0.84 0.50 TRUEFixing size at zero exercises only empty vectors. The predicate passes, but the coverage requirements prevent the run from passing:
empty_only <- check_law(vector_laws(identity)$slice_values,
tests = 10L, seed = 1L, max_size = 0L)
empty_only
#> Law 'slicing preserves selected values and order' passed 10 tests but missed coverage requirements (seed 1).
#> Case coverage (10 accepted cases):
#> "nonempty": 0/10 (0%; minimum 50% unmet)
#> "repeated": 0/10 (0%; minimum 5% unmet)
#> "reordered": 0/10 (0%; minimum 10% unmet)
#> "fractional": 0/10 (0%; minimum 50% unmet)
#> "empty": 10/10 (100%; minimum 5%)
#> "subsequence": 10/10 (100%; minimum 20%)
#> "permutation": 10/10 (100%; minimum 20%)This follows the test-data classification discussed by Claessen
and Hughes (2000), §2.4. These minima describe observed proportions
within the chosen test budget; they carry no statistical confidence
guarantee. Discards, errors, and shrink evaluations contribute no
counts. A falsifying generated case does count, and a run that ends
early reports partial coverage alongside its primary failure. See
new_law() for classifier requirements and result
fields.
This subclass inherits the correct length and value methods, but its
slice method reverses the requested order. All required methods are
available, so implements() succeeds. Its slices also have
valid representations and the expected value type and length. This run
uses integer-valued doubles to keep the counterexample easy to read.
ReversedDepth <- new_class("ReversedDepth", parent = ReadDepth)
method(vec_slice, ReversedDepth) <- function(x, i) {
ReadDepth(position = x@position[rev(i)], depth = x@depth[rev(i)])
}
implements(ReversedDepth, VectorLike)
#> [1] TRUE
whole_numbers <- gen_map(gen_integer(-10L, 10L), as.double, prototype = double())
broken_results <- lapply(
vector_laws(function(values) ReversedDepth(position = seq_along(values), depth = values),
element = whole_numbers),
check_law, tests = 100L, seed = 1L
)
vapply(broken_results, function(result) result@status, character(1))
#> values length slice_values slice_length
#> "passed" "passed" "falsified" "passed"Only the law about selected values and their order fails. The reduced input still produces a valid slice, but its values differ from the reference slice.
failure <- broken_results$slice_values
failure
#> Law 'slicing preserves selected values and order' was falsified after 3 attempts and 1 shrinks (seed 1).
#> The law returned FALSE.
#> Shrinking stopped: no child of this counterexample preserves the failure.
#> Smallest counterexample found:
#> List of 1
#> $ input:List of 3
#> ..$ x : <ReversedDepth>
#> .. ..@ position: int [1:2] 1 2
#> .. ..@ depth : num [1:2] 0 -1
#> ..$ values: num [1:2] 0 -1
#> ..$ i : int [1:2] 1 2
#> Case coverage (3 accepted cases; partial run):
#> "nonempty": 1/3 (33.3%; minimum 50% unmet)
#> "repeated": 0/3 (0%; minimum 5% unmet)
#> "reordered": 0/3 (0%; minimum 10% unmet)
#> "fractional": 0/3 (0%; minimum 50% unmet)
#> "empty": 2/3 (66.7%; minimum 5%)
#> "subsequence": 3/3 (100%; minimum 20%)
#> "permutation": 3/3 (100%; minimum 20%)
example <- failure@counterexample@minimal$input
example$values
#> [1] 0 -1
example$i
#> [1] 1 2
with(VectorLike, vec_values(vec_slice(example$x, example$i)))
#> [1] -1 0
example$values[example$i]
#> [1] 0 -1The result records the inputs and run parameters needed to replay the failure: