String
Transkrypt
String
ROZDZIAŁ 8: KLASY TYPÓW Definicja typu Bool: data Bool = False | True Słowo kluczowe data oznacza, że definiujemy nowy typ danych. Część przed = określa typ jakim jest bool . Cześć po.. = są to konstruktory wartości, określają one różne wartości jakie może mieć ten typ. | jest czytane jako lub ( or) więc znaczy to : typ bool może mieć wartość true lub false. Zarówno nazwa jak i konstruktory wartości tworzą jedną całość. ALGEBRAICZNE TYPY DANYCH data Shape = Circle Float Float Float | Rectangle Float Float Float Float Dzięki temu typowi możemy stworzyć koło lub prostokąt Koło może być opisane jako (43.1, 55.0, 10.4) pierwsze 2 liczby to współrzędne środka okręgu 3 liczba to promień. Lepszym rozwiązaniem byłoby nasz typ miał własny kształt. Powiedzmy, że ten kształt by był kołem lub prostokątem Koło posiada konstruktor wartości z 3 polami typu float, więc kiedy piszemy wartość konstruktora możemy dodać parę typów definiując jakie typy będą zawierały . Pierwsze 2 pola są współrzędnymi jego środka, a 3 jego promieniem. Konstruktor wartości prostokąta ma 4 pola typu float . Pierwsze 2 wartości są współrzędnymi jego lewego górnego rogu , a 2 kolejne są długością i wysokością. DEFINICJA WŁASNEGO TYPU „SHAPE” surface :: Shape -> Float surface ( Circle _ _ r) = pi * r ^ 2 surface ( Rectangle x1 y1 x2 y2 ) = ( abs $ x2 - x1 ) * ( abs $ y2 - y1 ) Deklaracja typu mówi nam ze funkcja przyjmuje Shape i zwraca Float. Nie możemy napisać deklaracji typu Circle -> Float ponieważ Circle nie jest typem tylko typem jest Shape. Tak samo nie możemy napisać deklaracji True -> Int. We wzorcu funkcji na pole koła użyliśmy "podkreślników" zamiast 2 pierwszych wartości ponieważ w wyliczaniu pola nie są nam potrzebne. DEFINICJA FUNKCJI Z WŁASNYM TYPEM data Shape = Circle Float Float Float | Rectangle Float Float Float Float deriving ( Show ) Użycie deriving show na końcu deklaracji data haskell automatycznie przypisuje nasz typ do typu klasy show ghci > Circle 10 20 5 Circle 10.0 20.0 5.0 ghci > Rectangle 50 230 60 90 Rectangle 50.0 230.0 60.0 90.0 DERIVING ( SHOW ) Lista kół o tym samym środku z różnymi promieniami ghci > map ( Circle 10 20) [4 ,5 ,6 ,6] [ Circle 10.0 20.0 4.0 , Circle 10.0 20.0 5.0 , Circle 10.0 20.0 6.0 , Circle 10.0 20.0 6.0] MAPOWANIE KONSTRUKTORÓW WARTOŚCI data Point = Point Float Float deriving ( Show ) data Shape = Circle Point Float | Rectangle Point Point deriving ( Show ) Podczas definiowania Point użyliśmy tej samej nazwy dla typu danych i konstruktora wartości nie ma specjalnego znaczenia jednakże powszechnym jest używanie tej samej nazwy jaką ma typ. Jeżeli jest tylko jeden konstruktor wartości . Od teraz Circle ma 2 pola jedna jest typu Point a 2-gi typu Float. Teraz nasze kształty są łatwiejsze w rozumieniu. Przedefiniowanie funkcji surface: surface :: Shape -> Float surface ( Circle _ r) = pi * r ^ 2 surface ( Rectangle ( Point x1 y1 ) ( Point x2 y2 )) = ( abs $ x2 - x1 ) * ( abs $ y2- y1 ) Jedyną rzeczą którą musieliśmy zmienić był wzorzec. Dla koła zastąpiliśmy 2 podkreślniki jednym ponieważ nie potrzebujemy wartości zmiennej typu point. ghci > surface ( Rectangle ( Point 0 0) ( Point 100 100)) 10000.0 ghci > surface ( Circle ( Point 0 0) 24) 1809.5574 „SHAPE” Z POINT-EM nudge :: Shape -> Float -> Float -> Shape nudge ( Circle ( Point x y) r) a b = Circle ( Point ( x+a) (y+ b )) r nudge ( Rectangle ( Point x1 y1 ) ( Point x2 y2 )) a b = Rectangle ( Point ( x1 +a) ( y1 +b )) ( Point ( x2 + a) ( y2 +b )) Wywołanie: ghci > nudge ( Circle ( Point 34 34) 10) 5 10 PRZESUNIĘCIE FIGURY WZGLĘDEM OSI X,Y data Person = Person String String Int Float String String deriving ( Show ) Aby uzyskać poszczególne informacje (tj. tylko imię, nazwisko) należy zdefiniować następujące funkcje: firstName :: Person -> String firstName ( Person firstname _ _ _ _ _) = firstname lastName :: Person -> String lastName ( Person _ lastname _ _ _ _) = lastname age :: Person ->Int age ( Person _ _ age _ _ _) = age height :: Person ->Float height ( Person _ _ _ height _ _) = height phoneNumber :: Person -> String phoneNumber ( Person _ _ _ _ number _ ) = number flavor :: Person -> String flavor ( Person _ _ _ _ _ flavor ) = flavor SKŁADNIA REKORDÓW data Person = Person { firstName :: String , lastName :: String , age :: Int , height :: Float , phoneNumber :: String , flavor :: String } deriving ( Show ) Zamiast określania typów pól jeden po drugim oddzielonymi spacjami, używamy klamr. Najpierw piszemy nazwę pola np. firstName a następnie dodajemy :: (czterokropek) i określamy typ pola. Dzięki temu tworzymy jednocześnie ‘odnośniki’ do poszczególnych pól. SKŁADNIA REKORDÓW Typ konstruktorów mogą pobierać typy jako parametry do tworzenia nowych typów data Maybe a = Nothing | Just a Mamy tutaj a jako typ parametru . I właśnie dlatego wywołujemy funkcję Maybe. Ten typ może nam zwrócić Nothing lub typ wejściowy. Aby było to prawdziwe potrzebne są zapełnione typy parametrowe. ghci > Just " Haha " Just " Haha " ghci > Just 84 Just 84 ghci > :t Just " Haha " Just " Haha " :: Maybe [ Char ] ghci > :t Just 84 Just 84 :: ( Num t) => Maybe t ghci > :t Nothing Nothing :: Maybe a ghci > Just 10 :: Maybe Double Just 10.0 TYP PARAMETROWY Typy parametrowe są użyteczne ponieważ możemy zrobić różne typy w zależności od rodzaju typów jakie chcemy by były zawarte w naszym typie danych. Kiedy piszemy : " :t just " hahah" kompilator interpretuje że jest to Maybe z listą Charów [Char] , ponieważ widzi że otrzymał String/[Char] na wejściu. Typ maybe a może określić podobny do typu maybe int jeżeli jest 5 to wtedy przyjmuje inta lub double . Podobnie jeżeli lista jest pusta to wtedy jest [a] .Pusta lista moze działać jak listy czegokolwiek dlatego możemy zrobić [1,2,3] ++ [] and ["ha","ha","ha"] ++ []. Użycie typu parametrowego jest bardzo korzystne jeśli używamy go sensownie . Zazwyczaj używamy kiedy nasz typ danych będzie niezależne od typu wartości , wtedy wartości trzyma w środku. Podobnie jak typ " maybe a " JEŻELI NASZ TYP DZIAŁA JAK PEWNEGO RODZAJU POLE ( PUDEŁKO) WARTO Z NIEJ KORZYSTAĆ . MOŻEMY ZMIENIĆ NASZ CAR DATA: data Car = Car { company :: String , model :: String data Car a b c = Car { company :: a , year :: Int , model :: b } deriving ( Show ) , year :: c tellCar :: Car -> String } deriving ( Show ) tellCar ( Car { company = c , model = m , year = y }) = " This " ++ c ++ " " ++ m ++ " was made in " ++ show y tellCar :: ( Show a) => Car String String a -> String ghci > let stang = Car { company =" Ford " , model =" Mustang " , year =1967} tellCar ( Car { company = c , model = m , year = y }) = " This " ++ c ++ " " ++ m ghci > tellCar stang ++ " was made in " ++ show y " This Ford Mustang was made in 1967 " ghci > tellCar ( Car " Ford " " Mustang " 1967) " This Ford Mustang was made in 1967 " ghci > tellCar ( Car " Ford " " Mustang " " nineteen sixty seven ") " This Ford Mustang was made in \" nineteen sixty seven \" " ghci > :t Car " Ford " " Mustang " 1967 Car " Ford " " Mustang " 1967 :: ( Num t) = > Car [ Char ] [ Char ] t ghci > :t Car " Ford " " Mustang " " nineteen sixty seven " Car " Ford " " Mustang " " nineteen sixty seven " :: Car [ Char ] [ Char ] [ Char ] KLASY TYPÓW (DOŁĄCZANIE INSTANCJI) Klasy typów (typeclass) to rodzaj interfejsu, który określa pewne zachowania. Przykład: Typ int znajduje się w typie klasy eq ponieważ definiuje zachowanie elementów które mogą zostać utożsamione ze sobą. Ponieważ można porównywać liczby całkowite int jest on częścią typu klasy eq . Najczęściej wykorzystywaną funkcją w eq jest interfejs porównania == i /=. Jeśli typ jest częścią typu klasy eq, możemy użyć funkcji == z wartościami tego typu. Dlatego wyrażenia, takie jak 4 == 4 i "foo" / = "bar" może porównać te wartości. Wspomnijmy również o tym, że są one często mylone z klasami w językach takich jak Java, Python, C++ i tym podobnych , co czyni wielu ludzi. W tych językach klasy są projektem z których tworzymy obiekty, one zaś zawierają stany i mogą robić kilka czynności. Klasy typów są bardziej jak interfejsy. Nie tworzymy danych z typów klas. Zamiast tego, musimy najpierw zrobić nasz typ danych, a następnie zastanowić się jak możemy go wykorzystać. Jeśli może działać jak coś, co można porównać, robimy to w instancji typu klasy Eq. Jeśli może działać jak coś co można posortować, robimy to w instancji typu klasy Ord. Możemy ręcznie dodawać nasze typy instancji po przez realizacje funkcji zdefiniowanych przez typy klas. Haskell może automatycznie wykonać nasz typ instancji w następujących typach klas: eq, Ord, Enum ,Bounded, Show, Read. Haskell może "podczepić" zachowanie naszych typów w tych kontekstach, jeśli użyjemy słów kluczowych. data Person = Person { firstName :: String , lastName :: String , age :: Int } W powyższym kodzie opisaliśmy osobę. Załóżmy, że nie ma dwóch osób o tym samym imieniu, nazwisku oraz wieku. Teraz jeśli mamy rekord dla dwóch osób, czy ma on sens jeśli reprezentuję tę samą osobę? Pewnie, że nie. Możemy starać się je porównać i zobaczyć czy są one identyczne, czy nie. Wtedy ma to sens i dlatego jest częścią klasy typu eq . Gdy pobierzemy z eq instancję dla typu, a następnie spróbujemy porównać dwie wartości tego typu z == lub/=, Haskell spostrzeże że elementy nie pasują do siebie, a następnie sprawdzi, wszystkie dane zawarte wewnątrz testując każdą parę pól z ==. Jest tylko jeden haczyk, typy we wszystkich dziedzinach muszą również być częścią typu klasy eq . Ponieważ zarówno string i int są , więc jest w porządku. data Person = Person { firstName :: String , lastName :: String , age :: Int } deriving (Eq) SPRAWDŹMY NASZE INSTANCJE ghci > let mikeD = Person { firstName = " Michael ", lastName = " Diamond ", age = EQ: 43} ghci > let adRock = Person { firstName = " Adam ", lastName = " Horovitz ", age = 41} ghci > let mca = Person { firstName = " Adam ", lastName = " Yauch ", age = 44} ghci > mca == adRock False ghci > mikeD == adRock False ghci > mikeD == mikeD True ghci > mikeD == Person { firstName = " Michael ", lastName = " Diamond ", age = 43} True Oczywiście person jest teraz w eq, możemy użyć go do wszystkich funkcji które mają klasy ograniczone w eq, takich jak elem. ghci > let beastieBoys = [mca , adRock , mikeD ] ghci > mikeD `elem ` beastieBoys True Klasy typów show i Read, są elementami które mogą być konwertowane do lub z ciągów znaków. Podobnie jak w eq, jeśli konstruktory mają pola, ich typ mógłby być częścią show lub read, jeśli chcemy aby nasz typ instancji zawierał się w nich. Uczyńmy nasz typ danych person częścią show i read. data Person = Person { firstName :: String , lastName :: String , age :: Int } deriving (Eq , Show , Read ) Teraz możemy wypisać osoby na terminalu: ghci > let mikeD = Person { firstName = " Michael ", lastName = " Diamond ", age =43} ghci > mikeD Person { firstName = " Michael ", lastName = " Diamond ", age = 43} ghci > " mikeD is: " ++ show mikeD " mikeD is: Person { firstName = \" Michael \", lastName = \" Diamond \", age = 43} " Read jest odwrotnością klasy typu Show. Show jest konwersją wartości typu string. Read jest konwersją stringów do wartości naszego typu. Pamiętajmy jednak, gdy używamy funkcji read, musimy użyć adnotacji wyraźnego typu by powiedzieć Haskelowi jaki typ chcemy uzyskać w wyniku. Jeśli nie poprosimy o typ w wyniku, Haskell nie będzie wiedział jaki typ chcemy uzyskać. ghci > read " Person { firstName =\" Michael \", lastName =\" Diamond \", age = 43}":: Person Person { firstName = " Michael ", lastName = " Diamond ", age = 43} Jeśli użyjemy wyniku naszego read to Haskell będzie mógł wywnioskować, że powinien ją przeczytać jako person, wtedy nie musimy używać typu adnotacji ghci > read " Person { firstName =\" Michael \", lastName =\" Diamond \", age = 43}"== mikeD True Możemy również z read odczytać parametry typów, lecz pierw musimy je nimi wypełnić. Więc nie możemy napisać read ”Just 't' ’’::maybe a, ale możemy zapisać read”Just ‘t’’::Maybe Char. data Bool = False | True deriving (Ord) Ponieważ konstruktor false jest pierwszy a konstruktor true jest określony po nim, możemy rozważyć true jako wartość większą niż wartość false. ghci > True `compare ` False GT ghci > True > False True ghci > True < False False W typie danych Maybe konstruktor wartości Nothing określony jest przed wartością konstruktora Just, więc wartość Nothing jest zawsze mniejsze ni ż wartość Just, nawet jeśli wynosi ona minus miliard bilionów. Ale jeśli porównać dwie wartości Just, to należy porównać ich wnętrze. ghci > Nothing < Just 100 True ghci > Nothing > Just ( -49999) False ghci > Just 3 `compare ` Just 2 GT ghci > Just 100 > Just 50 True Lecz nie możemy robić czegoś takiego: Just (*3)>Just(*2), ponieważ (*3) i (*2) są funkcjami które nie są instancjami w Ord. Możemy za to łatwo korzystać z algebraicznych typów danych do wyliczeń i klasy typów Enum oraz bounded pomoże nam z tym. Rozważmy następujący typ danych: data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday Ponieważ wszystkie konstruktory wartości są puste( nie podejmują parametrów takich jak pola), możemy zrobić z niego część typu klasy Enum. Enum jest dla wartości, które posiadają poprzedników i następców. Możemy również zrobić to w części klasie typu Bounded,który jest dla typów, które mają najniższą możliwą wartość i najwyższą możliwą wartość. A skoro już przy tym jesteśmy, sprawdźmy wystąpienie wszystkich typów klas i zobaczmy, co możemy z nimi zrobić: data Day = Monday | Tuesday | Wednesday | Thursday | Friday | Saturday | Sunday deriving (Eq , Ord , Show , Read , Bounded , Enum ) Ponieważ jest to część klas typów Show I Read, możemy przekonwertować wartość tego typu jako string. ghci > Wednesday Wednesday ghci > show Wednesday " Wednesday " ghci > read " Saturday " :: Day Saturday Ponieważ jest to część klas typów eq i Ord, możemy je porównać (porównać dni): ghci > Saturday == Sunday False ghci > Saturday == Saturday True ghci > Saturday > Friday True ghci > Monday `compare ` Wednesday LT Jest to także część Bounded, więc możemy dodać najniższy i najwyższy dzień: ghci > minBound :: Day Monday ghci > maxBound :: Day Sunday Jest to również instancja Enum. Możemy dostać poprzednie i następne dni. I możemy zrobić listy zakresów od nich ghci > succ Monday Tuesday ghci > pred Saturday Friday ghci > [ Thursday .. Sunday ] [ Thursday , Friday , Saturday , Sunday ] ghci > [ minBound .. maxBound ] :: [Day] [ Monday , Tuesday , Wednesday , Thursday , Friday , Saturday , Sunday ] Wcześniej wspominaliśmy że podczas pisania typów [Char] i String typy te są sobie równoważne i są wymienne. Oznacza to że są zaimplementowane w typach synonimowych. Typy synonimowe tak naprawdę nie robi nic w sobie, jedynie daje pewne typy różnych nazw, więc nadaje sens komuś kto czyta nasz kod i dokumentacje. Tak więc standardowa biblioteka definiuje String jako synonim z [Char]. type String = [ Char ] Wprowadziliśmy słowo kluczowe type. Niektóre Słowa kluczowe mogą być mylące, gdyż aktualnie nie robimy niczego nowego ( zrobiliśmy to z kluczowych danych), właśnie wykonaliśmy synonim z już istniejących typów. Jeżeli zrobimy funkcje która konwertuje string do dużych liter i nazwiemy ją toUpperString, możemy nadać mu deklaracje typu toUpperString :: [Char] -> [Char] lub toUpperString :: String -> String. Obydwie deklaracje są zasadniczo takie same , jedynie ostatni zapis jest łatwiejszy do czytania. 8.5 TYPY SYNONIMOWE Kiedy mamy doczynienia z modułem Data. Map, najpierw zaprezentujemy książkę telefoniczną z listy asocjacyjnej następnie przekształciliśmy ją w map. Lista asocjacyjna to lista stworzona z par kluczy-wartości. Nasza wcześniejsza książka wyglądała następująco: phoneBook :: [( String , String )] phoneBook = [( " betty " ," 555 -2938 " ) ,(" bonnie " ," 452 -2928 ") ,(" patsy " ," 493 -2928 ") ,(" lucille " ," 205 -2928 " ) ,(" wendy " ," 939 -8282 ") ,(" penny " ," 853 -2492 ") ] Jak widzimy typ phoneBook jest [(String,String)]. Mówi nam to że jest to lista asocjacyjna która mapuje ze stringa na stringa, lecz nic więcej. Zróbmy typ synonimowy który przekazuje więcej informacji w deklaracji typu. type PhoneBook = [( String , String )] Teraz deklaracja typu z twojej książki telefonicznej może być phoneBook :: PhoneBook. Zróbmy typ synonimowy także dla String. type PhoneNumber = String type Name = String type PhoneBook = [( Name , PhoneNumber )] Nadawanie String typów synonimowych jest czymś co programiści Haskella chcą przekazać więcej informacji o stingach do funkcji które powinny być użyte jak i co one reprezentują. Teraz kiedy zaimplementujemy funkcje która pobiera nazwę i nr to widzi czy taka kombinacja naszej nazwy i nr jest w naszej książce telefonicznej. Możemy nadać bardziej opisową deklaracje typu: inPhoneBook :: Name -> PhoneNumber -> PhoneBook -> Bool inPhoneBook name pnumber pbook = ( name , pnumber ) `elem ` pbook Jeżeli postanowilibyśmy nie używać typów synonimowych, nasza funkcja wyglądała by: String -> String -> [(String,String)] -> Bool. W tym wypadku, deklaracja typu korzystająca z typów synonimowych jest łatwiejsza do zrozumienia. Jednak nie powinno się go nadużywać. Wprowadzamy typy synonimowe gdy opisujemy niektóre istniejące reprezentacje typów w naszych funkcjach (by ulepszyć dokumentację) lub kiedy coś ma długi typ który jest często powtarzany np: ([(String,String)]) . Typy synonimów mogą być również parametryzowane. Jeżeli chcemy aby typ który reprezentuje typ listy asocjacyjnej był nadal ogólny to możemy używać jakichkolwiek typów jako kluczy i wartości. Możemy zrobić to tak: type AssocList k v = [(k ,v )] Gdybyśmy chcieli typ który reprezentuje mapę (map) (z Data.Map) z liczb całkowitych możemy to zrobić tak: type IntMap v = Map Int v lub : type IntMap = Map Int Tak czy inaczej konstruktor typu IntMap bierze jeden parametr, który będzie wskazywać na liczby całkowite Innym fajnym typem danych który przyjmuje 2 typy jako swoje parametry jest typEither a b. data Either a b = Left a | Right b deriving (Eq , Ord , Read , Show ) Posiada dwa konstuktory wartości. Jeżeli Left jest użyty, to jego zawartością jest a i jeżeli Right jest użyty to jego zawartością jest b. Więc możemy użyć tego typu w enkapsulacji wartości z jednego typu lub innego i gdy mamy wartość typu Either a b, zazwyczaj wzór dopasowuje zarówno Left i Right, wtedy bazujemy na jednym z nich: ghci > Right 20 Right 20 ghci > Left " w00t " Left " w00t " ghci > :t Right 'a ' Right 'a ' :: Either a Char ghci > :t Left True Left True :: Either Bool b Do tej pory widzieliśmy Maybe a używane do reprezentacji wyników obliczeń które mogą być zarówno poprawne lub nie. Lecz niekiedy , Maybe jest niewystarczająco dobre gdyż Nothing nie przekazuje wielu informacji kiedy mu się nie powiedzie. Jest dobry tylko dla funkcji, które mogą wyłącznie w jeden sposób zawieść lub nie interesuje nas w jaki sposób funkcja zawiodła. Data.Map wyszukuje błędy tylko wtedy kiedy klucz którego szukaliśmy nie był w mapie, wtedy wiemy co się dokładnie stało. Jednak kiedy jesteśmy zainteresowani jak funkcja się popsuła lub dlaczego to zazwyczaj używamy typu wynikowego Either a b, gdzie a jest jakiegoś typu, który może coś powiedzieć o możliwej awarii i b jest typu udanych obliczeń. Stąd błędy używają Left wartości konstruktorów natomiast wyniki używają Right. Przykład: w szkole średniej są szafki tak, że uczniowie mają miejsce by umięsić swoje plakaty GUNS'N'ROSES. Każda szafka posiada kod. Kiedy uczeń chce nową szafkę mówi nadzorcy którą szafkę chce, a ten daje mu kod, ale jeśli ktoś używa tą szafkę, nie może powiedzieć mu kodu do szafki i uczeń musi wybrać inną. Użyjemy map z Data.Map do reprezentowania szafek. To będzie mapa z numerami szafek, z parą czy szafka jest używana czy nie i kod. import qualified Data . Map as Map data LockerState = Taken | Free deriving ( Show , Eq ) type Code = String type LockerMap = Map . Map Int ( LockerState , Code ) Wprowadzamy nowy typ danych do reprezentowania czy szafka jest wolna czy zajęta i ustawiamy typ synonimowy dla kodu szafki. Robimy również typ synonimowy dla typu który mapuje z intiger do par stanu szafek i ich kodu. Teraz przejdźmy do funkcji, która wyszukuje kod w mapie szafek. Zamierzamy użyć typu Either String Code do reprezentowania naszego wyniku, ponieważ nasze wyszukiwanie może się nie powieść w 2 przypadkach – kiedy szafka mogą być zajęte, w tym przypadku nie może powiedzieć kodu lub kiedy numer szafki może nie istnieć. Jeśli wyszukiwanie się nie powiedzie, użyjemy stringa by powiedzieć co się stało. lockerLookup :: Int -> LockerMap -> Either String Code lockerLookup lockerNumber map = case Map . lookup lockerNumber map of Nothing -> Left $ " Locker number " ++ show lockerNumber ++ " doesn 't exist !" Just ( state , code ) -> if state /= Taken then Right code else Left $ " Locker " ++ show lockerNumber ++ " is already taken !" Robimy normalne wyszukiwanie na mapie. Jeżeli otrzymamy Nothing, zwraca wartość typu Left String, mówiąc że szafka w ogóle nie istnieje. Jeśli znajdzie ją wtedy wykonuje kolejne sprawdzenie, czy szafka jest wolna. Jeżeli nie jest zwraca Left mówiąc jest niedostępna. Jeżeli nie jest wtedy zwraca wartość typu Right Code w którym dajemy studentowi prawidłowy kod do szafki. To aktualny Right String, lecz wprowadziliśmy typ synonimowy do wprowadzenia pewnej dodatkowej dokumentacji do deklaracji typu. Przykład map: lockers :: LockerMap lockers = Map . fromList [(100 ,( Taken ," ZD39I " )) ,(101 ,( Free ," JAH3I " )) ,(103 ,( Free ," IQSA9 " )) ,(105 ,( Free ," QOTSA " )) ,(109 ,( Taken ," 893 JJ " )) ,(110 ,( Taken ," 99292 " )) A oto kod szafki: ghci > lockerLookup 101 lockers Right " JAH3I " ghci > lockerLookup 100 lockers Left " Locker 100 is already taken !" ghci > lockerLookup 102 lockers Left " Locker number 102 doesn 't exist !" ghci > lockerLookup 110 lockers Left " Locker 110 is already taken !" ghci > lockerLookup 105 lockers Right " QOTSA " Moglibyśmy skorzystać z Maybe do prezentacji wyników lecz wtedy nie wiedzielibyśmy dlaczego nie możemy dostać kodu, a w tym przypadku dostajemy informację na temat problemu;) REKURENCYJNE STRUKTURY DANYCH Jak widzieliśmy konstruktor w algebraicznym typie danych może mieć kilka (lub wcale) pól i każde pole musi być jakimś konkretnym typem. Mając to na uwadze, możemy mieć typy których konstruktory mają pola które są tego samego typu. Możemy stworzyć rekurencyjne typy danych gdzie jedna wartość pewnego typu zawiera wartość danego typu, które zawierają więcej wartości tego samego typu i tak dalej. Przeanalizujmy listę: [5]. Jest to po prostu cukier syntaktyczny dla 5:[].Po lewej stronie : jest wartość a po prawej jest lista. W tym przypadku jest ona pustą listą. Teraz co z listą [4,5]? Więc "pocukrowane" to 4:(5:[]). Patrząc na pierwsze :, widzimy że ma również element z lewej strony i listę (:5) z prawej strony. Identycznie odnosi się do listy jak 3:(4:(5:6:[])), które mogą być napisane podobnie bądź 3:4:5:6[] (gdyż : jest prawo-asocjacyjne) lub [3,4,5,6]. Można powiedzieć że lista może być pustą listą lub może być elementem połączonym razem z : z inną listą (które mogą być pustymi listami lub nie). Użyjmy algebraicznych typów danych do implementacji nasza lista to: data List a = Empty | Cons a ( List a ) deriving ( Show , Read , Eq , Ord ) 8.6 REKURENCYJNE STRUKTURY DANYCH To wygląda jak definicja naszej listy z wcześniejszego paragrafu. To jest pusta lista lub połączenie głowy z jakąś wartością i listy. data List a = Empty | Cons { listHead :: a , listTail :: List a } deriving ( Show , Read , Eq , Ord ) Może nas zmieszać tutaj Cons konstruktora. Cons jest kolejnym słowem dla : . Widzisz w listach : jest faktycznym konstruktorem przyjmującym wartości oraz inną listę i zwracającym listę. Możemy już korzystać z naszej nowej listy typów! Innymi słowy ma dwa pola. Jedno pole jest typu a i kolejne jest typu [a]. ghci>Empty Empty ghci> 5 ` Cons ` Empty Cons 5 Empty ghci> 4 ` Cons ` (5 `Cons ` Empty ) Cons 4 ( Cons 5 Empty ) ghci> 3 ` Cons ` (4 `Cons ` (5 `Cons ` Empty )) Cons 3 ( Cons 4 ( Cons 5 Empty )) Funkcja infix-owa (wrostkowa) Wywołaliśmy nasz konstruktor Cons który jest wywołany infixowo więc możesz zobaczyć co do czego jest podobne :. Empty podobne do [] i 4 ‘Cons’ (5 `Cons` Empty)podobne do 4:(5:[]). Możemy zdefiniować że funkcje będą automatycznie infixowane tworząc je zawierające jedynie znaki specjalne. Możemy tak samo zrobić z konstruktorami ponieważ są one tylko funkcjami i zwracają jakiś typ danych. infixr 5 : -: data List a = Empty | a : -: ( List a) deriving ( Show , Read , Eq , Ord ) Po pierwsze zauważyliśmy nową konstrukcje składniową oraz deklaracje stałości. Kiedy definiujemy funkcje jako operatory możemy tego użyć by nadać im stałość (ale nie musimy tego robić). Stałość ustala jak mocno operator jest przypisany i czy jest powiązany lewostronnie czy prawostronnie. Dla przykładu stałość dla * to infixl 7 *i stałość + to infixl 6. Oznacza to że oba operatory są powiązane lewostronnie (4 * 3 * 2 to to samo co (4 * 3) * 2) ale * powiązuje mocniej niż + bo ma większą stałość więc 5 * 4 + 3to(5 * 4) + 3. W innym przypadku możemy napisać :-: (List a) zamiast Cons a (List a). Teraz więc możemy zapisywać nasze listy w sposób następujący ghci> 3 : -: 4 : -: 5 : -: Empty (: -:) 3 ((: -:) 4 ((: -:) 5 Empty )) ghci>let a = 3 : -: 4 : -: 5 : -: Empty ghci> 100 : -: a (: -:) 100 ((: -:) 3 ((: -:) 4 ((: -:) 5 Empty ))) Kiedy nasz typ wywodzi się od Show haskell nadal wyświetli go tak jak by był konstruktorem funkcji prefiksowej stąd nawiasy na zewnątrz operatora (4 + 3 to to samo co (+) 4 3). Zróbmy funkcje która doda dwie z naszych list razem. W ten sposób ++ definiujemy dla normalnych list: infixr 5 ++ (++) :: [ a] -> [a] -> [a] [] ++ ys = ys (x: xs ) ++ ys = x : ( xs ++ ys ) Nazwą funkcji będzie .++. infixr 5 .++ (.++) :: List a -> List a -> List a Empty .++ ys = ys (x : -: xs ) .++ ys = x : -: ( xs .++ ys ) I zobaczmy jak to działa ghci>let a = 3 : -: 4 : -: 5 : -: Empty ghci>let b = 6 : -: 7 : -: Empty ghci> a .++ b (: -:) 3 ((: -:) 4 ((: -:) 5 ((: -:) 6 ((: -:) 7 Empty )))) Teraz przejdźmy do implementacji drzewa wyszukiwania binarnego. Definicja drzewa binarnego: każdy punkty elementu posiada dwa elementy jednego z lewej a drugiego z prawej. Element po lewej jest mniejszy od elementu po prawej. Każdy z tych elementów może również wskazywać na dwa elementy (lub jeden, lub brak).W efekcie, każdy element ma maksymalnie dwa poddrzewa. Wszystkie elementy w lewym poddrzewie powiedzmy 5 będą mniejsze od 5. Elementy w prawym poddrzewie będą większe. Więc jeśli musimy znaleźć 8 jeśli jest w naszym drzewie chcemy zacząć od 5 i dlatego że 8 jest większa od 5 to pójdziemy w prawo. Jesteśmy teraz na 7 i dlatego że 8 jest większa od 7 więc pójdziemy znów w prawo. I znaleźliśmy nasz element w trzech ruchach. Teraz jeżeli to była normalna lista (lub drzewo, ale niezrównoważone), zajęło by nam to siedem ruchów zamiast trzech aby zobaczyć czy istnieje 8. Zestawy i mapy z Data.Set i Data.Map są zaimplementowane z wykorzystaniem drzewa, jedynie zamiast zwykłego przeszukiwania drzewa binarnego, używają zrównoważonego przeszukiwania drzewa binarnego, które są zrównoważone. Ale teraz trzeba po prostu zaimplementować normalne binarne drzewo przeszukiwań. Drzewo jest albo pustym drzewem lub drzewem które zawiera elementy o pewnej wartości i dwa poddrzewa. Brzmi jak idealne dopasowanie do typu danych algebraicznych! data Tree a = EmptyTree | Node a ( Tree a) ( Tree a) deriving ( Show , Read , Eq ) W języku tj. C robimy to modyfikując wskaźnik i wartość wewnątrz drzewa. W Haskellu nie możemy tak naprawdę modyfikować naszego drzewa, więc musimy zrobić nowe subdrzewo za każdym razem gdy decydujemy się iść w lewo lub w prawo, a na koniec wstawiania funkcja zwraca kompletnie nowe drzewo gdyż Haskell naprawdę nie ma pojęcia o wskaźnikach. Stąd typ naszej funkcji wstawiania będzie a ->Tree a - >Tree a. Pobiera element i drzewo i zwraca nowe drzewo które ma ten element w sobie. Więc mamy 2 funkcje. Jedna jest funkcją do tworzenia drzewa singleton (drzewo z jednym węzłem) i funkcja do wstawiania elementu do drzewa. singleton :: a ->Tree a singleton x = Node x EmptyTreeEmptyTree treeInsert :: ( Ord a ) = > a ->Tree a ->Tree a treeInsert x EmptyTree = singleton x treeInsert x ( Node a leftright ) | x == a = Node x leftright | x < a = Node a ( treeInsert x left ) right | x > a = Node a left ( treeInsert x right ) Funkcja singleton jest jedynie skrótem do tworzenia węzła, który coś zawiera i dwa puste sub-drzewa. W funkcji wstawiania najpierw mamy stany krawędzie jako wzorzec. Jeżeli doszliśmy do pustego sub-drzewa, to oznacza że jesteśmy tam gdzie chcemy i zamiast pustego drzewa stawiamy drzewo singleton z naszego elementu. Jeżeli nie wstawiamy do pustego drzewa to musimy sprawdzić kilka rzeczy. Po pierwsze jeżeli mamy do włożenia element równy elementowi głównemu po prostu zwróci drzewo takie same. Jeśli jest mniejszy zwróci drzewo które ma taką samą główną wartość. Ten sam korzeń prawego subdrzewa ale zamiast jego lewego sub-drzewa wstawi drzewo które ma wartość wstawioną do niego. Tak samo (lecz w drugą stronę) idzie gdy wartość jest większa od elementu głównego. treeElem :: ( Ord a) => a ->Tree a ->Bool treeElem x EmptyTree = False treeElem x ( Node a leftright ) | x == a = True | x < a = treeElem x left | x > a = treeElem x right Wszystko co musimy zrobić to napisać w poprzednim akapicie w kodzie. Zamiast ręcznej budowy jednego (choć możemy) użyjemy konkretnie do budowy drzewa z listy. Wypełnijmy nasze drzewo: ghci>letnums = [8 ,6 ,4 ,1 ,7 ,3 ,5] ghci>letnumsTree = foldrtreeInsertEmptyTreenums ghci>numsTree Node 5 ( Node 3 ( Node 1 EmptyTreeEmptyTree ) ( Node 4 EmptyTreeEmptyTree )) ( Node 7 ( Node 6 EmptyTreeEmptyTree ) ( Node 8 EmptyTreeEmptyTree )) W tym foldr, treeInsert była funkcją składania (przyjmuje drzewo i element listy i tworzy nowe drzewo) i EmptyTree zaczyna akumulować. Nums, była listą składaną. Kiedy ‘drukujemy’ nasze drzewo do konsoli to nie jest to zbytnio czytelne, ale jeśli spróbujemy możemy zrozumieć jego strukturę. Widzimy że węzeł główny jest 5 i ma dwa sub-drzewa jedno zawiera węzeł 3 i inny 7, itp. ghci> 8 ` treeElem ` numsTree True ghci> 100 ` treeElem ` numsTree False ghci> 1 ` treeElem ` numsTree True ghci> 10 ` treeElem ` numsTree False Klasy typów definiują pewne zachowanie (takie jak porównywanie, zliczanie) i typy mogą odnosić się do tworzenia instancji tych klas typów. W przypadku klasy typów zarchiwizowanych przy definicji funkcji lub tylko zadeklarowane typy które następnie wdrażamy. Więc kiedy mówimy że typy są instancją typów klas mamy na myśli że możemy użyć funkcje zdefiniowanych klas typów z typem. KLASY TYPÓW 102 class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y) Po pierwsze kiedy piszemy class Eq a where, to oznacza definicje nowego typu klas i nazywa się Eq. A jest typem zmiennej i oznacza że będzie odgrywać rolę typu wkrótce robi instancję Eq. Następnie definiujemy kilka funkcji. W każdym razie wdrażamy ciało funkcji dla funkcji Eq tylko określiliśmy je pod względem wzajemnej rekursji. Powiedzieliśmy że dwie instancje Eq są równe jeżeli nie są one różne i są różne jeżeli nie są równe. Jeżeli mamy mówić class Eq a where i następnie określić typ deklaracji w tej klasie (==) :: a -> -a -> Bool,wtedy gdy badamy typ tej funkcji później będzie miał typ (Eq a) =>a -> a -> Bool . data TrafficLight = Red | Yellow | Green Jeśli zdefiniowaliśmy TrafficLight. Zauważmy że nie czerpią z żadnej instancji klasy. To dlatego że mamy zamiar napisać kilka instancji po kolei, nawet jeżeli możemy czerpać z takich typów jak Eq i Show. Instancję dla Eq: instance Eq TrafficLight where Red == Red = True Green == Green = True Yellow == Yellow = True _ == _ = False Zrobiliśmy to za pomocą słowa kluczowego instance. Więc class jest dla definiowania nowej klasy typu i instance do tworzenia rodzaj instancji w typie klasy. == jest zdefiniowane w kategoriach /= i odwrotnie w deklaracji class. Aby spełnić minimalną pełną definicje dla Eq musimy nadpisać == lub /=. Aby spełnić minimalną kompletną definicje dla Show po prostu musimy zaimplementować funkcje Show która przyjmuje wartość i zmienia go na stringa. instance Show TrafficLight where show Red = " Red light " show Yellow = " Yellow light " show Green = " Green light " ghci > Red == Red True ghci > Red == Yellow False ghci > Red `elem ` [ Red , Yellow , Green ] True ghci > [ Red , Yellow , Green ] [ Red light , Yellow light , Green light ] Mogliśmy po prostu zaczerpnąć Eq i miałaby taki sam efekt. Jednak zaczerpnięcie Show musiałby tylko bezpośrednio przetłumaczyć wartość konstruktorów do stringa. Ale jeśli chcemy światło czerwone "Red light", wtedy musimy sporządzić deklarację instancji ręcznie. Można także dokonać typ klasy które są podklasą innych typów klas. Deklaracja klas z Num jest nieco długa ale tu jest pierwsza część class ( Eq a) => Num a where Jak już wcześniej wspomniano istnieje wiele miejsc w których możemy doczepić w konstruktorze klasy. Więc to jest jak pisanie class Num a where. Możemy w zasadzie powiedzieć że musimy dokonać typu instancji Eq, zanim będziemy mogli zrobić instancje z Num. Zanim jakiś typ może być uważany za numer, to ma sens gdyż możemy określić czy wartość tego typu można porównać czy nie. To wszystko to jest podklasa , to jedynie ograniczona klasa w klasie deklaracji. Kiedy określamy ciało funkcji w deklaracji klasy lub kiedy definiujemy je w deklaracji instancji możemy założyć że 'a' jest częścią Eq a więc możemy użyć == na wartościach tego typu. class Eq a where (==) :: a -> a -> Bool (/=) :: a -> a -> Bool x == y = not (x /= y) x /= y = not (x == y) Z deklaracji typu możemy zauważyć że 'a' jest użyte jako konkretny typ ponieważ wszystkie typy funkcji muszą być konkretne. Dlatego nie możemy zrobić czegoś takiego: instance Eq Maybe where Ponieważ jak widzieliśmy 'a' jest konkretnego typu lecz Maybe nie jest konkretnego typu. Ten typ konstruktora może mieć jeden parametr i tworzy konkretny typ. Byłoby to także żmudne pisząc instance Eq (Maybe Int) where, instance Eq (Maybe Char) where, itp. dla każdego typu. instance Eq ( Maybe m) where Just x == Just y = x == y Nothing == Nothing = True _ == _ = False To tak jakby powiedzieć że chcemy zrobić wszystkie typy z Maybe something instancji Eq. aktułalnie nie możemy napisać (Maybe something) ale zwykle decydujemy sie na pojedyńcze litery by były prawdziwe w stylu Haskella . (Maybe m) pełni tu rolę a gdzie class Eq a where. Kiedy maybe nie jest typu konkretnego maybe m jest. Okreslajac typ parametru ( m , jest małymi literami) powiedzieliśmy ze chcemy wszystkie typy które są w postaci Maybe m gdzie m jest dowolnego typu . W JavaScript i innych typach języków, można umieścić niemal wszystko wewnątrz wyrażenia. Na przykład, można zrobić wszystko z następującymi czynnościami: if (0) alert("YEAH!") else alert("NO!"), if ("") alert ("YEAH!") else alert("NO!"), if (false) alert("YEAH") else alert("NO!), itp., a wszystkie z nich będą wrzucać alert “NO!”. Jeśli to zrobisz, (“WHAT”), alert (“YEAH!”) else alert (“NO”), będzie alert(“YEAH”), KLASY TYPU YES-NO class YesNo a where yesno :: a -> Bool Całkiem proste. YesNo jest typem klasy określającej jedną funkcję. Ta funkcja przyjmuje jedną wartość typu, który może być uznane za wartość true-false, i mówi nam dla pewności czy to prawda czy nie. Zauważ, że gdy użyjemy a w funkcji, a staje się konkretnym typem. Następnie w kolejce zdefiniujemy kilka instancji. Dla liczb, będziemy zakładać (podobnie jak w JavaScript) każdy numer który nie jest 0 to true, a 0 jest false. Dla liczb, będziemy zakładać (podobnie jak w JavaScript) każdy numer który nie jest 0 to true, a 0 jest false. instance YesNo Int where yesno 0 = False yesno _ = True Puste listy (przez rozszerzenie stringów są watości no, a niepuste listy są wartości yes. instance YesNo [a] where yesno [] = False yesno _ = True zauważ, jak po prostu umieściliśmy rodzaj parametru a w tym, aby lista była konkretnego typu, chociaż nie dokonywaliśmy żadnych założeń co do rodzaju który jest zawarty w wykazie. Wiemy również że Bool sam posiada również true i false, i jest to dość oczywiste, co jest czym. instance YesNo Bool where yesno = id Id jest to po prostu zwykła funkcja biblioteki, która przyjmuje parametr i zwraca to samo, co ogólnie napiszemy. Zróbmy Maybe a w instancji: instance YesNo ( Maybe a) where yesno ( Just _) = True yesno Nothing = False Nie potrzebujemy klasy ograniczenia bo nie robimy założeń o treści Maybe. Po prostu powiedzieliśmy, że true jest to wartość Just i false jest Nothing. Sygnalizacja świetlna może być lub nie wartością. Pewnie. Jeśli jest czerwone musisz się zatrzymać, jeśli jest zielone to jedziesz. instance YesNo TrafficLight where yesno Red = False yesno _ = True Przetestujmy: ghci > yesno $ length [] False ghci > yesno " haha " True Funkcja, naśladująca konstrukcje warunkową, ale działa z wartością YesNo yesnoIf :: ( YesNo y) => y -> a -> a -> a yesnoIf yesnoVal yesResult noResult = if yesno yesnoVal then yesResult else noResult False Całkiem proste. Przyjmuje wartość yes-no i dwóch rzeczy. Jeśli wartość yes-no jest raczej tak, że zwraca pierwsze z dwóch rzeczy, w przeciwnym razie zwraca drugi z nich ghci > yesno $ Just 0 ghci > yesnoIf [] " YEAH !" "NO!" True "NO!" ghci > yesno True ghci > yesnoIf [2 ,3 ,4] " YEAH !" "NO!" True " YEAH !" ghci > yesno [] ghci > yesnoIf True " YEAH !" "NO!" False " YEAH !" ghci > yesno [0 ,0 ,0] ghci > yesnoIf ( Just 500) " YEAH !" "NO!" True " YEAH !" ghci > :t yesno ghci > yesnoIf Nothing " YEAH !" "NO!" yesno :: ( YesNo a) => a -> Bool "NO!" ghci > yesno "" Funkcja, naśladująca konstrukcje warunkową, ale działa z wartością YesNo yesnoIf :: ( YesNo y) => y -> a -> a -> a yesnoIf yesnoVal yesResult noResult = if yesno yesnoVal then yesResult else noResult Całkiem proste. Przyjmuje wartość yes-no i dwóch rzeczy. Jeśli wartość yes-no jest True, to zwraca pierwszą z dwóch rzeczy, w przeciwnym razie zwraca drugą z nich ghci > yesnoIf [] " YEAH !" "NO!" "NO!" ghci > yesnoIf [2 ,3 ,4] " YEAH !" "NO!" " YEAH !" ghci > yesnoIf True " YEAH !" "NO!" " YEAH !" ghci > yesnoIf ( Just 500) " YEAH !" "NO!" " YEAH !" ghci > yesnoIf Nothing " YEAH !" "NO!" "NO!" Teraz mamy zamiar spojrzeć na klasę typu ¡span class=”label class”¿Functor¡/span¿. Która jest w zasadzie odwzorowaniem. Typ listy jest częścią klasy typu Functor. class Functor f where fmap :: ( a -> b) -> f a -> f b Widzimy że definiuje jedną funkcję , fmap i nie ustawia żadnych domyślnych implementacji dla niego. W definicjach z klas typów typ zmiennej odgrywa rolę konkretnej klasy typu jak a w (==) :: (Eq a) =>a -> a -> Bool. Zamienia funkcję z jednego typu na inny oraz listę jednego typu i zwraca listę innego typu moi przyjaciele myślę ze mamy sam funktor . W rzeczywistości map jest fmap i działa tylko na listach . Tu jest jak lista jest instancją typu klasy Funktor instance Functor [] where fmap = map KLASA TYPU FUNKTOR To jest to zauważ jak nie napisać instance funktor [a] where , bo z fmap :: (a -> b) -> f a -> f b widzimy że f może być konstruktorem jednego typu. [a] jest juz konkretnego typu, kiedy [] jest konstruktorem typu które wykonuje jeden typ i produkuje typy podobne do [Int], [String] lub nawet [[String]] Ponieważ na listach fmap to map możemy uzyskać takie same wyniki przy użyciu ich na listach map :: (a -> b ) -> [ a] -> [ b] ghci > fmap (*2) [1..3] [2 ,4 ,6] ghci > map (*2) [1..3] [2 ,4 ,6] Typami które mogą działać jako pudełko ( box) mogą być funktory. Można myśleć o liście jako box które zawiera nieskończoną ilość małych przedziałów a wszystkie one mogą być puste .Jeden może być pełny a pozostałe puste lub ich liczba może być pełna . Inną rzeczą która może być odwzorowana i wykonana w instancji functor jest typ tree .Może ona być traktowana jako box ( pudełko) w sposób (posiada kilka lub nie ma wartości) i konstruktor tree trzyma dokładnie jeden typ parametru . Jeśli spojrzeć na fmap tak jak była wykonana tylko dla funkcji drzewa jej sygnatura będzie wyglądać (a -> b) -> Tree a -> Tree b zamierzamy użyć rekursji. Mapowanie na pustym drzewie będzie produkować puste drzewa, mapowanie na nie pustym drzewie będzie drzewo składające sie z naszej funkcji stosowane do korzenia i jego wartości lewego i prawego poddrzewa , tylko nasza funkcja będzie odwzorowana na nich. instance Functor Tree where fmap f EmptyTree = EmptyTree fmap f ( Node x leftsub rightsub ) = Node (f x) ( fmap f leftsub ) ( fmap f rightsub ) ghci > fmap (*2) EmptyTree EmptyTree ghci > fmap (*4) ( foldr treeInsert EmptyTree [5 ,7 ,3 ,2 ,1 ,7]) Node 28 ( Node 4 EmptyTree ( Node 8 EmptyTree ( Node 12 EmptyTree ( Node 20 EmptyTree EmptyTree )))) EmptyTree RODZAJE I PEWNE TYPY FOO Typy konstruktorów mogą przyjmować różne parametry, ewentualnie produkować konkretne typy. Zalety rodzaji: ghci > :k Int Int :: * "*" oznacza że jest to konkretny typ, który nie przyjmuje żadnych parametrów wartości. ghci > :k Maybe Int Maybe Int :: * Zastosowano typ parametru Maybe i otrzymaliśmy konkretny typ ( to co *->* oznacza równoległe lecz nie równoważne, typy i rodzaje są dwoma różnymi rzeczami :k używamy na typach aby uzyskać ich rodzaj. Tak samo możemy użyć :t na wartości aby uzyskać jego typ. Typy etykiet wartości i rodzaje etykiet typów są podobne do siebie. Sposób działania Maybe ghci > :k Maybe Maybe :: * -> * Typ konstruktora Maybe bierze jeden rodzaj konretnego typu (jak int) następnie zwraca Maybe Int. Tak jak Int -> Int znaczy to że funkcja pobiera int i zwraca int, * -> * oznacza to że typ konstruktora bierze jeden konkretny typ i go zwraca. ghci > :k Either Either :: * -> * -> * ghci > :k Either String Either String :: * -> * ghci > :k Either String Int Either String Int :: * Either bierze dwa konkretne typy jako typy parametrów od produkcji konkretnego typu. Wygląda to tak że typy deklaracji funkcji pobierają dwie wartości które coś zwracają. Typy konstruktorów tak jak i funkcje możemy zastosować częściowo. Kiedy chcemy zrobić Eitheri częścią Functor typu klas. Musimy częściowo zastosować je ponieważ Functor wymaga typów pobierających jeden parametr kiedy Either pobiera dwa. Innymi słowy Factor chce typy rodzaju *->*. Aby uzyskać typ rodzaju *->* zamiast orginalnego rodzaju *->*->* Jeżeli patrzymy na definicje Functor ponownie class Functor f where fmap :: ( a -> b) -> f a -> f b To typ zmiennej f jest używana jako typ pobierający do produkcji konkretnego typ. Wiemy że ma stworzyć konkretny typ gdyż jest używany jako typ o wartości funkcji, i od tego możemy wnioskować że typy zaprzyjaźnione z Functor muszą być rodzaju *->*. Definicja typu foo: class Tofu t where tofu :: j a -> t a j Ponieważ j a jest użyty jako typ wartości funkcji Tofu biorący jako parametr j a musi mieć rodzaj *. Zakładając * dla a możemy wnioskować że j może mieć rodzaj *->*. Widzimy że t produkuje konkretną wartość i przyjmuje ono dwa typy. I wiedząc że a ma rodzaj * i j ma rodzaj *->* możemy wnioskować że t musi mieć rodzaj *->(*->*)->* więc to ma konkretny typ 'a', typ konstruktora pobiera jeden konkretny typ (j) i tworzy konkretny typ Stwórzmy typ rodzaju *->(*->*)->* data Frank a b = Frank { frankField :: b a} deriving ( Show ) Załóżmy * dla a co oznacza że b przyjmuje jeden typ parametru rodzaju*->*. Teraz znamy rodzaj a i b, a ponieważ są parametrami dla Franka widzimy że Frank ma rodzaj *->(*->*)->*. Pierwsza * reprezentuje a i (*->*) to b. ghci > :t Frank { frankField = Just " HAHA " } Frank { frankField = Just " HAHA "} :: Frank [ Char ] Maybe ghci > :t Frank { frankField = Node 'a ' EmptyTree EmptyTree } Frank { frankField = Node 'a ' EmptyTree EmptyTree } :: Frank Char Tree ghci > :t Frank { frankField = " YES "} Frank { frankField = " YES "} :: Frank Char [] Ponieważ FrankField ma typ postaci a b, jego wartości muszą mieć podobnej postaci rodzaj. Więc może to być Just hahaha który ma typ Maybe [Char] lub może mieć wartość ['Y', 'e', 's'] który ma typ [Char]. Widzimy że typ wartości Frank odpowiada jego rodzajowi. [Char] ma rodzaj * i Maybe ma rodzaj *->* ponieważ w celu uzyskania wartości musi być konkretnego typu zatem musi być w pełni zastosowane. Każda wartość Frank blah blaah ma rodzaj *. data Barry t k p = Barry { yabba :: p , dabba :: t k } Functor chce typ rodzaju *->* ale Barry go nie posiada. Więc widzimy pobieranie 3 typów parametrów: something -> something -> something -> *. p jest konkretnego typu a więc ma rodzaj *. Dla k zakładamy gwiazdkę więc tym samym t jest rodzaju *->*. Teraz zamieniamy te rodzaje z czymś co jest używane jako zmienne i widzimy że ma rodzaj: (* -> *) -> * -> * -> *. ghci > :k Barry Barry :: (* -> *) -> * -> * -> * Teraz aby ten typ był częścią Fuktor musimy częściowo zastosować dwa pierwsze typy parametrów więc jesteśmy z lewej *->*. Oznacza to że początek zgłoszenia instancji będzie wynosić instance Functor (Barry a b) where. Jeśli spojrzymy na fmap jak by był wykonany dla Barry miałby rodzaj fmap :: (a -> b) -> Barry c d a -> Barry c d b ponieważ chcemy zamienić Functor's f z Barry c d. Trzecim typem parametru z Barry musimy zamienić i widzimy że jest wygodne dla własnego pola. instance Functor ( Barry a b ) where fmap f ( Barry { yabba = x , dabba = y }) = Barry { yabba = f x , dabba = y} Właśnie zmapowaliśmy f w pierwszym polu.