Реализация строгих сигнатур перегруженных методов, предотвращающих неправильное использование

#r #methods #signature #s4

#r #методы #подпись #s4

Вопрос:

Я пытаюсь реализовать разные версии метода, которые имеют похожие, но разные требования к вводу. Исходя из языков со статической и строгой типизацией, я пытаюсь написать эти методы таким образом, чтобы свести к минимуму возможность для целевых пользователей (это для пакета) использовать методы непреднамеренными способами. Разные версии метода имеют разное количество параметров, и я обнаружил, что поддержка версий метода с двумя или более параметрами позволяет вводить бессмыслицу в версию метода, которая ожидает только один параметр. Вот тривиальный пример:

 setGeneric(
  "foo",
  function(a, b) {
    standardGeneric("foo")
  })

setMethod(
  "foo",
  signature(a = "numeric"),
  function(a) {
    abs(a)
  })

setMethod(
  "foo",
  signature(a = "numeric", b = "numeric"),
  function(a, b) {
    abs(c(a, b))
  })
  

Он работает, как и ожидалось, со следующими входными данными (некоторые допустимы, некоторые нет, и выдают ошибки, как и должны):

 > foo(-1)
[1] 1

> foo(-1, -2)
[1] 1 2

> foo("cat")
 Error in (function (classes, fdef, mtable)  : 
  unable to find an inherited method for function ‘foo’ for signature ‘"character", "missing"> foo()
 Error in (function (classes, fdef, mtable)  : 
  unable to find an inherited method for function ‘foo’ for signature ‘"missing", "missing"> foo(-1, -2, "cat")
Error in foo(-1, -2, "cat") : unused argument ("cat")
  

Но тогда есть один сценарий, в котором он ведет себя не так, как должно быть приемлемо:

 > foo(-1, "cat")
[1] 1
  

Это вызывает первую сигнатуру метода и игнорирует второй параметр. Это потенциально серьезная логическая ошибка для пользователей, что является проблемой для меня, потому что мои целевые пользователи не являются компьютерными специалистами; большинство из них не понимают, что такое логические ошибки, насколько они опасны или как их отслеживать. Есть ли в R способ настроить методы так, чтобы этот последний пример foo(-1, "cat") выдавал ошибку, а не создавал у пользователя впечатление, что все хорошо?

Обратите внимание, что, хотя разные версии методов, над которыми я работаю, фундаментально связаны, фактические реализации для каждого из них сильно отличаются. Я мог бы использовать функции с необязательными аргументами, но это потребовало бы нескольких проверок для запуска совершенно разных больших фрагментов кода. Я надеялся избежать этого, потому что это не особенно чисто или элегантно.

Ответ №1:

Вы можете заставить его работать, если используете специальную "missing" подпись следующим образом:

 setGeneric(
  "foo",
  function(a, b) {
    standardGeneric("foo")
  })

setMethod(
  "foo",
  signature(a = "numeric", b = "missing"),
  function(a, b) {
    abs(a)
  })

setMethod(
  "foo",
  signature(a = "numeric", b = "numeric"),
  function(a, b) {
    abs(c(a, b))
  }
)
  

Проверка вызовов:

 foo(-1)
#[1] 1

foo(-1, -2)
#[1] 1 2

foo(-1, "cat")
#Error in (function (classes, fdef, mtable)  : 
#  unable to find an inherited method for function ‘foo’ for
#  signature ‘"numeric", "character"’ 

foo("cat")
# Error in (function (classes, fdef, mtable)  : 
#  unable to find an inherited method for function ‘foo’ for
#  signature ‘"character", "missing"’ 
  

Аналогично, foo() и foo(-1, -2, "cat") , как и раньше, завершается с ошибкой.

Как вы сами заметили, если вы добавите некоторые print инструкции в свои методы, вы увидите, что вызов foo(-1, "cat") отправляется вашему методу с одним аргументом. Причина, по которой ошибка не выдается, заключается в том, что обещание для b аргумента никогда не требуется и не оценивается. Я не очень хорошо разбираюсь в деталях правил диспетчеризации метода S4 в подобных случаях и в том, ожидалось ли это. Но в любом случае, в свете «отсутствующей» сигнатуры, я полагаю, что хорошей практикой является то, что аргументы методов всегда совпадают с аргументами общих методов; и я почти уверен, что что-то вроде R CMD check будет жаловаться, если они не совпадают.

Комментарии:

1. Я полагаю, что такое поведение было задумано разработчиками. Проверка R CMD не будет жаловаться на разные сигнатуры между методами, если все они являются общими аргументами; возможно, вы захотите использовать общий numeric метод и использовать его только для (numeric,numeric) примера (я полагаю, вы могли бы использовать ANY для этой цели)