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.

Podobne dokumenty