Тип 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 применяя конструктор данных. Конструктор данных либо содержит значение, аналогичное переменной, либо принимает в качестве аргумента другие значения и создает новое значение. Если вы уже занимались программированием, эта концепция не должна быть для вас странной.

антракт

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

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, которое представляет собой двоичное дерево, которое хранит 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 являются конкретными типами (или, если хотите, вызовами функций конструктора типов, которые возвращают конкретные типы.)

У 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 тип.

Другие вопросы по тегам