2009-06-13 6 views
56

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

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

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

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

+0

Hrm ... похоже, я не могу отметить более одного ответа, как принято. Я надеялся каким-то образом принять разумное представление о разных мнениях по этому вопросу ... – StevenC

ответ

52

Основное назначение ньютайпов являются:

  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) 

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

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

+0

Это невероятно классный материал Криса. Я просто использовал это для решения класса класса для Real World Haskell, глава 8, упражнение 2 из первого набора упражнений. Он просит предоставить способ выбора нечувствительного к регистру glob-соответствия. Спасибо :) –

+0

Как выглядит ViewPattern в последнем примере, отличном от '(ConferenceID cid)'? – Dan

+2

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

10

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

19

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

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

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

newtype Query = Query String 

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

+0

В ответ на пример пути к файлу вопрос больше связан с дизайном, который вы делаете, и меньше о том, что уже было разработано где у вас нет контроля. В прежней ситуации потребитель вашего модуля/функции/независимо от того, что код не получит, получит примитив. В последней ситуации, в худшем случае, один призыв вернуть примитив обратно перед вызовом. С другой стороны, именно поэтому я спросил: чтобы понять, что разные программисты из Хэскеля думают о выборе дизайна. По общему признанию, я человек, который склоняется к безопасности по сравнению с удобством. – StevenC

+0

Как я уже сказал, так как я хотел получить представление об отличии. практики в культуре haskell, ваш ответ по-прежнему ценен. Я еще не закончил. :) – StevenC

+3

Я понимаю, что вы рассматриваете свою собственную дизайнерскую практику: я просто хотел привести несколько практических примеров. Стоимость слова «newtype FilePath» находится в программном времени; функции преобразования предназначены только для того, чтобы держать контролер типов счастливым и не иметь реализации. Главное, что если вы многократно конвертируете свой новый тип и из него, то у вас нет реальной дополнительной безопасности, просто много запутывающих вызовов функций. Итак, при разработке библиотеки вам нужно подумать о точке зрения программистов приложений. –

14

Для простых 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. Некоторые из определений newtype были типами и были определенно конвертированы по конкретным причинам, о которых вы говорили.

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

Смежные вопросы