Тип Haskell против нового типа в отношении безопасности типов

Я знаю, что newtype чаще сравнивают с data в Haskell, но я представляю это сравнение скорее с точки зрения дизайна, чем как техническую проблему.

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

Итак, как часто программисты на Haskell используют newtype, чтобы различать типы примитивных значений? Использование type вводит псевдоним и придает программе более четкую семантику, но не предотвращает случайный обмен значениями. Когда я изучаю haskell, я замечаю, что система типов такая же мощная, как и любая другая, с которой я сталкивался. Поэтому я думаю, что это естественная и обычная практика, но я не видел много или какое-либо обсуждение использования newtype в этом свете.

Конечно, многие программисты делают что-то по-другому, но разве это вообще распространено в Haskell?


person StevenC    schedule 13.06.2009    source источник
comment
Хм... похоже, я не могу отметить более одного ответа как принятого. Я надеялся каким-то образом принять разумное представление различных мнений по этому вопросу...   -  person StevenC    schedule 17.06.2009


Ответы (4)


Основные области применения новых типов:

  1. Для определения альтернативных экземпляров типов.
  2. Документация.
  3. Обеспечение правильности данных/формата.

Сейчас я работаю над приложением, в котором я широко использую новые типы. newtypes в Haskell — это чисто концепция времени компиляции. Например. с распаковщиками ниже, unFilename (Filename "x") скомпилирован в тот же код, что и "x". Существует абсолютно нулевое попадание во время выполнения. Есть с data типами. Это делает его очень хорошим способом достижения перечисленных выше целей.

-- | A file name (not a file path).
newtype Filename = Filename { unFilename :: String }
    deriving (Show,Eq)

Я не хочу случайно рассматривать это как путь к файлу. Это не путь к файлу. Это имя концептуального файла где-то в базе данных.

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

-- | A sanitized (safe) filename.
newtype SanitizedFilename = 
  SanitizedFilename { unSafe :: String } deriving Show

-- | Unique, sanitized filename.
newtype UniqueFilename =
  UniqueFilename { unUnique :: SanitizedFilename } deriving Show

-- | An uploaded file.
data File = File {
   file_name     :: String         -- ^ Uploaded file.
  ,file_location :: UniqueFilename -- ^ Saved location.
  ,file_type     :: String         -- ^ File type.
  } deriving (Show)

Предположим, у меня есть эта функция, которая очищает имя файла из загруженного файла:

-- | Sanitize a filename for saving to upload directory.
sanitizeFilename :: String            -- ^ Arbitrary filename.
                 -> SanitizedFilename -- ^ Sanitized filename.
sanitizeFilename = SanitizedFilename . filter ok where 
  ok c = isDigit c || isLetter c || elem c "-_."

Теперь из этого я создаю уникальное имя файла:

-- | Generate a unique filename.
uniqueFilename :: SanitizedFilename -- ^ Sanitized filename.
               -> IO UniqueFilename -- ^ Unique filename.

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

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

-- | Get the form fields for a form.
formFields :: ConferenceId -> Controller [Field]
formFields (unConferenceId -> cid) = getFields where
   ... code using cid ..

Может быть, вы скажете, что развернуть его в функции — это проблема — что, если вы неправильно передадите cid в функцию? Не проблема, все функции, использующие идентификатор конференции, будут использовать тип ConferenceId. Возникает что-то вроде системы контрактов между функциями, которая принудительно применяется во время компиляции. Довольно мило. Так что да, я использую его так часто, как могу, особенно в больших системах.

person Christopher Done    schedule 10.10.2010
comment
Это невероятно крутая штука, Крис. Я только что использовал это для решения класса типов для Real World Haskell, глава 8, упражнение 2 из первого набора упражнений. Он просит предоставить способ выбора соответствия glob без учета регистра. Спасибо :) - person Alain O'Dea; 23.08.2012
comment
Чем ViewPattern в последнем примере отличается от (ConferenceID cid)? - person Dan; 11.06.2014
comment
В моем случае я не экспортирую конструктор, потому что не хочу создавать произвольные значения из любого старого целого числа, оно должно исходить только из базы данных. Я могу безопасно развернуть один и использовать это целое число. - person Christopher Done; 23.06.2014

Я думаю, что это в большей степени зависит от ситуации.

Учитывайте пути. В стандартной прелюдии указано «type FilePath = String», потому что для удобства вы хотите иметь доступ ко всем операциям со строками и списками. Если бы у вас был «newtype FilePath = FilePath String», вам понадобился бы filePathLength, filePathMap и т. д., иначе вы навсегда использовали бы функции преобразования.

С другой стороны, рассмотрим SQL-запросы. SQL-инъекция — это обычная дыра в безопасности, поэтому имеет смысл иметь что-то вроде

newtype Query = Query String

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

person Paul Johnson    schedule 14.06.2009
comment
В ответ на пример пути к файлу вопрос больше касается контекста дизайна, который вы делаете, и меньше о том, что уже было разработано, где вы не имеете контроля. В первой ситуации потребитель вашего модуля/функции/чего-либо не увидит код для получения примитива. В последнем случае это в худшем случае один вызов, чтобы вернуть примитив непосредственно перед вызовом. С другой стороны, именно поэтому я и спросил: чтобы понять, что разные программисты на Haskell думают о выборе дизайна. По общему признанию, я человек, который предпочитает безопасность удобству. - person StevenC; 14.06.2009
comment
Как я уже сказал, так как я хотел получить представление о разнице. практики в культуре Haskell, ваш ответ по-прежнему ценен. Я еще не закончил. :) - person StevenC; 14.06.2009
comment
Я понимаю, что вы рассматриваете собственную практику проектирования: я просто хотел привести пару практических примеров. Стоимость указания нового типа FilePath заключается во времени программиста; функции преобразования нужны только для проверки типов и не имеют реализации. Суть в том, что если вы постоянно конвертируете в свой новый тип и из него, то у вас нет реальной дополнительной безопасности, просто много запутанных вызовов функций. Поэтому при проектировании библиотеки вам нужно думать о точке зрения программистов приложений. - person Paul Johnson; 15.06.2009

Для простых объявлений X = Y type — это документация; newtype — проверка типов; вот почему newtype сравнивают с data.

Я довольно часто использую newtype именно для той цели, которую вы описываете: гарантировать, что что-то, что хранится (и часто манипулируется) таким же образом, как и другой тип, не будет перепутано с чем-то другим. Таким образом, это работает как немного более эффективное объявление data; нет особой причины предпочесть одно другому. Обратите внимание, что с расширением GHC GeneralizedNewtypeDeriving для любого из них вы можете автоматически получать классы, такие как Num, позволяя добавлять и вычитать ваши температуры или иены так же, как вы можете с Ints или чем-то еще, лежащим под ними. Однако с этим нужно быть немного осторожным; обычно нельзя умножать температуру на другую температуру!

Чтобы понять, как часто эти вещи используются, в одном довольно большом проекте, над которым я сейчас работаю, у меня около 122 применений data, 39 применений newtype и 96 применений type.

Но соотношение, что касается «простых» типов, немного ближе, чем это демонстрирует, потому что 32 из этих 96 применений type на самом деле являются псевдонимами для типов функций, таких как

type PlotDataGen t = PlotSeries t -> [String]

Здесь вы заметите две дополнительные сложности: во-первых, это фактически функциональный тип, а не просто псевдоним X = Y, а во-вторых, он параметризован: PlotDataGen — это конструктор типа, который я применяю к другому типу для создания нового типа, такого как PlotDataGen (Int,Double). . Когда вы начинаете делать такие вещи, type уже не просто документация, а фактически функция, хотя и на уровне типа, а не на уровне данных.

newtype иногда используется там, где type быть не может, например, когда необходимо определение рекурсивного типа, но я считаю, что это достаточно редко. Таким образом, похоже, по крайней мере, в этом конкретном проекте около 40% моих определений «примитивных» типов — это newtypes, а 60% — types. Некоторые из определений newtype раньше были типами и определенно были преобразованы именно по указанным вами причинам.

Короче говоря, да, это частая идиома.

person cjs    schedule 14.06.2009

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

person GS - Apologise to Monica    schedule 13.06.2009