W data
deklaracji 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. Colour
jest typem i Green
jest konstruktorem zawierającym wartość typu Colour
. Podobnie Red
i Blue
oba 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 RGB
nie jest to wartość - to funkcja pobierająca trzy Inty i zwracająca wartość! RGB
ma typ
RGB :: Int -> Int -> Int -> Colour
RGB
jest 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ę RGB
do trzech wartości, otrzymamy wartość koloru!
Prelude> RGB 12 92 27
#0c5c1b
Mamy skonstruowana wartość typu Colour
poprzez 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 String
s, możesz sobie wyobrazić coś takiego
data SBTree = Leaf String
| Branch String SBTree SBTree
Widzimy tutaj typ, SBTree
który zawiera dwa konstruktory danych. Innymi słowy, istnieją dwie funkcje (mianowicie Leaf
i Branch
), które konstruują wartości SBTree
typu. 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 String
w jakiś sposób je przechowuje .
Widzimy również, że oba konstruktory danych przyjmują String
argument - 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 SBTree
i BBTree
są 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 BTree
stał 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, Bool
argument do BTree
, zwraca typ BTree Bool
, który jest drzewem binarnym, które przechowuje Bool
s. Zastąp każde wystąpienie zmiennej a
typu typem Bool
, a sam przekonasz się, czy to prawda.
Jeśli chcesz, możesz wyświetlić BTree
jako funkcję z rodzajem
BTree :: * -> *
Rodzaje są w pewnym sensie podobne do typów - *
oznacza konkretny typ, więc mówimy, że BTree
przechodzi 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 a
typ. Jego definicja to
data Maybe a = Nothing
| Just a
Tutaj Maybe
jest konstruktor typu, który zwraca konkretny typ. Just
jest konstruktorem danych, który zwraca wartość. Nothing
jest konstruktorem danych zawierającym wartość. Jeśli spojrzymy na typ Just
, widzimy to
Just :: a -> Maybe a
Innymi słowy, Just
przyjmuje wartość typu a
i zwraca wartość typu Maybe a
. Jeśli spojrzymy na ten rodzaj Maybe
, zobaczymy to
Maybe :: * -> *
Innymi słowy, Maybe
przyjmuje konkretny typ i zwraca konkretny typ.
Jeszcze raz! Różnica między konkretnym typem a funkcją konstruktora typu. Nie możesz utworzyć listy Maybe
s - jeśli próbujesz wykonać
[] :: [Maybe]
pojawi się błąd. Możesz jednak utworzyć listę Maybe Int
lub Maybe a
. Dzieje się tak, ponieważ Maybe
jest to funkcja konstruktora typu, ale lista musi zawierać wartości konkretnego typu. Maybe Int
i Maybe a
są typami konkretnymi (lub, jeśli chcesz, wywołania funkcji konstruktora typu, które zwracają konkretne typy).
Car
jest zarówno konstruktorem typu (po lewej stronie=
), jak i konstruktorem danych (po prawej stronie). W pierwszym przykładzieCar
konstruktor typu nie przyjmuje żadnych argumentów, w drugim przyjmuje trzy. W obu przykładachCar
konstruktor danych przyjmuje trzy argumenty (ale typy tych argumentów są w jednym przypadku ustalone, aw drugim sparametryzowane).