Haskell Type vs Data Constructor

124

Uczę się Haskella z learnyouahaskell.com . Mam problem ze zrozumieniem konstruktorów typów i konstruktorów danych. Na przykład tak naprawdę nie rozumiem różnicy między tym:

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

i to:

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

Rozumiem, że pierwszym jest po prostu użycie jednego konstruktora ( Car) do zbudowania danych typu Car. Drugiego naprawdę nie rozumiem.

Ponadto, w jaki sposób typy danych zdefiniowane w ten sposób:

data Color = Blue | Green | Red

pasuje do tego wszystkiego?

Z tego co rozumiem, trzeci przykład ( Color) jest typem, który może występować w trzech stanach: Blue, Greenlub Red. Ale to jest sprzeczne z tym, jak rozumiem pierwsze dwa przykłady: czy jest tak, że typ Carmoże być tylko w jednym stanie Car, którego budowa może wymagać różnych parametrów? Jeśli tak, to jak pasuje do tego drugi przykład?

Zasadniczo szukam wyjaśnienia, które ujednolica powyższe trzy przykłady / konstrukcje kodu.

Aristides
źródło
18
Twój przykład Car może być trochę zagmatwany, ponieważ Carjest zarówno konstruktorem typu (po lewej stronie =), jak i konstruktorem danych (po prawej stronie). W pierwszym przykładzie Carkonstruktor typu nie przyjmuje żadnych argumentów, w drugim przyjmuje trzy. W obu przykładach Carkonstruktor danych przyjmuje trzy argumenty (ale typy tych argumentów są w jednym przypadku ustalone, aw drugim sparametryzowane).
Simon Shine
pierwszy polega po prostu na użyciu jednego konstruktora danych ( Car :: String -> String -> Int -> Car) do zbudowania danych typu Car. druga polega po prostu na użyciu jednego konstruktora danych ( Car :: a -> b -> c -> Car a b c) do tworzenia danych typu Car a b c.
Will Ness

Odpowiedzi:

228

W datadeklaracji konstruktor typu znajduje się po lewej stronie znaku równości. Konstruktor danych (s) są rzeczy, na prawej stronie znaku równości. Używasz konstruktorów typów, w których oczekiwany jest typ, i używasz konstruktorów danych, w których oczekiwana jest wartość.

Konstruktory danych

Aby uprościć sprawę, możemy zacząć od przykładu typu, który reprezentuje kolor.

data Colour = Red | Green | Blue

Tutaj mamy trzy konstruktory danych. Colourjest typem i Greenjest konstruktorem zawierającym wartość typu Colour. Podobnie Redi Blueoba są konstruktorami, które konstruują wartości typu Colour. Moglibyśmy sobie jednak wyobrazić doprawienie tego!

data Colour = RGB Int Int Int

Nadal mamy tylko typ Colour, ale RGBnie jest to wartość - to funkcja pobierająca trzy Inty i zwracająca wartość! RGBma typ

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

RGBjest konstruktorem danych, który jest funkcją przyjmującą pewne wartości jako argumenty, a następnie używa ich do konstruowania nowej wartości. Jeśli zrobiłeś jakiekolwiek programowanie obiektowe, powinieneś to rozpoznać. W OOP konstruktory również przyjmują pewne wartości jako argumenty i zwracają nową wartość!

W tym przypadku, jeśli zastosujemy się RGBdo trzech wartości, otrzymamy wartość koloru!

Prelude> RGB 12 92 27
#0c5c1b

Mamy skonstruowana wartość typu Colourpoprzez stosowanie konstruktora danych. Konstruktor danych zawiera wartość podobną do zmiennej lub przyjmuje inne wartości jako argument i tworzy nową wartość . Jeśli wcześniej programowałeś, ta koncepcja nie powinna być dla ciebie zbyt dziwna.

Przerwa

Jeśli chcesz zbudować drzewo binarne do przechowywania Strings, możesz sobie wyobrazić coś takiego

data SBTree = Leaf String
            | Branch String SBTree SBTree

Widzimy tutaj typ, SBTreektóry zawiera dwa konstruktory danych. Innymi słowy, istnieją dwie funkcje (mianowicie Leafi Branch), które konstruują wartości SBTreetypu. Jeśli nie wiesz, jak działają drzewa binarne, po prostu trzymaj się tam. Właściwie nie musisz wiedzieć, jak działają drzewa binarne, tylko, że to Stringw jakiś sposób je przechowuje .

Widzimy również, że oba konstruktory danych przyjmują Stringargument - jest to String, który zamierzają przechowywać w drzewie.

Ale! A gdybyśmy chcieli mieć możliwość przechowywania Bool, musielibyśmy stworzyć nowe drzewo binarne. Może to wyglądać mniej więcej tak:

data BBTree = Leaf Bool
            | Branch Bool BBTree BBTree

Konstruktory typów

Oba SBTreei BBTreesą konstruktorami typów. Ale jest rażący problem. Czy widzisz, jakie są podobne? To znak, że naprawdę potrzebujesz gdzieś parametru.

Więc możemy to zrobić:

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

Teraz wprowadzamy zmienną typu a jako parametr do konstruktora typu. W tej deklaracji BTreestał się funkcją. Przyjmuje typ jako argument i zwraca nowy typ .

Ważne jest tutaj, aby wziąć pod uwagę różnicę między rodzaju betonu (przykłady obejmują Int, [Char]i Maybe Bool), który jest typem, który może być przypisany do wartości w programie, a także funkcja typu konstruktor które trzeba nakarmić typ, aby móc być przypisane do wartości. Wartość nigdy nie może być typu „lista”, ponieważ musi to być „lista czegoś ”. W tym samym duchu, wartość nigdy nie może być typu „drzewo binarne”, ponieważ musi to być „drzewo binarne przechowujące coś ”.

Jeśli przekażemy, powiedzmy, Boolargument do BTree, zwraca typ BTree Bool, który jest drzewem binarnym, które przechowuje Bools. Zastąp każde wystąpienie zmiennej atypu typem Bool, a sam przekonasz się, czy to prawda.

Jeśli chcesz, możesz wyświetlić BTreejako funkcję z rodzajem

BTree :: * -> *

Rodzaje są w pewnym sensie podobne do typów - *oznacza konkretny typ, więc mówimy, że BTreeprzechodzi od konkretnego typu do konkretnego.

Podsumowując

Cofnij się na chwilę i zwróć uwagę na podobieństwa.

  • Konstruktor danych to „funkcja”, która przyjmuje 0 lub więcej wartości i zwraca nową wartość.

  • Konstruktor typów to „funkcja”, która przyjmuje 0 lub więcej typów i zwraca nowy typ.

Konstruktory danych z parametrami są fajne, jeśli zależy nam na niewielkich różnicach w naszych wartościach - umieszczamy te różnice w parametrach i pozwalamy facetowi, który tworzy wartość, decydować, jakie argumenty wstawią. W tym samym sensie konstruktory typów z parametrami są fajne jeśli chcemy drobnych różnic w naszych typach! Umieszczamy te warianty jako parametry i pozwalamy facetowi, który tworzy typ, zdecydować, jakie argumenty wstawią.

Studium przypadku

Jako odcinek domowy możemy rozważyć Maybe atyp. Jego definicja to

data Maybe a = Nothing
             | Just a

Tutaj Maybejest konstruktor typu, który zwraca konkretny typ. Justjest konstruktorem danych, który zwraca wartość. Nothingjest konstruktorem danych zawierającym wartość. Jeśli spojrzymy na typ Just, widzimy to

Just :: a -> Maybe a

Innymi słowy, Justprzyjmuje wartość typu ai zwraca wartość typu Maybe a. Jeśli spojrzymy na ten rodzaj Maybe, zobaczymy to

Maybe :: * -> *

Innymi słowy, Maybeprzyjmuje konkretny typ i zwraca konkretny typ.

Jeszcze raz! Różnica między konkretnym typem a funkcją konstruktora typu. Nie możesz utworzyć listy Maybes - jeśli próbujesz wykonać

[] :: [Maybe]

pojawi się błąd. Możesz jednak utworzyć listę Maybe Intlub Maybe a. Dzieje się tak, ponieważ Maybejest to funkcja konstruktora typu, ale lista musi zawierać wartości konkretnego typu. Maybe Inti Maybe asą typami konkretnymi (lub, jeśli chcesz, wywołania funkcji konstruktora typu, które zwracają konkretne typy).

kqr
źródło
2
W pierwszym przykładzie zarówno RED GREEN, jak i BLUE to konstruktory, które nie przyjmują żadnych argumentów.
OllieB
3
Twierdzenie, że data Colour = Red | Green | Blue„ w ogóle nie mamy żadnych konstruktorów” jest po prostu błędne. Konstruktory typów i konstruktory danych nie muszą pobierać argumentów, patrz np. Haskell.org/haskellwiki/Constructor, który wskazuje, że w programie data Tree a = Tip | Node a (Tree a) (Tree a)„istnieją dwa konstruktory danych, Tip i Node”.
Frerich Raabe
1
@CMCDragonkai Masz całkowitą rację! Rodzaje to „typy typów”. Typowe podejście do łączenia pojęć typów i wartości nazywa się typowaniem zależnym . Idris to zainspirowany Haskellem język zależnie typizowany. Dzięki odpowiednim rozszerzeniom GHC możesz również zbliżyć się do pisania zależnego w Haskell. (Niektórzy żartują, że „Badania Haskella dotyczą ustalenia, jak blisko do typów zależnych możemy zbliżyć się bez posiadania typów zależnych.”)
kqr
1
@CMCDragonkai W rzeczywistości nie można mieć pustej deklaracji danych w standardowym Haskellu. Ale jest rozszerzenie GHC ( -XEmptyDataDecls), które pozwala to zrobić. Ponieważ, jak mówisz, nie ma wartości tego typu, funkcja f :: Int -> Zmoże na przykład nigdy nie zwracać (bo co by zwróciła?). Mogą być jednak przydatne, gdy potrzebujesz typów, ale tak naprawdę nie przejmujesz się wartościami .
kqr
1
Naprawdę to niemożliwe? Właśnie próbowałem w GHC i uruchomiłem to bez błędów. Nie musiałem ładować żadnych rozszerzeń GHC, tylko waniliowy GHC. Mógłbym wtedy napisać :k Zi po prostu dał mi to gwiazdę.
CMCDragonkai,
42

Haskell ma algebraiczne typy danych , które ma bardzo niewiele innych języków. Być może właśnie to cię dezorientuje.

W innych językach można zwykle utworzyć „rekord”, „strukturę” lub coś podobnego, które zawiera kilka nazwanych pól zawierających różne typy danych. Można również czasami zrobić „wyliczenie”, który ma (mały) zestaw możliwych wartości stałych (np twój Red, Greeni Blue).

W Haskell możesz łączyć oba te elementy jednocześnie. Dziwne, ale prawdziwe!

Dlaczego nazywa się to „algebraicznym”? Cóż, nerdy mówią o „typach sum” i „typach produktów”. Na przykład:

data Eg1 = One Int | Two String

Eg1Wartość jest w zasadzie albo liczbą całkowitą lub łańcuchem. Zatem zbiór wszystkich możliwych Eg1wartości jest „sumą” zbioru wszystkich możliwych wartości całkowitych i wszystkich możliwych wartości łańcuchowych. Dlatego nerdy określane są Eg1jako „typ sumy”. Z drugiej strony:

data Eg2 = Pair Int String

Każda Eg2wartość składa się zarówno liczbą całkowitą i ciąg. Zatem zbiór wszystkich możliwych Eg2wartości jest iloczynem kartezjańskim zbioru wszystkich liczb całkowitych i zbioru wszystkich ciągów. Te dwa zestawy są „mnożone” razem, więc jest to „typ produktu”.

Typy algebraiczne Haskella to sumy typów produktów . Dajesz konstruktorowi wiele pól, aby utworzyć typ produktu, i masz wiele konstruktorów, aby utworzyć sumę (produktów).

Jako przykład, dlaczego może to być przydatne, załóżmy, że masz coś, co wyprowadza dane jako XML lub JSON i wymaga rekordu konfiguracji - ale oczywiście ustawienia konfiguracji dla XML i JSON są zupełnie inne. Więc może zrobić coś takiego:

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

(Oczywiście z odpowiednimi polami). Nie można robić takich rzeczy w normalnych językach programowania, dlatego większość ludzi nie jest do tego przyzwyczajona.

MathematicalOrchid
źródło
4
świetny! tylko jedno: „Można je ... konstruować w prawie każdym języku”, mówi Wikipedia . :) W np. C / ++, to jest unions, z dyscypliną tagów. :)
Will Ness
5
Tak, ale za każdym razem, gdy o tym wspominam union, ludzie patrzą na mnie jak „kto do diabła kiedykolwiek tego używa ?” ;-)
MathematicalOrchid
1
Widziałem wiele unionużywanych w mojej karierze C. Proszę, nie mów, że to niepotrzebne, ponieważ tak nie jest.
trueadjustr
26

Zacznij od najprostszego przypadku:

data Color = Blue | Green | Red

Definiuje „konstruktor typu”, Colorktóry nie przyjmuje argumentów - i ma trzy „konstruktory danych” Blue, Greeni Red. Żaden z konstruktorów danych nie przyjmuje żadnych argumentów. Oznacza to, że istnieją trzy typu Color: Blue, Greeni Red.

Konstruktor danych jest używany, gdy trzeba utworzyć jakąś wartość. Lubić:

myFavoriteColor :: Color
myFavoriteColor = Green

tworzy wartość myFavoriteColorprzy użyciu Greenkonstruktora danych - i myFavoriteColorbędzie typu, Colorponieważ jest to typ wartości generowanych przez konstruktor danych.

Konstruktor typów jest używany, gdy trzeba utworzyć jakiś typ . Zwykle ma to miejsce podczas pisania podpisów:

isFavoriteColor :: Color -> Bool

W tym przypadku wywołujesz Colorkonstruktor typu (który nie przyjmuje żadnych argumentów).

Nadal ze mną?

Teraz wyobraź sobie, że nie tylko chciałeś stworzyć wartości koloru czerwonego / zielonego / niebieskiego, ale także chciałeś określić „intensywność”. Na przykład wartość z przedziału od 0 do 256. Możesz to zrobić, dodając argument do każdego z konstruktorów danych, więc otrzymasz:

data Color = Blue Int | Green Int | Red Int

Teraz każdy z trzech konstruktorów danych przyjmuje argument typu Int. Konstruktor typu ( Color) nadal nie przyjmuje żadnych argumentów. Więc mój ulubiony kolor to ciemnozielony, mogłem pisać

    myFavoriteColor :: Color
    myFavoriteColor = Green 50

I znowu wywołuje Greenkonstruktor danych i otrzymuję wartość typu Color.

Wyobraź sobie, że nie chcesz dyktować ludziom sposobu wyrażania intensywności koloru. Niektórzy mogą chcieć wartości liczbowej, tak jak właśnie zrobiliśmy. Inni mogą być w porządku, jeśli tylko wartość logiczna oznacza „jasny” lub „nie tak jasny”. Rozwiązaniem tego problemu jest nie kodowanie Intna stałe w konstruktorach danych, ale raczej użycie zmiennej typu:

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

Teraz nasz konstruktor typu przyjmuje jeden argument (inny typ, który właśnie wywołujemy a!), A wszystkie konstruktory danych przyjmą jeden argument (wartość!) Tego typu a. Więc mogłeś

myFavoriteColor :: Color Bool
myFavoriteColor = Green False

lub

myFavoriteColor :: Color Int
myFavoriteColor = Green 50

Zwróć uwagę, jak wywołujemy Colorkonstruktor typu z argumentem (innym typem), aby uzyskać typ „efektywny”, który zostanie zwrócony przez konstruktory danych. Dotyka to pojęcia rodzajów, o których warto poczytać przy filiżance kawy lub dwóch.

Teraz dowiedzieliśmy się, jakie są konstruktory danych i konstruktory typów oraz w jaki sposób konstruktory danych mogą przyjmować inne wartości jako argumenty, a konstruktory typów mogą przyjmować inne typy jako argumenty. HTH.

Frerich Raabe
źródło
Nie jestem pewien, czy przyjaźnię się z twoją koncepcją zerowego konstruktora danych. Wiem, że to powszechny sposób mówienia o stałych w Haskell, ale czy nie udowodniono tego kilka razy?
kqr
@kqr: Konstruktor danych może mieć wartość nullary, ale wtedy nie jest już funkcją. Funkcja to coś, co pobiera argument i zwraca wartość, tj. Coś, co ma ->w podpisie.
Frerich Raabe
Czy wartość może wskazywać na wiele typów? A może każda wartość jest powiązana tylko z jednym typem i to wszystko?
CMCDragonkai
1
@jrg Istnieje pewne nakładanie się, ale nie jest to spowodowane konstruktorami typów, ale zmiennymi typu, np. ain data Color a = Red a. ajest symbolem zastępczym dla dowolnego typu. Możesz mieć to samo w zwykłych funkcjach, np. Funkcja typu (a, b) -> apobiera krotkę dwóch wartości (typów ai b) i zwraca pierwszą wartość. Jest to funkcja „ogólna”, ponieważ nie narzuca typu elementów krotki - określa jedynie, że funkcja zwraca wartość tego samego typu, co pierwszy element krotki.
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.Jest to bardzo pomocne.
Jonas,
5

Jak zauważyli inni, polimorfizm nie jest tutaj tak strasznie przydatny. Spójrzmy na inny przykład, który prawdopodobnie już znasz:

Maybe a = Just a | Nothing

Ten typ ma dwa konstruktory danych. Nothingjest trochę nudny, nie zawiera żadnych przydatnych danych. Z drugiej strony Justzawiera wartość a- dowolnego typu a. Napiszmy funkcję używającą tego typu, np. Pobieranie nagłówka Intlisty, jeśli taka istnieje (mam nadzieję, że się zgadzasz, jest to bardziej przydatne niż rzucanie błędu):

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

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

Więc w tym przypadku ajest to Int, ale zadziałaby również dla każdego innego typu. W rzeczywistości możesz sprawić, by nasza funkcja działała dla każdego typu listy (nawet bez zmiany implementacji):

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

Z drugiej strony możesz pisać funkcje, które akceptują tylko określony typ Maybe, np

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

Krótko mówiąc, dzięki polimorfizmowi dajesz swojemu typowi elastyczność w pracy z wartościami innych typów.

W swoim przykładzie możesz w pewnym momencie zdecydować, że Stringnie wystarczy do zidentyfikowania firmy, ale musi ona mieć swój własny typ Company(który zawiera dodatkowe dane, takie jak kraj, adres, rachunki tylne itp.). Twoja pierwsza implementacja Carmusiałaby zostać zmieniona na używanie Companyzamiast Stringpierwszej wartości. Twoja druga implementacja jest w porządku, używasz jej tak Car Company String Int, jak poprzednio (oczywiście funkcje dostępu do danych firmy wymagają zmiany).

Landei
źródło
Czy można używać konstruktorów typów w kontekście danych innej deklaracji danych? Coś jak data Color = Blue ; data Bright = Color? Wypróbowałem to w ghci i wydaje się, że konstruktor Color w typie nie ma nic wspólnego z konstruktorem danych Color w definicji Bright. Istnieją tylko 2 konstruktory Color, jeden to Data, a drugi to Type.
CMCDragonkai
@CMCDragonkai Nie sądzę, że możesz to zrobić i nie jestem nawet pewien, co chcesz dzięki temu osiągnąć. Możesz „zawinąć” istniejący typ za pomocą datalub newtype(np. data Bright = Bright Color), Lub możesz użyć go typew celu zdefiniowania synonimu (np type Bright = Color.).
Landei
5

Drugi zawiera w sobie pojęcie „polimorfizmu”.

a b cMoże być dowolnego typu. Na przykład amoże być [String], bmoże być [Int] i cmoże być [Char].

Podczas gdy pierwszy typ jest ustalony: firma to a String, model to a, Stringa rok to Int.

Przykład samochodu może nie wskazywać na znaczenie stosowania polimorfizmu. Ale wyobraź sobie, że twoje dane są typu listy. Lista może zawierać. String, Char, Int ...W takich sytuacjach potrzebny będzie drugi sposób zdefiniowania danych.

Jeśli chodzi o trzeci sposób, nie sądzę, aby pasował do poprzedniego typu. To tylko inny sposób definiowania danych w Haskell.

To jest moja skromna opinia jako początkującego.

Przy okazji: upewnij się, że dobrze trenujesz swój mózg i czujesz się z tym dobrze. To klucz do późniejszego zrozumienia Monady.

McBear Holden
źródło
1

Chodzi o typy : w pierwszym przypadku ustawiasz typy String(dla firmy i modelu) oraz Introk. W drugim przypadku twój jest bardziej ogólny. a, bi cmogą być tego samego typu, co w pierwszym przykładzie, lub coś zupełnie innego. Np. Przydatne może być podanie roku w postaci łańcucha zamiast liczby całkowitej. A jeśli chcesz, możesz nawet użyć swojego Colortypu.

Matthias
źródło