Polimorfizm (z gr. wielopostaciowość) to w odniesieniu do funkcji możliwość działania na obiektach różnych typów. W tym miejscu zajmiemy się podstawową formą polimorfizmu - polimorfizmem parametrycznym.
Przypomnijmy funkcję activityOf
:
activityOf :: world ->
(Event -> world -> world) ->
(world -> Picture) ->
IO ()
Funkcja ta jest polimorficzna: możemy ją zastosować używając w miejscu zmiennej world
dowolnego typu.
Ważne aby pamiętać, że możliwość wyboru typu leży po stronie wywołującego. Oznacza to, że implementacja funkcji musi działać dla wszystkich typów. Co więcej, musi działać dla wszystkich typów tak samo.
Dlaczego parametryczność jest ważna?
-
Umożliwia wycieranie typów. Skoro funkcja wywoływana działa dla każdego typu tak samo, nie potrzebuje informacji, jakiego typu są jej faktyczne parametry. W związku z tym informacja typowa nie jest potrzebna w trakcie wykonania, a jedynie w trakcie kompilacji.
-
Ograniczenie sposobów działania funkcji polimorficznych daje nam twierdzenia za darmo (theorems for free, termin ukuty przez Phila Wadlera, a zarazem tytuł jego słynnej pracy).
📝 Rozważmy na przykład funkcje o następujących sygnaturach
zagadka1 :: a -> a
zagadka2 :: a -> b -> a
ile różnych implementacji potrafisz napisać?
📝 Trochę trudniejsze, być może do domu: co można powiedzieć o rodzinie funkcji typu
(a -> a) -> a -> a
Czy da się zdefiniować uniwersalną równość, czyli parametryczną funkcję typu eq :: a -> a -> Bool
?
Możemy podejrzewać, że nie, ale jak to udowodnić?
Otóż "darmowe twierdzenie" dla tego typu mówi że dla dowolnych typów A,B i funkcji
f :: A -> B
mamy
(eq x y) = (eq (f x) (f y))
co pokazuje, że eq
jest funkcją stałą, czyli nie jest zbyt użyteczna jako równość (chyba, że w sensie "wszyscy są równi").
O tym jak poradzić sobie z tym problemem porozmawiamy na kolejnych zajęciach.
Polimorficzne mogą być nie tylko funkcje, ale i typy danych. W poprzednim tygodniu pisaliśmy wariant funkcji activityOf
pozwalający na cofnięcie poziomu do stanu początkowego.
Spróbujmy teraz rozszerzyć tę funkcję o wyświetlanie ekranu startowego i rozpoczynanie właściwej gry po naciśnięciu spacji.
Na początek możemy stworzyć bardzo prosty ekran startowy:
startScreen :: Picture
startScreen = scaled 3 3 (lettering "Sokoban!")
Musimy wiedzieć czy jestesmy na ekranie startowym czy też gra już się toczy. Najprościej zapamiętać tę informację w stanie
data SSState = StartScreen | Running world
Niestety przy takim kodzie dostaniemy komunikat o błędzie Not in scope: type variable ‘world’
. Istotnie, co to jest world
?
Możemy uczynić go parametrem typu:
data SSState world = StartScreen | Running world
Teraz możemy zaimplementować:
startScreenActivityOf ::
world ->
(Event -> world -> world) ->
(world -> Picture) ->
IO ()
startScreenActivityOf state0 handle draw
= activityOf state0' handle' draw'
where
state0' = StartScreen
handle' (KeyPress key) StartScreen
| key == " " = Running state0
handle' _ StartScreen = StartScreen
handle' e (Running s) = Running (handle e s)
draw' StartScreen = startScreen
draw' (Running s) = draw s
📝 Dodaj ekran startowy do swojej gry.
Chcielibyśmy teraz połączyć funkcjonalność startScreenActivityOf
z funkcjonalnością resettableActivityOf
resettableActivityOf ::
world ->
(Event -> world -> world) ->
(world -> Picture) ->
IO ()
tak, aby nacisnięcie ESC
wracało do ekranu startowego. Ale nie możemy - obie te funkcje daja wynik typu IO()
a nie biorą argumentów takiego typu. Musimy spróbowac innego podejścia.
Gdybyśmy mieli typ Activity
, opisujący interakcje, oraz funkcje
resettable :: Activity -> Activity
withStartScreen :: Activity -> Activity
moglibyśmy uzyskać pożądany efekt przy pomocy ich złożenia. Potrzebowalibyśmy jeszcze funkcji
runActivity :: Activity -> IO ()
Jak możemy zdefiniować taki typ Activity
?
Na razie obrazek, zeby zasugerować rozwiązanie, ale jeszcze go nie zdradzać:
Musimy opakować argumenty funkcji activityOf
wewnątrz typu Activity
:
data Activity world = Activity {
actState :: world,
actHandle :: (Event -> world -> world),
actDraw ::(world -> Picture)
}
Zwróćmy uwagę, że dla pełnej ogólności typ świata world
musi być parametrem typu Activity
.
Implementacja funkcji resettable
nie przedstawia większych trudności - musimy po prostu wypakować potrzebne wartości
przy pomocy dopasowania wzorca:
resettable :: Activity s -> Activity s
resettable (Activity state0 handle draw)
= Activity state0 handle' draw
where handle' (KeyPress key) _ | key == "Esc" = state0
handle' e s = handle e s
Implementacja (a co najmniej zapisanie typu) funkcji withStartScreen
wymaga chwili namysłu. Zauważmy, że funkcjonalność tę osiagnęliśmy przez rozszerzenie stanu świata:
data SSState world = StartScreen | Running world
Sygnatura naszej funkcji może wyglądać tak:
withStartScreen :: Activity s -> Activity (SSState s)
a implementacja np. tak:
withStartScreen (Activity state0 handle draw)
= Activity state0' handle' draw'
where
state0' = StartScreen
handle' (KeyPress key) StartScreen
| key == " " = Running state0
handle' _ StartScreen = StartScreen
handle' e (Running s) = Running (handle e s)
draw' StartScreen = startScreen
draw' (Running s) = draw s
Do kompletu potrzebujemy jeszcze funkcji runActivity
.
📝 Napisz funkcję runActivity :: Activity s -> IO ()
📝 Przepisz funkcje walk2
i walk3
ze swojego rozwiązania tak aby używały funkcji runActivity
i resettable
.
📝 Napisz funkcję walk4 :: IO ()
rozszerzającą walk3
o ekran startowy.
https://github.com/jnp3-haskell-2021/sokoban-3
Oddawanie przez https://classroom.github.com/a/KEvrUDqe Termin: 22.11.2022 18:00