Тип 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конструктор даних приймає три аргументи (але типи цих аргументів в одному випадку виправлені, а в іншому параметризовані).
Simon Shine

Перше - просто використовувати один конструктор даних ( Car :: String -> String -> Int -> Car) для побудови даних типу Car. друге - просто використовувати один конструктор даних ( Car :: a -> b -> c -> Car a b c) для побудови даних типу Car a b c.
Буде Несс

Відповіді:


228

У dataдекларації конструктором типу є річ з лівої сторони знака рівності. Конструктор (s) дані речі на правій стороні від знака рівності. Ви використовуєте конструктори типів там, де очікується тип, і ви використовуєте конструктори даних там, де очікується значення.

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

Щоб зробити прості речі, ми можемо почати з прикладу типу, який представляє колір.

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- це конструктор даних, який є функцією, яка приймає деякі значення в якості аргументів, а потім використовує ці для побудови нового значення. Якщо ви виконали будь-яке об'єктно-орієнтоване програмування, вам слід визнати це. В OOP конструктори також приймають деякі значення як аргументи і повертають нове значення!

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


2
У вашому першому прикладі і КРАСНО-ЗЕЛЕНИЙ, і СВІТИЙ - це конструктори, які не беруть аргументів.
OllieB

3
Твердження, що в data Colour = Red | Green | Blue"у нас взагалі немає конструкторів", явно неправильно. Конструкторам типів та конструкторам даних не потрібно брати аргументи, див., Наприклад, haskell.org/haskellwiki/Constructor, який вказує на те, що в data Tree a = Tip | Node a (Tree a) (Tree a)"існують два конструктори даних, Підказка та Вузол".
Фріріх Раабе

1
@CMCDragonkai Ви абсолютно праві! Види - це "типи типів". Загальний підхід до об'єднання понять типів і значень називається залежним типізацією . Ідріс - натхненна 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значень є декартовим твором множини всіх цілих чисел і безлічі всіх рядків. Два набори "множать" разом, тому це "тип товару".

Алгебраїчні типи Хаскелла - це типові типи продуктів . Ви даєте конструктору кілька полів для створення типу продукту, а у вас є кілька конструкторів, щоб зробити суму (з продуктів).

Як приклад, чому це може бути корисним, припустимо, у вас є щось, що видає дані як XML або JSON, і для цього потрібен запис конфігурації, але, очевидно, налаштування конфігурації для XML і для JSON абсолютно різні. Таким чином , ви могли б зробити що - щось на зразок цього:

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

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


4
чудово! лише одне: "Вони можуть бути побудовані майже будь-якою мовою", - каже Вікіпедія . :) Наприклад, C / ++, це unions, з тегом дисципліни. :)
Буде Несс

5
Так, але кожен раз, коли я згадую union, люди дивляться на мене так, як "хто, до біса, коли-небудь використовує , що ??» ;-)
MathematicalOrchid

1
Я бачив багато union у своїй кар’єрі на С. Будь ласка, не робіть це непотрібним, бо це не так.
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 конструктор типу з аргументом (іншого типу), щоб отримати "ефективний" тип, який буде повернутий конструкторами даних. Це стосується поняття видів, про які ви можете прочитати за чашкою кави або двох.

Тепер ми розібралися, що таке конструктори даних та конструктори типів, і як конструктори даних можуть приймати інші значення як аргументи, а конструктори типів можуть приймати інші типи як аргументи. HTH.


Я не впевнений, що я знайомий з вашим уявленням про конструктор даних про нульові дані. Я знаю, що це звичайний спосіб говорити про константи в Хаскеллі, але хіба це було доволі некоректним?
kqr

@kqr: Конструктор даних може бути нульовим, але тоді це вже не функція. Функція - це те, що бере аргумент і дає значення, тобто щось із ->підписом.
Фріріх Раабе

Чи може значення вказувати на кілька типів? Або кожне значення пов'язане лише з 1 типом, і це все?
CMCDragonkai

1
@jrg Існує деяке перекриття, але це не конкретно через конструкторів типів, а через змінних типів, наприклад, ain data Color a = Red a. aє заповнювачем для довільного типу. Ви можете мати те ж саме у звичайних функціях, наприклад, наприклад, що функція типу (a, b) -> aприймає кортеж з двох значень (типів aі b) і дає перше значення. Це "загальна" функція, оскільки вона не диктує тип елементів кортежа - вона лише вказує, що функція дає значення того ж типу, що і перший елемент кортежу.
Фріріх Раабе

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.Це дуже корисно.
Йонас

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


Чи можете ви використовувати конструктори типів у контексті даних іншого декларування даних? Щось на кшталт data Color = Blue ; data Bright = Color? Я спробував це в ghci, і здається, що колір у конструкторі типу не має нічого спільного з конструктором даних Color у визначенні Bright. Існує лише 2 кольорових конструкторів, один - Data, а другий - Type.
CMCDragonkai

@CMCDragonkai Я не думаю, що ти можеш це зробити, і я навіть не впевнений, чого ти хочеш досягти з цим. Ви можете "обернути" існуючий тип за допомогою dataабо newtype(наприклад data Bright = Bright Color), або ви можете використовувати typeдля визначення синоніма (наприклад type Bright = Color).
Ландей

5

Друга - в ній поняття «поліморфізм».

Консерви a b cможуть бути будь-якого типу. Наприклад, aможе бути [String], bможе бути [Int] і cможе бути [Char].

Хоча тип першого є фіксованим: компанія - це String, модель - це Stringрік і рік Int.

Приклад Авто може не показати важливості використання поліморфізму. Але уявіть, що ваші дані є списком. Список може містити String, Char, Int ...У цих ситуаціях вам знадобиться другий спосіб визначення ваших даних.

Щодо третього способу, я не думаю, що він повинен відповідати попередньому типу. Це просто ще один спосіб визначення даних у Haskell.

Це моя покірна думка як початківця.

Btw: Переконайтесь, що ви добре тренуєте свій мозок і відчуваєте себе комфортно до цього. Це ключ до розуміння Монади пізніше.


1

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.