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

Я пытаюсь использовать код "хороший стиль Haskell", поэтому я стараюсь следовать типичным стандартам кодирования, которые я нахожу. Кроме того, компиляция с использованием -Wall и -Werror, к которой я привык, когда вы приходите из C. Одно из предупреждений, которые я часто получаю, это "Связывание верхнего уровня без сигнатуры типа", а затем компилятор сообщает мне, что такое подпись типа.

Мне не хватает того, что явно имеет явная подпись типа. В качестве конкретного примера:

-- matchStr :: String -> String ->  Maybe (String)
matchStr str s
        | isPrefixOf str s = Just(drop (length str) s)
        | otherwise = Nothing

Теперь, что произойдет, если я хочу изменить тип от String до ByteString для повышения производительности; Мне придется импортировать пакет ByteString и использовать квалифицированную версию некоторых функций. Никаких других изменений не требуется. Если у меня есть подпись типа, тогда мне также нужно изменить это, и все же компилятор Haskell заметит это изменение и правильно выведет новые типы.

Так что мне не хватает? Почему считается хорошей идеей явно помещать сигнатуры типа в функции в общем случае? то есть я понимаю, что могут быть исключения, когда это хорошая идея, но почему она считается хорошей вообще?

Ответы

Ответ 1

Если вы ошиблись при определении своей функции, компилятор может вывести тип, который не является тем, что вы ожидали от него. Если вы объявили ожидаемый тип, компилятор сообщит об ошибке в определении функции.

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

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


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

foo n = n + 1

Компилятор выведет тип Num a => a -> a, что означает, что он должен скомпилировать общий код, который может работать с любым экземпляром Num. Если вы объявите тип как Int -> Int, компилятор может создать более эффективный код, специализированный только для целых чисел.


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

Обратите внимание, что Haddock комментарии прикреплены к объявлениям, а не к определениям. Запись объявления типа - это первый шаг к предоставлению дополнительной документации для функции с использованием Haddock.

Ответ 2

Я бы рассмотрел документальное одно из преимуществ наличия явной сигнатуры типа.

В разделе "Типы и языки программирования":

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

Ответ 3

Существует несколько причин.

  • Это может облегчить чтение кода для людей. (OTOH, если у вас есть десятки крошечных определений, иногда сигнатуры типа добавляют больше беспорядка.)
  • Если ваша реализация неверна, компилятор может вывести неверный тип. Это может привести к ошибочным выводам других типов функций, что, в конечном итоге, приведет к ошибке типа, очень далекой от фактической сломанной функции.
  • Возможно, вы захотите, чтобы функция была менее полиморфной, чем она могла бы, по соображениям производительности.
  • Возможно, вы захотите использовать псевдонимы типов. Это позволяет быстро изменить тип в нескольких местах, а также документировать некоторые из целей за значением. (Сравните FilePath vs String.)
  • Компилятор может автоматически определять типы, но не все внешние инструменты могут это сделать. (Например, изначально Хэддок отказывался создавать документацию для функций, не имеющих явной сигнатуры типа, хотя я понимаю, что это исправлено сейчас.)

Стоит отметить, что некоторые люди выступают за то, что вы начинаете с подписей типа, и заполните их позже.

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

Ответ 4

Ваш надуманный пример действительно надуман, поскольку тело функции не зависит от типа содержимого списка. В этом случае действительно трудно понять, в чем преимущество определения типа [String] -> ([String],[String]) вместо [a]->([a],[a])

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

Указание типа говорит компилятору немного о намерениях. Затем компилятор сможет сообщить о несоответствии намерения и реализации.