Когда → я действительно полезен в ракетке?

Я проходил Контракты в Руководстве по сборке для ракеток.

Конструкция ->i позволяет помещать произвольные ограничения на вход/выход функции.

Например, я мог бы иметь функцию unzip, которая берет список пар и возвращает два списка. Используя контракты, я мог бы подтвердить, что каждый элемент in-list является парой и что out-lists имеют соответствующие элементы.

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

Каков конкретный пример того, где код каким-то образом улучшается по контракту более сложным, чем простые типы?

Ответ 1

Как вы описали, практически любая проверка, которая может быть выполнена в ->i, может выполняться внутри самой функции, но опять же любая проверка, выполняемая контрактами, может, по большей части, выполняться внутри самих функций. Кодирование информации в контракте дает несколько преимуществ.

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

Это наиболее очевидно с ->i, когда контракт должен указывать зависимости внутри аргументов, предоставляемых функции. Например, у меня есть библиотека коллекций, которая включает в себя функцию subsequence. Он принимает три аргумента, последовательность, начальный индекс и конечный индекс. Это контракт, который я использую для его защиты:

(->i ([seq sequence?]
      [start exact-nonnegative-integer?]
      [end (start) (and/c exact-nonnegative-integer? (>=/c start))])
     [result sequence?])

Это позволяет мне явно указать, что конечный индекс должен быть больше или равен начальному индексу, и мне не нужно беспокоиться о проверке этого инварианта внутри моей функции. Когда я нарушаю этот контракт, я получаю приятное сообщение об ошибке:

> (subsequence '() 2 1)
subsequence: contract violation
  expected: (and/c exact-nonnegative-integer? (>=/c 2))
  given: 1
  which isn't: (>=/c 2)

Он также может использоваться для обеспечения более сложных инвариантов. Я также определяю свою собственную функцию map, которая, подобно встроенной в Racket map, поддерживает переменное количество аргументов. Процедура, предоставляемая map, должна принимать такое же количество аргументов, сколько предоставляется. Я использую следующий контракт для map:

(->i ([proc (seqs) (and/c (procedure-arity-includes/c (length seqs))
                          (unconstrained-domain-> any/c))])
     #:rest [seqs (non-empty-listof sequence?)]
     [result sequence?])

Этот контракт обеспечивает две вещи. Прежде всего, аргумент proc должен принимать такое же количество аргументов, что и последовательности, как упоминалось выше. Кроме того, он также требует, чтобы эта функция всегда возвращала одно значение, так как функции Racket могут возвращать несколько значений.

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

Вы всегда хотите кодировать каждый отдельный инвариант функции в контракт? Возможно нет. Но если вам нужен дополнительный уровень контроля, ->i доступен.