Почему в Haskell есть "данные" и "новый тип"?

Похоже, что определение newtype - это просто определение data, которое подчиняется некоторым ограничениям (например, только одному конструктору) и что из-за этих ограничений система времени выполнения может более эффективно обрабатывать newtype. И обработка соответствия шаблонов для значений undefined несколько отличается.

Но предположим, что Хаскелл знал бы только определения data, no newtype s: не мог ли компилятор выяснить, соответствует ли данное определение данных этим ограничениям и автоматически ли оно более эффективно обрабатывать?

Я уверен, что я упускаю что-то, для этого должна быть какая-то более глубокая причина.

Ответ 1

Оба newtype и один конструктор data вводят один конструктор значений, но конструктор значений, введенный newtype, является строгим, а конструктор значений, введенный data, ленив. Поэтому, если у вас есть

data D = D Int
newtype N = N Int

Тогда N undefined эквивалентно undefined и вызывает ошибку при оценке. Но D undefined не эквивалентен undefined, и его можно оценить, если вы не пытаетесь заглянуть внутрь.

Не удалось компилятору обработать это для себя.

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

newtype Feet = Feet Double
newtype Cm   = Cm   Double

оба ведут себя точно так же, как Double во время выполнения, но компилятор promises не позволяет вам их путать.

Ответ 2

В соответствии с Изучите Haskell:

Вместо ключевого слова данных используется ключевое слово newtype. Теперь почему что? Ну, для одного, новый тип быстрее. Если вы используете ключевое слово data оберните тип, есть некоторые накладные расходы на все, что обертывание и разворачивание когда ваша программа запущена. Но если вы используете newtype, Haskell знает что вы просто используете его для переноса существующего типа в новый тип (отсюда и название), потому что вы хотите, чтобы он был таким же внутренним, но имеют другой тип. Имея это в виду, Haskell может избавиться от обертывание и разворачивание после того, как оно решит, какое значение относится к типу.

Так почему бы просто не использовать newtype все время вместо данных? Что ж, когда вы создаете новый тип из существующего типа, используя новый тип ключевое слово, вы можете иметь только один конструктор значений и это значение конструктор может иметь только одно поле. Но с данными вы можете сделать данные типы, которые имеют несколько конструкторов значений, и каждый конструктор может имеют ноль или более полей:

data Profession = Fighter | Archer | Accountant  

data Race = Human | Elf | Orc | Goblin  

data PlayerCharacter = PlayerCharacter Race Profession 

При использовании newtype вы ограничены только одним конструктором с одним поле.

Теперь рассмотрим следующий тип:

data CoolBool = CoolBool { getCoolBool :: Bool } 

Это ваш тип данных алгебраических данных, который был определен с помощью ключевое слово данных. Он имеет один конструктор значений, который имеет одно поле чей тип - Bool. Пусть создается функция, совпадающая с шаблоном CoolBool и возвращает значение "привет", независимо от того, является ли Bool внутри CoolBool был True или False:

helloMe :: CoolBool -> String  
helloMe (CoolBool _) = "hello"  

Вместо того, чтобы применять эту функцию к нормальному CoolBool, бросьте его Curveball и примените его к undefined!

ghci> helloMe undefined  
"*** Exception: Prelude.undefined  

Хлоп! Исключение! Теперь почему это исключение произошло? Определенные типы с ключевым словом data может иметь несколько конструкторов значений (даже хотя у CoolBool только один). Поэтому, чтобы узнать, указано ли заданное значение для нашей функции соответствует шаблону (CoolBool _), Haskell должен достаточно оценить значение, чтобы увидеть, какой конструктор значения использовался когда мы сделали это значение. И когда мы пытаемся оценить undefinedзначение, даже немного, генерируется исключение.

Вместо использования ключевого слова data для CoolBool, попробуйте использовать Newtype:

newtype CoolBool = CoolBool { getCoolBool :: Bool }   

Нам не нужно измените нашу функцию helloMe, поскольку синтаксис соответствия шаблону то же самое, если вы используете newtype или data для определения вашего типа. Позвольте сделать здесь же примените helloMe к значению undefined:

ghci> helloMe undefined  
"hello"

Это сработало! Хм, почему? Ну, как мы уже говорили, когда мы используем newtype, Haskell может внутренне представлять значения нового типа так же, как и исходные значения. Он не должен добавлять вокруг них, он должен знать о значениях Различные типы. И поскольку Хаскелл знает, что типы, сделанные с помощью Ключевое слово newtype может иметь только один конструктор, он не должен оценить значение, переданное функции, чтобы убедиться, что оно соответствует шаблону (CoolBool _), поскольку типы newtype могут имеют один возможный конструктор значений и одно поле!

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

Вот еще один источник. Согласно этой статье Newtype:

Объявление newtype создает новый тип почти так же, как и данные. Синтаксис и использование новых типов практически идентичны синтаксису объявления данных - на самом деле вы можете заменить ключевое слово newtype данных, и он все равно будет компилироваться, даже если у вас есть программа по-прежнему будет работать. Обратное неверно, однако данные могут заменяется только на newtype, если тип имеет ровно один конструктор с ровно одним полем внутри него.

Некоторые примеры:

newtype Fd = Fd CInt
-- data Fd = Fd CInt would also be valid

-- newtypes can have deriving clauses just like normal types
newtype Identity a = Identity a
  deriving (Eq, Ord, Read, Show)

-- record syntax is still allowed, but only for one field
newtype State s a = State { runState :: s -> (s, a) }

-- this is *not* allowed:
-- newtype Pair a b = Pair { pairFst :: a, pairSnd :: b }
-- but this is:
data Pair a b = Pair { pairFst :: a, pairSnd :: b }
-- and so is this:
newtype Pair' a b = Pair' (a, b)

Звучит довольно ограниченно! Итак, почему кто-то использует newtype?

Краткая версия Ограничение на один конструктор с одним полем означает, что новый тип и тип поля находятся в прямой Соответствие:

State :: (s -> (a, s)) -> State s a
runState :: State s a -> (s -> (a, s))

или в математических терминах они изоморфны. Это означает, что после тип проверяется во время компиляции, во время выполнения два типа могут быть обрабатываются практически одинаково, без накладных расходов или косвенных обычно связанный с конструктором данных. Поэтому, если вы хотите объявить типы экземпляров класса типа для определенного типа или хотите сделать абстрактный тип, вы можете обернуть его в новый тип, и это будет рассмотрено отличные от типа проверки, но идентичные во время выполнения. Тогда вы можете использовать всевозможные глубокие трюки типа phantom или рекурсивные типы без беспокоясь о том, что GHC перетасовывает ведра байтов без причины.

Смотрите статью для беспорядочных битов...

Ответ 3

Простая версия для людей, одержимых списками пули (не удалось найти их, поэтому нужно написать их самостоятельно):

данные - создает новый алгебраический тип со конструкторами значений

  • Может иметь несколько конструкторов значений
  • Конструкторы значений ленивы
  • Значения могут иметь несколько полей
  • Влияет как на компиляцию, так и на время выполнения, накладные расходы времени выполнения
  • Созданный тип - это новый новый тип
  • Может иметь свои собственные экземпляры класса типов
  • При сопоставлении шаблонов с конструкторами значений WILL будет оцениваться, по крайней мере, в слабой форме головы (WHNF) *
  • Используется для создания нового типа данных (пример: Адрес {zip:: String, street:: String})

newtype - создает новый тип "украшения" со значением конструктора

  • Может иметь только один конструктор значений
  • Конструктор значений строгий
  • Значение может иметь только одно поле
  • Влияет только на компиляцию, не накладные расходы времени выполнения
  • Созданный тип - это новый новый тип
  • Может иметь свои собственные экземпляры класса типов
  • При сопоставлении шаблонов с конструктором значений CAN вообще не оценивается *
  • Используется для создания концепции более высокого уровня на основе существующего типа с различным набором поддерживаемых операций или не взаимозаменяемым с оригинальным типом (пример: Meter, Cm, Feet is Double)

type - создает альтернативное имя (синоним) для типа (например, typedef в C)

  • Конструкторы значений
  • Нет полей
  • Влияет только на компиляцию, не накладные расходы времени выполнения
  • Нет нового типа (только новое имя для существующего типа)
  • Не имеет собственных экземпляров класса типа
  • Когда сопоставление шаблонов с конструктором данных ведет себя так же, как оригинальный тип
  • Используется для создания концепции более высокого уровня на основе существующего типа с тем же набором поддерживаемых операций (пример: String is [ Char])

[*] В соответствии с совпадением лени:

data DataBox a = DataBox Int
newtype NewtypeBox a = NewtypeBox Int

dataMatcher :: DataBox -> String
dataMatcher (DataBox _) = "data"

newtypeMatcher :: NewtypeBox -> String 
newtypeMatcher (NewtypeBox _) = "newtype"

ghci> dataMatcher undefined
"*** Exception: Prelude.undefined

ghci> newtypeMatcher undefined
"newtype"

Ответ 4

Сверху моей головы; декларации данных используют ленивую оценку в доступе и хранении своих "членов", в то время как новый тип - нет. Newtype также удаляет все экземпляры предыдущего типа из своих компонентов, эффективно скрывая его реализацию; тогда как данные оставляют реализацию открытой.

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

Когда я впервые прочитал об этом, я нашел эту главу "Нежное введение в Haskell" довольно интуитивным.