Статьи

Новые типы не так круты, как вы думаете

В моем последнем посте говорилось о том, что не так с классами типов (в целом, но также и конкретно в Haskell). Эта публикация дала отличные отзывы о Reddit , в том числе некоторую обоснованную критику, которую я не объяснил, почему я так ненавидел новые типы.

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

Что нового?

Новые типы — это особенность Haskell, которая позволяет вам определять новый тип в терминах существующего типа.

В следующем примере я создаю новый тип для Email, который «содержит» a String.

newtype Email = Email String

Они похожи на синонимы типов ( type Email = String), за исключением того, что синонимы типов не создают новые типы, они просто позволяют ссылаться на существующие типы под другими именами.

Каждый новый тип может быть легко переведен в объявление данных. На самом деле меняется только ключевое слово:

data Email = Email String

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

Обещание Newtypes

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

Другое обещание новых типов заключается в том, что мы можем использовать их, чтобы сделать наш код более безопасным. StringНапример, вместо того, чтобы рассылаться по электронной почте, мы можем создать сверхлегкий «упаковщик» вокруг Stringвызываемого Emailи сделать ошибкой использование Stringвезде, где Emailожидается.

Эта практика не ограничивается Haskell. Даже в Java считается хорошей практикой кодирования заключать примитивы в классы, имена которых обозначают значение оболочки (Email, SSN, Address и т. Д.).

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

  1. Смоделируйте параметр электронной почты с String. В этом случае я могу случайно использовать электронную почту, где я намеревался использовать два других строковых параметра, или я могу использовать один из двух других строковых параметров, где я намеревался использовать электронную почту. Учитывая только эти варианты, есть пять способов моя программа может пойти не так , если я использую неверное имя в неправильном положении.
  2. Смоделируйте параметр электронной почты с newtype. В этом случае я не могу использовать электронную почту, где я намеревался использовать два других строковых параметра, потому что компилятор может остановить меня. Точно так же я не могу использовать два других строковых параметра там, где я намеревался использовать электронную почту, по той же причине. Глядя только эти варианты, есть 0 путь моя программа может пойти не так.

Таким образом, новые типы, как и все хорошие практики FP, уменьшают количество способов, которыми моя программа может пойти не так.

К сожалению, на мой взгляд, они не зашли достаточно далеко.

Ложная безопасность

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

В моем предыдущем примере, учитывая String, я могу получить электронную почту ( Email "foo"). Учитывая Email, я также могу получить String, например, путем сопоставления с шаблоном в Emailконструкторе.

Иначе говоря , а также приблизительно , потому что я игнорирую Внизу: Stringи Emailтипы изоморфны; они содержат одинаковых жителей, для любого полезного определения «то же самое». Единственное существенное различие между предыдущим Stringи Emailявляется имя конструктора данных (вызов , и что изменилось?).EmailAbergrackleFoozyWatzit

Следовательно, моя предыдущая критика новых типов как «программирования по имени».

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

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

Параметричность позволяет значительно сократить количество ошибок, которые могут возникнуть в моей программе. Новые типы, с другой стороны, просто усложняют задачу, добавляя один слой косвенности.

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

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

По моему опыту, слишком много новых типов создают изоморфизм между вещами, которые должным образом смоделированы и не являются изоморфными.

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

Умные Конструкторы

Если я определю Emailв модуле, я могу сделать его конструктор данных закрытым и экспортировать вспомогательную функцию для создания Email. Такие вспомогательные функции называются умными конструкторами .

Они могут использоваться, чтобы сломать естественные изоморфизмы, созданные newtyping.

Пример показан ниже:

newtype Email = MkEmail String

mkEmail :: String -> Maybe Email
mkEmail s = ...

В этом примере я создаю умный конструктор, который не обещает, что он может превратить каждую строку в электронное письмо. Он обещает только то , что она может быть в состоянии превратить строку в электронную почту, возвращая Maybe Email.

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

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

Умные Конструкторы, Тупые Данные

Умные конструкторы делают нас на шаг ближе к моделированию данных безопасным способом.

К сожалению, я все еще не думаю, что это достаточно далеко.

С смарт — конструкторами, наша модель данных принципиально underconstrained , поэтому мы латать , что до путем ограничения , которые могут создавать данные. Это ставит пластырь на реальную проблему.

Почему бы просто не решить коренную проблему — то есть, что наша модель данных недостаточно ограничена?

Тупые конструкторы, умные данные

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

То есть, создание модели данных таким образом, чтобы в пространстве состояний наших данных не было областей, соответствующих недопустимым состояниям.

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

(В некоторой степени это отказ большинства языков, я знаю, что такие спецификации не могут быть легко переведены в код без утомительного шаблона!)

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

Резюме

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

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

Точность может быть утомительной из-за ограничений языков, на которых мы работаем, но, честно говоря, что более утомительно, чем отладка неработающего кода?