Разве это плохо для создания новых типов/данных для ясности?

Я хотел бы знать, если это плохо, делать что-то вроде этого:

data Alignment = LeftAl | CenterAl | RightAl
type Delimiter = Char
type Width     = Int

setW :: Width -> Alignment -> Delimiter -> String -> String

Скорее чем то вроде этого:

setW :: Int -> Char -> Char -> String -> String

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

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

Ответ 1

Вы используете псевдонимы типов, они только немного помогают с читабельностью кода. Тем не менее, лучше использовать newtype вместо type для лучшей безопасности типов. Как это:

data Alignment = LeftAl | CenterAl | RightAl
newtype Delimiter = Delimiter { unDelimiter :: Char }
newtype Width     = Width { unWidth :: Int }

setW :: Width -> Alignment -> Delimiter -> String -> String

Вы будете иметь дело с дополнительной упаковкой и распаковкой newtype. Но код будет более устойчивым к дальнейшему рефакторингу. Это руководство по стилю предлагает использовать type только для специализации полиморфных типов.

Ответ 2

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

Можно найти примеры использования псевдонимов типов в различных "базовых" библиотеках. Например, класс Read определяет этот метод:

readList :: ReadS [a]

Тип ReadS - это просто псевдоним типа.

type ReadS a = String -> [(a, String)]

Другим примером является тип Forest в Data.Tree:

type Forest a = [Tree a]

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

Ответ 3

Использование newtype который создает новый тип с тем же представлением, что и базовый тип, но не заменяемый им - считается хорошей формой. Это дешевый способ избежать примитивной одержимости, и он особенно полезен для Haskell, потому что в Haskell имена аргументов функций не видны в сигнатуре.

Newtypes также может быть местом, где можно повесить полезные экземпляры классов типов.

Учитывая, что в Хаскеле повсеместно распространены новые типы, со временем язык приобрел некоторые инструменты и идиомы для управления ими:

  • Coercible "Волшебный" класс типов, который упрощает преобразования между newtypes и их базовыми типами, когда конструктор newtype находится в области видимости. Часто полезно избегать шаблонов в реализации функций.

    ghci> coerce (Sum (5::Int)) :: Int

    ghci> coerce [Sum (5::Int)] :: [Int]

    ghci> coerce ((+) :: Int → Int → Int) :: Identity Int → Identity Int → Identity Int

  • ala Идиома (реализованная в различных пакетах), которая упрощает выбор нового типа, который мы могли бы использовать с такими функциями, как foldMap.

    ala Sum foldMap [1,2,3,4 :: Int] :: Int

  • GeneralizedNewtypeDeriving. Расширение для автоматического получения экземпляров для вашего нового типа на основе экземпляров, доступных в базовом типе.

  • DerivingVia Более общее расширение для автоматического получения экземпляров для вашего нового типа на основе экземпляров, доступных в некотором другом новом типе с тем же базовым типом.

Ответ 4

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

Что касается синонимов, мнения разные. Лично я считаю, type синонимы для небольших типов, таких как Int может повысить познавательную нагрузку, заставляя вас отслеживать различные имена для того, что является строго то же самое. Тем не менее, leftaroundabout имеет большое значение в том, что этот вид синонима может быть полезен на ранних этапах создания прототипа решения, когда вам не обязательно беспокоиться о деталях конкретного представления, которое вы собираетесь принять для своего домена. объекты.

(Стоит отметить, что замечания здесь о type основном не относятся к newtype. Однако варианты использования различны: хотя type просто вводит другое имя для одной и той же вещи, newtype вводит другую вещь с помощью fiat. Это может быть Удивительно мощный ход - см. ответ Данидиаса для дальнейшего обсуждения.)

Ответ 5

Определенно это хорошо, и вот еще один пример, предположим, у вас есть этот тип данных с некоторыми операциями:

data Form = Square Int | Rectangle Int Int | EqTriangle Int

perimeter :: Form -> Int
perimeter (Square s)      = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s)  = s * 3

area :: Form -> Int
area (Square s)      = s ^ 2
area (Rectangle b h) = (b * h)
area (EqTriangle s)  = (s ^ 2) 'div' 2 

Теперь представьте, что вы добавили круг:

data Form = Square Int | Rectangle Int Int | EqTriangle Int | Cicle Int

добавить свои операции:

perimeter (Cicle r )      = pi * 2 * r

area (Cicle r)       = pi * r ^ 2

это не очень хорошо, верно? Теперь я хочу использовать Float... Я должен изменить каждый Int для Float

data Form = Square Double | Rectangle Double Double | EqTriangle Double | Cicle Double


area :: Form -> Double

perimeter :: Form -> Double

но что, если для ясности и даже для повторного использования я использую тип?

data Form = Square Side | Rectangle Side Side | EqTriangle Side | Cicle Radius

type Distance = Int
type Side = Distance
type Radius = Distance
type Area = Distance

perimeter :: Form -> Distance
perimeter (Square s)      = s * 4
perimeter (Rectangle b h) = (b * h) * 2
perimeter (EqTriangle s)  = s * 3
perimeter (Cicle r )      = pi * 2 * r

area :: Form -> Area
area (Square s)      = s * s
area (Rectangle b h) = (b * h)
area (EqTriangle s)  = (s * 2) / 2
area (Cicle r)       = pi * r * r

Это позволяет мне изменять тип, изменяя только одну строку в коде, но я хочу, чтобы расстояние было в Int, я только изменю это

perimeter :: Form -> Distance
...

totalDistance :: [Form] -> Distance
totalDistance = foldr (\x rs -> perimeter x + rs) 0

Я хочу, чтобы расстояние было в Float, поэтому я просто изменяю:

type Distance = Float

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