Тип Haskell vs Data Constructor
Итак, я изучаю Haskell из learnyouahaskell.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
, который может принимать различные параметры для сборки? Если да, то как второй пример вписывается?
По сути, я ищу объяснение, которое объединяет приведенные выше три примера кода/конструкции.
Ответы
Ответ 1
В объявлении data
конструктор типа является вещью в левой части знака равенства. Конструктор данных - это вещи в правой части знака равенства. Вы используете конструкторы типов, где ожидается тип, и вы используете конструкторы данных, где ожидается значение.
Конструкторы данных
Чтобы сделать вещи простыми, мы можем начать с примера типа, который представляет цвет.
data Colour = Red | Green | Blue
Здесь мы имеем три конструктора данных. Colour
- это тип, а Green
- конструктор, содержащий значение типа Colour
. Аналогично, Red
и Blue
- оба конструктора, которые строят значения типа Colour
. Мы могли бы подумать, что это пригодится, хотя!
data Colour = RGB Int Int Int
У нас все еще есть тип Colour
, но RGB
не является значением - это функция, принимающая три входа и возвращающая значение! RGB
имеет тип
RGB :: Int -> Int -> Int -> Colour
RGB
- это конструктор данных, который является функцией, принимающей некоторые значения в качестве своих аргументов, а затем использует их для построения нового значения. Если вы сделали объектно-ориентированное программирование, вы должны это признать. В ООП конструкторы также принимают некоторые значения в качестве аргументов и возвращают новое значение!
В этом случае, если мы применим RGB
к трем значениям, получим значение цвета!
Prelude> RGB 12 92 27
#0c5c1b
Мы построили значение типа Colour
, применяя конструктор данных. Конструктор данных либо содержит значение, как переменная, либо принимает другие значения в качестве аргумента и создает новое значение. Если вы сделали предыдущее программирование, эта концепция не должна быть очень странной для вас.
Антракт
Если вы хотите построить двоичное дерево для хранения String
s, вы можете представить себе что-то вроде
data SBTree = Leaf String
| Branch String SBTree SBTree
Здесь мы видим тип SBTree
, который содержит два конструктора данных. Другими словами, существуют две функции (а именно Leaf
и Branch
), которые будут строить значения типа SBTree
. Если вы не знакомы с тем, как работают бинарные деревья, просто держитесь там. Вам фактически не нужно знать, как работают бинарные деревья, только то, что в этом случае хранится String
.
Мы также видим, что оба конструктора данных принимают аргумент 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
), который является типом, который может быть присвоен значению в вашей программе, и функцией конструктора типов которые вам нужно подавать, чтобы иметь возможность присвоить значение. Значение никогда не может быть типа "list", потому что оно должно быть "списком чего-то". В том же духе значение никогда не может быть типа "двоичного дерева", потому что оно должно быть "двоичным деревом, которое хранит что-то".
Если мы передадим, скажем, Bool
в качестве аргумента для BTree
, он возвращает тип BTree Bool
, который является двоичным деревом, в котором хранится Bool
s. Замените каждое вхождение переменной типа 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
принимает конкретный тип и возвращает конкретный тип.
Еще раз! Разница между конкретным типом и функцией конструктора типов. Вы не можете создать список Maybe
- если вы попытаетесь выполнить
[] :: [Maybe]
вы получите сообщение об ошибке. Однако вы можете создать список Maybe Int
или Maybe a
. Это потому, что Maybe
- это функция конструктора типов, но список должен содержать значения конкретного типа. Maybe Int
и Maybe a
являются конкретными типами (или, если хотите, вызовы для создания конструкторских функций, возвращающих конкретные типы.)
Ответ 2
Haskell имеет алгебраические типы данных, которые имеют очень мало других языков. Возможно, это вас смущает.
В других языках вы обычно можете создавать "запись", "структура" или подобное, в котором есть куча именованных полей, которые содержат различные типы данных. Иногда вы также можете сделать "перечисление", в котором есть (малый) набор фиксированных возможных значений (например, ваши Red
, Green
и Blue
).
В Haskell вы можете одновременно объединить оба из них. Странно, но верно!
Почему это называется "алгебраическим"? Ну, ботаники говорят о типах сумм и типах продуктов. Например:
data Eg1 = One Int | Two String
Значение An Eg1
в основном представляет собой целое число или строку. Таким образом, набор всех возможных значений Eg1
является "суммой" множества всех возможных целых значений и всех возможных значений строк. Таким образом, кретины ссылаются на Eg1
как на тип суммы. С другой стороны:
data Eg2 = Pair Int String
Каждое значение Eg2
состоит из целого числа и строки. Таким образом, множество всех возможных значений Eg2
является декартовым произведением множества всех целых чисел и множества всех строк. Эти два набора "умножаются" вместе, поэтому это "тип продукта".
Алгебраические типы Хаскеля представляют собой суммарные типы типов продуктов. Вы даете конструктору несколько полей для создания типа продукта, и у вас есть несколько конструкторов для создания суммы (продуктов).
В качестве примера того, почему это может быть полезно, предположим, что у вас есть что-то, что выводит данные как XML или JSON, и требует записи конфигурации, но, очевидно, настройки конфигурации для XML и JSON совершенно разные. Поэтому вы можете сделать что-то вроде этого:
data Config = XML_Config {...} | JSON_Config {...}
(Очевидно, что с некоторыми подходящими полями.) Вы не можете делать такие вещи на обычных языках программирования, поэтому большинство людей не привыкли к этому.
Ответ 3
Начните с простейшего случая:
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
.
Представьте, если вы не хотите диктовать, как люди выражают интенсивность цвета. Некоторым может потребоваться числовое значение, как мы это делали. Другие могут быть в порядке с просто булевым, обозначающим "яркий" или "не такой яркий". Решением этого является не hardcode 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
с аргументом (другим типом), чтобы получить "эффективный" тип, который будет возвращен конструкторами данных. Это затрагивает концепцию виды, которые вы можете прочитать о чаше кофе или двух.
Теперь мы выяснили, какие конструкторы данных и конструкторы типов и как конструкторы данных могут принимать другие значения, поскольку аргументы и конструкторы типов могут принимать другие типы в качестве аргументов. НТН.
Ответ 4
Как отмечали другие, полиморфизм здесь не так страшен. Давайте посмотрим на другой пример, с которым вы, вероятно, уже знакомы:
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
, и она будет работать по-прежнему (конечно, функции, связанные с данными компании, должны быть изменены).
Ответ 5
Второй имеет понятие "полиморфизм" в нем.
a b c может быть любого типа. Например, a может быть [String], b может быть [Int]
и c [ Char]
В то время как первый тип фиксирован: компания является String, модель - это String, а year - Int.
Пример автомобиля может не показать значимости использования полимпрефизма. Но представьте, что ваши данные относятся к типу списка. Список может содержать String, Char, Int... В этих ситуациях вам понадобится второй способ определения ваших данных.
Что касается третьего пути, я не думаю, что он должен соответствовать предыдущему типу. Это всего лишь один из способов определения данных в Haskell.
Это мое скромное мнение как новичок.
Btw: Убедитесь, что вы хорошо тренируете свой мозг и чувствуете себя комфортно. Это ключ к пониманию Монады позже.
Ответ 6
Это о типах: в первом случае, вы установите типы String
(для компании и модели) и Int
за год. Во втором случае ваши более общие. a
, b
и c
могут быть теми же типами, что и в первом примере, или что-то совершенно другое. Например, может быть полезно указать год как строку вместо целого. И если вы хотите, вы можете даже использовать свой тип Color
.