Подтвердить что ты не робот

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

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

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

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

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

4b9b3361

Ответ 1

Основные типы использования новых типов:

  • Для определения альтернативных экземпляров для типов.
  • Documentation.
  • Обеспечение достоверности данных/формата.

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

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

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

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

-- | 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. То, что возникает, - это своего рода система контрактов на уровне функций, которые вынуждены во время компиляции. Довольно приятно. Поэтому я использую его так часто, как могу, особенно в больших системах.

Ответ 2

Я думаю, что это в основном вопрос ситуации.

Рассмотрим пути. Стандартная прелюдия имеет "тип FilePath = String", потому что для удобства вы хотите получить доступ ко всем операциям с строками и списками. Если у вас есть "newtype FilePath = FilePath String", вам понадобится filePathLength, filePathMap и т.д., Иначе вы всегда будете использовать функции преобразования.

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

newtype Query = Query String

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

Ответ 3

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

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

Для того, чтобы понять, как часто эти вещи используются, в одном достаточно крупном проекте, над которым я сейчас работаю, у меня есть примерно 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% моих "примитивных" типов имеют newtype и 60% - type s. Некоторые из определений newtype были типами и были определенно конвертированы по тем причинам, о которых вы говорили.

Итак, да, это частая идиома.

Ответ 4

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