Тип Haskell против Конструктора Данных
Я изучаю Haskell от http://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
, который может принимать различные параметры для построения? Если да, то как вписывается второй пример?
По сути, я ищу объяснение, которое объединяет три приведенных выше примера кода / конструкции.
6 ответов
В 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
применяя конструктор данных. Конструктор данных либо содержит значение, аналогичное переменной, либо принимает в качестве аргумента другие значения и создает новое значение. Если вы уже занимались программированием, эта концепция не должна быть для вас странной.
антракт
Если вы хотите построить двоичное дерево для хранения 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
), который является типом, который может быть назначен значению в вашей программе, и функцией конструктора типа, которую необходимо передать типу, чтобы можно было присвоить значение. Значение никогда не может иметь тип "список", потому что оно должно быть "списком чего-либо". В том же духе значение никогда не может иметь тип "двоичное дерево", потому что оно должно быть "двоичным деревом, хранящим что-то".
Если мы перейдем, скажем, 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
s - если вы попытаетесь выполнить
[] :: [Maybe]
вы получите ошибку. Однако вы можете создать список Maybe Int
, или же Maybe a
, Это потому что Maybe
является функцией конструктора типа, но список должен содержать значения конкретного типа. Maybe Int
а также Maybe a
являются конкретными типами (или, если хотите, вызовами функций конструктора типов, которые возвращают конкретные типы.)
У Haskell есть алгебраические типы данных, которые есть у очень немногих других языков. Это, пожалуй, то, что вас смущает.
В других языках вы обычно можете создать "запись", "структуру" или подобное, в котором есть множество именованных полей, которые содержат различные типы данных. Вы также можете иногда сделать "перечисление", которое имеет (небольшой) набор фиксированных возможных значений (например, ваш Red
, Green
а также Blue
).
В Haskell вы можете комбинировать оба этих параметра одновременно. Странно, но это правда!
Почему это называется "алгебраическим"? Ну, ботаники говорят о "типах сумм" и "типах продуктов". Например:
data Eg1 = One Int | Two String
Eg1
значение в основном является целым числом или строкой. Итак, множество всего возможного Eg1
values - это сумма всех возможных целочисленных значений и всех возможных строковых значений. Таким образом, ботаники относятся к Eg1
как "тип суммы". С другой стороны:
data Eg2 = Pair Int String
каждый Eg2
значение состоит из целого числа и строки. Итак, множество всего возможного Eg2
Значения - это декартово произведение множества всех целых чисел и множества всех строк. Два набора "умножаются" вместе, так что это "тип продукта".
Алгебраические типы Хаскелла являются типами сумм типов произведений. Вы предоставляете конструктору несколько полей для создания типа продукта, и у вас есть несколько конструкторов для создания суммы (продуктов).
В качестве примера того, почему это может быть полезно, предположим, что у вас есть что-то, что выводит данные в виде XML или JSON, и для этого требуется запись конфигурации - но, очевидно, параметры конфигурации для XML и JSON совершенно разные. Так что вы можете сделать что-то вроде этого:
data Config = XML_Config {...} | JSON_Config {...}
(Очевидно, с некоторыми подходящими полями.) Вы не можете делать подобные вещи на обычных языках программирования, поэтому большинство людей к этому не привыкли.
Начнем с самого простого случая:
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
конструктор типа с аргументом (другого типа), чтобы получить "эффективный" тип, который будет возвращен конструкторами данных. Это касается концепции видов, о которых вы можете прочитать за чашкой кофе или двумя.
Теперь мы выяснили, что такое конструкторы данных и конструкторы типов, и как конструкторы данных могут принимать другие значения в качестве аргументов, а конструкторы типов могут принимать другие типы в качестве аргументов. НТН.
Как отмечали другие, полиморфизм здесь не так уж и полезен. Давайте рассмотрим другой пример, с которым вы, вероятно, уже знакомы:
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
и это будет работать как прежде (конечно, функции доступа к данным компании должны быть изменены).
Во втором есть понятие "полиморфизм".
a b c
может быть любого типа. Например, a
может быть [String]
, b
может быть [Int]
а также c
может быть [Char]
,
В то время как первый тип исправлен: компания является String
модель является String
и год Int
,
Пример Car может не показывать значение использования полиморфизма. Но представьте, что ваши данные относятся к типу списка. Список может содержать String, Char, Int ...
В этих ситуациях вам понадобится второй способ определения ваших данных.
Что касается третьего способа, я не думаю, что он должен соответствовать предыдущему типу. Это еще один способ определения данных в Haskell.
Это мое скромное мнение как начинающего.
Кстати: убедитесь, что вы хорошо тренируете свой мозг и чувствуете себя комфортно. Это ключ, чтобы понять Монаду позже.
Речь идет о типах: в первом случае вы устанавливаете типы String
(для компании и модели) и Int
на год. Во втором случае вы более универсальны. a
, b
, а также c
могут быть те же типы, что и в первом примере, или что-то совершенно другое. Например, может быть полезно указывать год в виде строки вместо целого числа. И если вы хотите, вы можете даже использовать свой Color
тип.