Основной способ задать новый тип данных в Haskell — это использование конструкции data. Однако, есть ещё и newtype. Практикующие программисты Haskell пользуются конструкцией newtype постоянно, популярный линтер hlint предлагает заменять data на newtype если это возможно. Но почему? Сначала я планировал эту статью для новичков. Действительно, конструкция newtype упоминается в первых главах учебников по Haskell. Вроде бы идея простая: мы ограничиваем представление для типа данных и выигрываем в скорости выполнения. В разных источниках можно найти упоминания, что newtype экономит на вложенности указателей. Но я не нашёл подробного разбора, что на самом деле происходит с этой конструкцией в компиляторе и почему это так круто и нужно. Чтобы заполнить эти пробелы, я и решил написать статью на Хабр. Итак, нам понадобится haskell stack и базовые знания о модели выполнения Haskell и GHC (знаем что такое ленивые вычисления, thunks (невычисленные объекты), слышали немного о стеке и куче GHC). Синтаксис конструкции newtype точно такой же, как и конструкции data, за исключением того, что newtype может иметь только один конструктор и только одно упакованное (boxed type) поле внутри. Кстати, всякие record syntax и phantom types тоже работают: Но в чём же отличие между MyValDL (MyValDS) и MyValN? В отчёте Haskell'98 говорится, что конструкция newtype вводит новый тип, чьё представление идентично существующему. То есть Int и MyValN изоморфны. Но разве это не так для data-типов? Оказывается, нет. Дело в том, что в Haskell есть "поднятые" (lifted) и "не поднятые" типы. Ко всем типам, объявленным с помощью data, добавляется дополнительный элемент "дно" (bottom, или ⊥) — они становятся поднятыми (для программы, "дно" это undefined). Это значит, что "MyValDLC ⊥ :: MyValDL" и "⊥ :: MyValDL" являются различными значениями. В отличие от data, newtype-тип не поднят (unlifted type), а значит ⊥ :: MyValN "заимствуется" у вложенного типа (через MyValN ⊥). Можно сказать, что для компилятора поднятый тип означает лишний слой указателей (указатель может ссылаться на вложенный объект, или на дно (undefined)). Для программиста это значит, что если поле data конструктора может содержать undefined, ещё не всё потеряно: пока мы не трогаем это поле, программа не сломается. С другой стороны, невычисленное поле может быть местом утечки памяти (чередой thunkов). Со строгим MyValDS дело обстоит немного сложнее: вроде бы MyValDSC ⊥ немедленно вычисляется до ⊥, но семантика этих двух конструкторов одинакова. Кстати, поэтому профессионалы рекомендуют объявлять все поля в конструкторах data строгими, если только нет реальной необходимости использовать поле лениво — так можно избежать долгих часов поиска утечек памяти. На Haskell wiki есть отличный набор примеров, иллюстрирующих разницу между data и newtype; я приведу их здесь с поправкой на вольный перевод и мои названия типов: Приведённое выше описание является моей интерпретацией Haskell wiki. Но на этом мы не остановимся и рассмотрим ещё несколько вопросов. Требование одинакового представления исходного типа и его newtype обёртки для компилятора означает, что: GHC работает с типами на двух уровнях: "видимая" система типов Haskell, и репрезентативные типы (representation types). Видимая система типов — это то, с чем мы работаем, программируя на Haskell. Однако, в определённый момент компилятор преобразует типы Haskell в машинное представление. Часто получается, что два разных типа в Haskell могут иметь одинаковое представление в машинном коде. В современном GHC, с помощью системы ролей, можно сравнивать типы не только номинально, но и по их представлению. Это позволило реализовать механизм бесплатных приведений типов (видимых только на уровне системы типов Haskell) в модулях Data.Type.Coercion и Data.Coerce. Принимая во внимание вышесказанное, общая идея реализации newtype очевидна: все упакованные значения в Haskell представляются в виде описания с указателем на конструктор (object info tables); следовательно, значение newtype-типа может в своём описании использовать указатель прямо на конструктор вложенного значения; получается что-вроде type синонимов, только на более низком уровне.  Каждый программист Haskell знает, что GHC не компилирует исходный код сразу в ассемблер, а производит сложную череду преобразований кода в несколько этапов, с оптимизациями почти на каждом из них. Сначала синтаксическое дерево Haskell преобразуется в упрощённую версию языка — GHC Core (который основан на системе F). Затем Core преобразуется в функциональный STG (Spineless Tagless G-machine), потом в императивный C-- (специальный промежуточный язык с поддержкой обработки исключений и сборщика мусора), а оттуда уже различными путями в ассемблер. Вроде бы, утверждение, что конструкторы newtype-типов "исчезают" во время выполнения общеизвестно. Однако, разобраться, как это происходит, оказалось достаточно сложно. Описание STG на сайте GHC даёт нам понять, что STG оперирует уже только репрезентативными типами, то есть newtype должен теряться до него. Другая страница GHC wiki намекает, что некоторые типы видны всегда, а некоторые только для type checker’а (последние, кстати, вроде бы имеют префикс tc на разных уровнях генерируемого кода). Чтобы узнать наверняка, что происходит с newtype типами, я решил посмотреть все промежуточные стадии компилируемого кода на следующем примере: Результат не заставил себя ждать на первом же этапе: Программа ghc-core, доступная в одноименном пакете на hackage, выводит немного подчищенный GHC Core. Уже на этом этапе все вхождения MyNewtypeConstr заменяются на операции cast, в то время как MyDataConstr честно остаётся на своём месте: Следующий этап — STG, его можно посмотреть следующей командой: Он не сильно отличается от Core, за исключением интересной детали: Main.MyDataConstr определяется явно в генерируемом коде, а Main.MyNewtypeConstr. На уровне С-- и далее можно найти такие конструкции, как Main.$tc'MyNewtypeConstr и Main.$tc'MyDataConstr, однако Main.MyDataConstr не находит себе пары newtype. Видимо, $tc (от слова Type Checker) — это ещё один намёк на типы, которые используются только на уровне проверки типов. Как мы видим, уже этапе преобразований GHC Core, newtype конструкторы заменяются на примитивные преобразования типов. На уровне C-- можно сравнить низкоуровневый императивный код Main.unNewtypeStruct_entry() и Main.unDataStruct_entry(), чтобы убедиться, что unNewtypeStruct не делает фактически ничего, в отличие от unDataStruct.
Все уровни вывода можно посмотреть с помощью нескольких команд: Отформатированный мной вывод есть под катом. Я оставил только более-ли-менее интересные части (текста и так слишком много). PS: Разумеется, сам тип newtype N не может никуда исчезнуть во время компиляции: скомпилированный модуль должен экспортировать символы для использования в других модулях. Кроме того, нужно где-то хранить указатели на функции классов реализованных этим типом. Реальная польза newtype в том, что на низком уровне не нужно проходить цепочку конструкторов чтобы добраться до искомого значения. В предыдущем параграфе мы неявно определили почему лучше использовать newtype вместо data где это возможно (newtype быстрее и компактнее). Теперь нерешённый вопрос: а зачем вообще использовать newtype — тип с одним конструктором и полем — когда можно просто использовать исходный тип или type синоним к нему? Для этого есть много причин. Синонимы типов разыменовываются во время вывода типов, а newtype это всегда отдельный тип. Рассмотрим следующий пример: И вывод компилятора: Намного проще понять, какой вид должна иметь функция Age -> Percent, чем Int -> Double. Знаменитая библиотека lens — могущественный инструмент, но разбор типов её комбинаторов часто снится мне в кошмарах. Весьма простая причина, но она почему-то редко упоминается в учебниках и туториалах. В предыдущем примере очень просто перепутать рост и вес в качестве аргументов функции diseaseRate', но компилятор не позволит этого сделать в функции diseaseRate. Разумеется, это не отменяет документацию, но является очень хорошим дополнением к ней.
Если не хочется плодить большое количество новых типов под каждую функцию, есть удобный приём — использовать теггирование: Можно теггировать перечисляемыми типами, или просто использовать GHC.TypeLits.
Первый раз этот приём я увидел на лекции Саймона Меера. Используем ли мы Float или Double в качестве типа данных для вывода процентов? Иногда, пользователю библиотеки этого не следует знать (тип может различаться для разных платформ, или мы хотим поменять его в новой версии библиотеки). Самая популярная причина. Создавая новый тип, мы можем по-новому реализовать некоторые классы. Например, я хочу выводить проценты в строке со знаком "%" после числа: Но, вообще-то, я бы хотел складывать и умножать проценты; желательно, без необходимости переопределять все методы Num и им подобные. В GHC для этого есть расширение GeneralizedNewtypeDeriving, которое позволяет естественным образом выводить реализации широкого круга классов для newtype-типов: Примеров использования newtype по различным причинам очень много. Может быть, он используется даже чаще чем data. Самый первый и главный пример — это, конечно, IO. Просто введите в консоли ghci команду :info IO:  Да, весь IO это всего лишь newtype обёртка вокруг примитивной функции вычисление+состояние. Точно так же, как и монада ST
newtype ST s a = ST (STRep s a). Все монады и монадные трансформеры из пакета transformers реализованы по похожему принципу
newtype RWST r w s m a = RWST { runRWST :: r -> s -> m (a, s, w) }. Два очень интересных и полезных модуля: Data.Semigroup и Data.Monoid содержат набор newtype-обёрток над типами данных для реализации поведения полугрупп и моноидов. Например, максимум (newtype Max a = Max { getMax :: a }) для типа Ord является полугруппой, а над Ord и Bounded становится моноидом. Но это тема для отдельной статьи! Аспирант, программист Haskell