Тип Haskell против конструктора данных

124

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

data Car = Car { company :: String  
               , model :: String  
               , year :: Int  
               } deriving (Show) 

и это:

data Car a b c = Car { company :: a  
                     , model :: b  
                     , year :: c   
                     } deriving (Show)  

Я понимаю, что первый просто использует один конструктор ( Car) для создания данных типа Car. Я не совсем понимаю второй.

Кроме того, как типы данных определяются следующим образом:

data Color = Blue | Green | Red

вписаться во все это?

Из того, что я понимаю, третий пример ( Color) представляет собой тип , который может находиться в трех состояниях: Blue, Greenили Red. Но это противоречит тому, как я понимаю первые два примера: тип Carможет быть только в одном состоянии Car, которое может принимать различные параметры для построения? Если да, то как здесь подходит второй пример?

По сути, я ищу объяснение, которое объединяет три приведенных выше примера / конструкции кода.

Аристид
источник
18
Ваш пример Car может немного сбивать с толку, потому что Carэто конструктор типов (слева =) и конструктор данных (справа). В первом примере Carконструктор типа не принимает аргументов, во втором - три. В обоих примерах Carконструктор данных принимает три аргумента (но типы этих аргументов в одном случае фиксированы, а в другом параметризованы).
Саймон Шайн
первый - просто использовать один конструктор данных ( Car :: String -> String -> Int -> Car) для создания данных типа Car. второй - просто использует один конструктор данных ( Car :: a -> b -> c -> Car a b c) для создания данных типа Car a b c.
Уилл Несс

Ответы:

228

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

Конструкторы данных

Чтобы упростить задачу, мы можем начать с примера шрифта, представляющего цвет.

data Colour = Red | Green | Blue

Здесь у нас есть три конструктора данных. Colour- это тип и Greenконструктор, содержащий значение типа Colour. Аналогично, Redи Blueоба являются конструкторами, которые создают значения типа Colour. Мы могли бы представить, как это приправить!

data Colour = RGB Int Int Int

У нас по-прежнему есть только тип Colour, но RGBэто не значение - это функция, которая принимает три Ints и возвращает значение! RGBимеет тип

RGB :: Int -> Int -> Int -> Colour

RGB- это конструктор данных, представляющий собой функцию, принимающую некоторые значения в качестве аргументов, а затем использующую их для создания нового значения. Если вы занимались объектно-ориентированным программированием, вы должны это признать. В ООП конструкторы также принимают некоторые значения в качестве аргументов и возвращают новое значение!

В этом случае, если мы применим RGBк трем значениям, мы получим значение цвета!

Prelude> RGB 12 92 27
#0c5c1b

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

антракт

Если вы хотите построить двоичное дерево для хранения Strings, вы можете представить, что делаете что-то вроде

data SBTree = Leaf String
            | Branch String SBTree SBTree

Здесь мы видим тип SBTree, содержащий два конструктора данных. Другими словами, есть две функции (а именно Leafи Branch), которые будут создавать значения SBTreeтипа. Если вы не знакомы с тем, как работают бинарные деревья, просто подождите. На самом деле вам не нужно знать, как работают двоичные деревья, только то, что это Stringкаким-то образом хранит s.

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

Но! Что, если бы мы также хотели иметь возможность хранить Bool, нам пришлось бы создать новое двоичное дерево. Это могло выглядеть примерно так:

data BBTree = Leaf Bool
            | Branch Bool BBTree BBTree

Конструкторы типов

Оба SBTreeи BBTreeявляются конструкторами типов. Но есть вопиющая проблема. Вы видите, насколько они похожи? Это признак того, что вам действительно нужен параметр.

Итак, мы можем сделать это:

data BTree a = Leaf a
             | Branch a (BTree a) (BTree a)

Теперь мы вводим переменную типа a в качестве параметра конструктора типа. В этом объявлении BTreeстало функцией. Он принимает тип в качестве аргумента и возвращает новый тип .

Здесь важно учитывать разницу между конкретным типом (примеры включают Int, [Char]и Maybe Bool), который является типом, который может быть назначен значению в вашей программе, и функцией конструктора типа, которой вам нужно передать тип, чтобы иметь возможность присвоено значение. Значение никогда не может быть типа «список», потому что оно должно быть «списком чего-то ». В том же духе значение никогда не может быть типа «двоичное дерево», потому что оно должно быть «двоичным деревом, хранящим что-то ».

Если мы передадим, скажем, Boolв качестве аргумента BTree, он вернет тип BTree Bool, который представляет собой двоичное дерево, в котором хранится Bools. Замените каждое вхождение переменной aтипа типом Bool, и вы сами убедитесь, насколько это верно.

Если вы хотите, вы можете просмотреть BTreeкак функцию с видом

BTree :: * -> *

Виды чем-то похожи на типы - *указывает на конкретный тип, поэтому мы говорим, что BTreeэто от конкретного типа к конкретному типу.

Подведение итогов

Вернитесь сюда на мгновение и обратите внимание на сходство.

  • Конструктор данных является «функцией» , которая принимает 0 или больше значений и дает Вам новое значение.

  • Конструктор типа является «функцией» , которая принимает 0 или более типов и дает Вам новый тип.

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

Пример из практики

В качестве финишной прямой здесь можно рассматривать Maybe aтип. Его определение

data Maybe a = Nothing
             | Just a

Вот Maybeконструктор типа, который возвращает конкретный тип. Justконструктор данных, возвращающий значение. Nothing- конструктор данных, содержащий значение. Если мы посмотрим на тип Just, мы увидим, что

Just :: a -> Maybe a

Другими словами, Justпринимает значение типа aи возвращает значение типа Maybe a. Если мы посмотрим на вид Maybe, мы увидим, что

Maybe :: * -> *

Другими словами, Maybeпринимает конкретный тип и возвращает конкретный тип.

Снова! Разница между конкретным типом и функцией-конструктором типа. Вы не можете создать список Maybes - если вы попытаетесь выполнить

[] :: [Maybe]

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

kqr
источник
2
В вашем первом примере КРАСНЫЙ ЗЕЛЕНЫЙ и СИНИЙ - это конструкторы, не принимающие аргументов.
OllieB
3
Утверждение, что data Colour = Red | Green | Blue«у нас вообще нет конструкторов», совершенно неверно. Конструкторы типов и конструкторы данных не должны принимать аргументы, см., Например, haskell.org/haskellwiki/Constructor, где указано , что в data Tree a = Tip | Node a (Tree a) (Tree a)«есть два конструктора данных, Tip и Node».
Frerich Raabe
1
@CMCDragonkai Вы абсолютно правы! Виды - это «типы типов». Общий подход к объединению понятий типов и значений называется зависимой типизацией . Idris - это язык с зависимой типизацией, вдохновленный Haskell. С правильными расширениями GHC вы также можете немного приблизиться к зависимой типизации в Haskell. (Некоторые люди шутили, что «исследование Haskell - это выяснение того, насколько близко мы можем приблизиться к зависимым типам, не имея зависимых типов».)
kqr
1
@CMCDragonkai На самом деле невозможно иметь пустое объявление данных в стандартном Haskell. Но есть расширение GHC ( -XEmptyDataDecls), которое позволяет вам это делать. Поскольку, как вы говорите, нет значений с этим типом, функция f :: Int -> Zможет, например, никогда не возвращать (потому что что она вернет?) Однако они могут быть полезны, когда вам нужны типы, но на самом деле значения не важны .
kqr
1
Неужели это невозможно? Я только что попробовал в GHC, и он запустил его без ошибок. Мне не нужно было загружать какие-либо расширения GHC, только ванильный GHC. Тогда я мог писать, :k Zи это дало мне звезду.
CMCDragonkai
42

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

На других языках обычно можно создать «запись», «структуру» или что-то подобное, в которой есть набор именованных полей, содержащих различные типы данных. Кроме того, можно иногда сделать «перечисление», который имеет (небольшой) набор фиксированных возможных значений (например, ваш Red, Greenи Blue).

В Haskell вы можете комбинировать и то, и другое одновременно. Странно, но факт!

Почему это называется «алгебраическим»? Ну, ботаники говорят о «типах сумм» и «типах продуктов». Например:

data Eg1 = One Int | Two String

Eg1Значение в основном либо целое число или строка. Таким образом, набор всех возможных Eg1значений - это «сумма» набора всех возможных целочисленных значений и всех возможных строковых значений. Таким образом, ботаники называют Eg1«типом суммы». С другой стороны:

data Eg2 = Pair Int String

Каждое Eg2значение состоит как из целого числа, так и из строки. Таким образом, набор всех возможных Eg2значений - это декартово произведение набора всех целых чисел и набора всех строк. Два набора «умножаются» вместе, так что это «тип продукта».

Алгебраические типы Haskell - это типы сумм типов продуктов . Вы даете конструктору несколько полей для создания типа продукта, и у вас есть несколько конструкторов для создания суммы (продуктов).

В качестве примера того, почему это может быть полезно, предположим, что у вас есть что-то, что выводит данные в формате XML или JSON и принимает запись конфигурации, но, очевидно, настройки конфигурации для XML и для JSON совершенно разные. Итак, вы можете сделать что-то вроде этого:

data Config = XML_Config {...} | JSON_Config {...}

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

MathematicalOrchid
источник
4
здорово! только одно: «Их можно ... создать практически на любом языке», - говорится в Википедии . :) Например, в C / ++, это unions, с дисциплиной тегов. :)
Уилл Несс
5
Да, но каждый раз , когда я упоминаю union, люди смотрят на меня , как «кто ад никогда не использует , что ??» ;-)
MathematicalOrchid
1
За unionсвою карьеру на C. я видел много чего . Пожалуйста, не делайте это ненужным, потому что это не так.
truthadjustr
26

Начнем с самого простого случая:

data Color = Blue | Green | Red

Это определяет «конструктор типа», Colorкоторый не принимает аргументов - и имеет три «конструктора данных» Blue, Greenи Red. Ни один из конструкторов данных не принимает аргументов. Это означает , что существует три типа Color: Blue, Greenи Red.

Конструктор данных используется, когда вам нужно создать какое-либо значение. Подобно:

myFavoriteColor :: Color
myFavoriteColor = Green

создает значение myFavoriteColorс помощью Greenконструктора данных - и myFavoriteColorбудет иметь тип, Colorпоскольку это тип значений, созданных конструктором данных.

Конструктор типа используется , когда нужно создать тип какой - то. Обычно это происходит при написании подписей:

isFavoriteColor :: Color -> Bool

В этом случае вы вызываете Colorконструктор типа (который не принимает аргументов).

Все еще со мной?

Теперь представьте, что вы не только хотите создать значения красного / зеленого / синего, но также хотите указать «интенсивность». Например, значение от 0 до 256. Вы можете сделать это, добавив аргумент к каждому из конструкторов данных, так что вы получите:

data Color = Blue Int | Green Int | Red Int

Теперь каждый из трех конструкторов данных принимает аргумент типа Int. Конструктор типа ( Color) по-прежнему не принимает никаких аргументов. Итак, мой любимый цвет - темно-зеленый, я мог написать

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

И снова он вызывает Greenконструктор данных, и я получаю значение типа Color.

Представьте, что вы не хотите диктовать, как люди выражают интенсивность цвета. Некоторым может потребоваться числовое значение, как мы только что сделали. Другие могут подойти только с логическим значением, указывающим «яркий» или «не такой яркий». Решение состоит в том, чтобы не жестко закодировать Intконструкторы данных, а использовать переменную типа:

data Color a = Blue a | Green a | Red a

Теперь наш конструктор типа принимает один аргумент (другой тип, который мы просто вызываем a!), А все конструкторы данных будут принимать один аргумент (значение!) Этого типа a. Так что вы могли бы

myFavoriteColor :: Color Bool
myFavoriteColor = Green False

или

myFavoriteColor :: Color Int
myFavoriteColor = Green 50

Обратите внимание, как мы вызываем Colorконструктор типа с аргументом (другим типом), чтобы получить «эффективный» тип, который будет возвращен конструкторами данных. Это касается концепции видов, о которых вы, возможно, захотите прочитать за чашкой или двумя кофе.

Теперь мы выяснили, что такое конструкторы данных и конструкторы типов, и как конструкторы данных могут принимать другие значения в качестве аргументов, а конструкторы типов могут принимать другие типы в качестве аргументов. НТН.

Фрерих Раабе
источник
Я не уверен, что поддерживаю ваше представление о конструкторе нулевых данных. Я знаю, что это обычный способ говорить о константах в Haskell, но разве это не было доказано несколько раз неверно?
kqr
@kqr: конструктор данных может быть нулевым, но тогда это уже не функция. Функция - это то, что принимает аргумент и возвращает значение, то есть что-то, что есть ->в подписи.
Фрерих Раабе,
Может ли значение указывать на несколько типов? Или каждое значение связано только с одним типом и все?
CMCDragonkai
1
@jrg Есть некоторое совпадение, но не из-за конструкторов типов, а из-за переменных типа, например, ain data Color a = Red a. aявляется заполнителем для произвольного типа. Однако вы можете иметь то же самое в простых функциях, например, функция типа (a, b) -> aпринимает кортеж из двух значений (типов aи b) и возвращает первое значение. Это «универсальная» функция, поскольку она не определяет тип элементов кортежа - она ​​только указывает, что функция возвращает значение того же типа, что и первый элемент кортежа.
Frerich Raabe
1
+1 Now, our type constructor takes one argument (another type which we just call a!) and all of the data constructors will take one argument (a value!) of that type a.Это очень полезно.
Jonas
5

Как отмечали другие, полиморфизм здесь не так уж и страшен. Давайте посмотрим на другой пример, с которым вы, вероятно, уже знакомы:

Maybe a = Just a | Nothing

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

maybeHead :: [Int] -> Maybe Int
maybeHead [] = Nothing
maybeHead (x:_) = Just x

> maybeHead [1,2,3]    -- Just 1
> maybeHead []         -- None

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

maybeHead :: [t] -> Maybe t
maybeHead [] = Nothing
maybeHead (x:_) = Just x

С другой стороны, вы можете писать функции, которые принимают только определенный тип Maybe, например

doubleMaybe :: Maybe Int -> Maybe Int
doubleMaybe Just x = Just (2*x)
doubleMaybe Nothing= Nothing

Короче говоря, с помощью полиморфизма вы даете своему собственному типу гибкость для работы со значениями разных других типов.

В вашем примере вы можете решить, что в какой-то момент этого Stringнедостаточно для идентификации компании, но у нее должен быть собственный тип Company(который содержит дополнительные данные, такие как страна, адрес, резервные счета и т. Д.). В вашей первой реализации Carнужно будет изменить использование Companyвместо Stringпервого значения. Ваша вторая реализация в порядке, вы используете ее, Car Company String Intи она будет работать как раньше (конечно, функции доступа к данным компании необходимо изменить).

Landei
источник
Можете ли вы использовать конструкторы типов в контексте данных другого объявления данных? Что-то вроде data Color = Blue ; data Bright = Color? Я пробовал это в ghci, и кажется, что Color в конструкторе типа не имеет ничего общего с конструктором данных Color в определении Bright. Есть всего 2 конструктора Color, один из которых - Data, а другой - Type.
CMCDragonkai
@CMCDragonkai Я не думаю, что вы можете это сделать, и я даже не уверен, чего вы хотите этим добиться. Вы можете «обернуть» существующий тип, используя dataили newtype(например data Bright = Bright Color), или вы можете использовать type, чтобы определить синоним (например type Bright = Color).
Landei
5

Во втором есть понятие «полиморфизм».

a b cМожет быть любого типа. Например, aможет быть [String], bможет быть [Int] и cможет быть [Char].

Пока фиксирован первый тип: компания - это String, модель - Stringа, год - Int.

Пример Car может не показывать важность использования полиморфизма. Но представьте, что ваши данные относятся к типу списка. Список может содержать String, Char, Int ...В таких ситуациях вам понадобится второй способ определения ваших данных.

Что касается третьего способа, я не думаю, что он должен вписываться в предыдущий тип. Это всего лишь еще один способ определения данных в Haskell.

Это мое скромное мнение как новичка.

Кстати: убедитесь, что вы хорошо тренируете свой мозг и чувствуете себя комфортно при этом. Это ключ к пониманию Монады позже.

МакБир Холден
источник
1

Речь идет о типах : в первом случае вы задаете типы String(для компании и модели) и Intдля года. Во втором случае ваши более общие. a,, bи cмогут быть теми же типами, что и в первом примере, или совсем другими. Например, может быть полезно указать год в виде строки вместо целого числа. А если хотите, можете даже использовать свой Colorшрифт.

Матиас
источник