|
|
pl:dydaktyka:pp:haskell:lab-more-funcs [2018/05/23 12:38] esimon [Zadania] |
pl:dydaktyka:pp:haskell:lab-more-funcs [2019/06/27 15:50] |
====== Lab: Więcej funkcji ====== | |
| |
Tematyka: | |
* funkcje wyższego rzędu | |
* składanie funkcji | |
* funkcje anonimowe (lambdy) | |
* funkcje: map, fold, filter | |
| |
===== Wprowadzenie ===== | |
| |
==== Funkcje wyższego rzędu ==== | |
| |
* Większość języków programowania pozwala na tworzenie funkcji, które mają dowolną liczbę argumentów, ale to jest niepotrzebne. W Haskellu wszystkie funkcje przyjmują dokładnie **jeden** argument, nie tracąc przy tym na ekspresywności języka. Nazywa się to rozwijaniem funkcji (ang. //currying//, na cześć [[wp>Haskell_Curry|Haskella Curry'ego]]; zgadnij skąd wzięła się nazwa języka "Haskell" :-D) | |
* Aby to działało czynimy użytek z **funkcji wyższego rzędu**, czyli funkcji, które zwracają funkcje. Spotkaliśmy się z nimi już wcześniej, wróćmy do funkcji sumującej z poprzednich zajęć: <code haskell>sum2c :: Int -> Int -> Int | |
sum2c m n = m + n</code> to co się tutaj tak naprawdę dzieje to: <code haskell>sum2c :: Int -> (Int -> Int)</code> czyli funkcja ''%%sum2c%%'' przyjmuje argument typu ''%%Int%%'' i zwraca funkcję, której typem jest ''%%Int -> Int%%'' | |
* Co nam to daje? | |
* Skoro po przyjęciu pierwszego argumentu zwracana jest funkcja i do niej dopiero jest aplikowany kolejny argument, poprawnym wyrażeniem będzie: <code haskell>dodaj1 :: Int -> Int | |
dodaj1 = sum2c 1</code> dodaj1 jest funkcją zwróconą przez ''%%sum2c 1%%'', czyli funkcją dodającą 1 do swojego argumentu\\ //Takie częściowe określenie argumentów funkcji nazywa się [[wp>Partial_application]]// | |
* Idźmy dalej: pamiętasz funkcję ''%%map%%'', którą implementowaliśmy na pierwszych zajęciach? Jaki jest jej typ? <code haskell>Prelude> :t map | |
map :: (a -> b) -> [a] -> [b]</code> dodajmy do tego brakujący nawias, aby nie było wątpliwości co tutaj się dzieje: <code haskell>map :: (a -> b) -> ([a] -> [b])</code> czyli: map przyjmuje jako swój argument funkcję o typie ''%%a -> b%%'' i zwraca funkcję o typie ''%%[a] -> [b]%%'', czyli jak najbardziej możemy wywołać: <code haskell>map dodaj1 [1..10]</code> nie musisz nawet tworzyć osobnej funkcji ''dodaj1''! map może przyjąć to co jest zwrócone przez ''%%sum2c 1%%'': <code haskell>map (sum2c 1) [1..10]</code> możesz nawet pójść o krok dalej i stworzyć funkcję, która dodaje 1 do wszystkich elementów listy! <code haskell>dodaj1doListy :: [Int] -> [Int] | |
dodaj1doListy = map dodaj1</code> | |
| |
==== Składanie funkcji ==== | |
| |
* Co w sytuacji kiedy chcemy zrobić więcej niż jedną operację na każdym z elementów listy? Nie tylko dodać 1, ale później również pomnożyć przez 2? | |
* Możemy oczywiście wykonać najpierw jedną operację na wszystkich elementach listy, a następnie drugą: <code haskell>map (* 2) (map dodaj1 [1..10])</code> | |
* Możemy też wykorzystać **składanie funkcji** wykonywane za pomocą **operatora ''.'' (kropka)**. Poniższy kod robi dokładnie to samo (ale w jednej iteracji po liście):<code haskell>map ((* 2) . dodaj1) [1..10]</code> | |
* Sprawdź typ operatora ''.'' za pomocą: <code haskell>:t (.)</code> czy rozumiesz ten typ? | |
| |
==== Jeszcze trochę o funkcjach: listy, lambdy, $ ==== | |
| |
* W Haskellu mamy trzy podstawowe funkcje robiące coś na listach: | |
* **''map''** -- wywołuje funkcję podaną jako argument na każdym z elementów; tę funkcję już znasz | |
* **''filter''** -- sprawdza czy każdy z elementów spełnia zadany warunek (czy funkcja podana jako argument zwróciła True), np. możemy wybrać z listy wszystkie elementy mniejsze od 5:<code haskell>filter (<5) [1..10]</code> | |
* **''fold''** -- przechodzi po wszystkich elementach, wykonując zadaną operację, a wynik zapisując do akumulatora; tak naprawdę jest to cała rodzina funkcji różniących się sposobem przechodzenia po liście -- dwie podstawowe to foldl i foldr, przechodzące po liście odpowiednio od lewej i od prawej strony, np. możemy zsumować wszystkie elementy listy: <code haskell>foldl (+) 0 [1..10]</code> gdzie (+) jest funkcją, a 0 jest akumulatorem, do którego będą dodawane kolejne elementy listy -> ładne wyjaśnienie graficzne działania fold można znaleźć [[http://www.cse.unsw.edu.au/~en1000/haskell/hof.html|na stronie]] | |
| |
* Przypisywanie nazwy do każdej funkcji mija się z celem - niektóre funkcje tworzymy ad hoc do jednorazowego wykorzystania, np. do funkcji map/fold/filter i przypisywanie ich do nazwy to marnowanie zasobów (i zwiększanie długości kodu). W takiej sytuacji możemy definiować **funkcje anonimowe (lambdy)** -- znasz je już pewnie z innych języków programowania. | |
* W Haskellu lambdy rozpoczynamy od ''\'' oraz wykorzystujemy operator ''%%->%%'' <code haskell>-- nazwana funkcja: | |
f x = x + 1 | |
-- dokładnie ta sama funkcjonalność zapisana jako funkcja anonimowa: | |
f = \x -> x+1</code> | |
* Możemy też zdefiniować funkcję anonimową, która przyjmuje więcej niż jeden argument (tzn. funkcję, która zwraca funkcję, itd). Wracając do przykładu z funkcją sumującą: <code haskell>sum2lambda = \x y -> x + y</code> | |
| |
* Jeżeli jeszcze tego nie pamiętasz (a już po pierwszych zajęciach powinno to się wryć w pamięć :!:) -- największy priorytet w Haskellu ma spacja, czyli aplikacja argumentu do funkcji. Efektem jest to, że czasami potrzeba dużo nawiasów, aby zrealizować złożone wywołanie, aby działało zgodnie z naszymi założeniami. Z pomocą przychodzi tutaj operator ''%%$%%'', który ma bardzo niski priorytet + co najważniejsze: jest łączony z prawej strony, a nie z lewej jak spacja. Co nam to daje? Porównaj dwa wywołania: | |
* bez ''%%$%%'' : <code haskell>sum (filter (> 10) (map (*2) [2..10])) -- f (g (z x))</code> | |
* z wykorzystaniem ''%%$%%'' : <code haskell>sum $ filter (> 10) $ map (*2) [2..10] -- f $ g $ z x</code> | |
* Jest to tylko //syntactic sugar// - kod jest czytelniejszy i ładniejszy (i bardziej Haskell-way), ale nie wnosi to nowej funkcjonalności | |
| |
* Przydatne mogą być słowa kluczowe ''where'' i ''let'' -> możesz o nich poczytać [[http://learnyouahaskell.com/syntax-in-functions#where|tutaj]] | |
| |
===== Zadania ===== | |
| |
- Dane jest wyrażenie <code haskell>[f x | x <- lista, p x]</code> Zdefiniuj własną funkcję wykorzystującą wybrane z funkcji: map, fold, filter, aby otrzymać dokładnie taki sam efekt. Aby to przetestować możesz skorzystać z poniższego kodu -- funkcje ''%%mojeLiczby%%'' i ''%%mojeLiczby'%%'' powinny zwracać w efekcie dokładnie tę samą listę! <code haskell>mojeLiczby = [f x | x <- lista, p x] | |
where f = \a -> 2 * a -- f mnoży liczbę razy 2 | |
lista = [1..10] -- lista początkowa | |
p = \b -> b `mod` 2 == 0 -- p wybiera liczby parzyste | |
| |
mojeLiczby' = -- TUTAJ WPISZ SWOJĄ FUNKCJĘ! | |
where f = \a -> 2 * a | |
lista = [1..10] | |
p = \b -> b `mod` 2 == 0</code> | |
- Zdefiniuj funkcję anonimową, która przyjmuje jako argument operator oraz liczbę i zwraca funkcję wykonującą daną operację matematyczną. Przypisz tę funkcję do nazwy ''generatorOperator''. | |
* **Nagłówek funkcji:** <code haskell>generatorOperator :: (lewa -> prawa -> wynik) -> lewa -> (prawa -> wynik) | |
-- funkcja przyjmuje operator, który jest typu (lewa -> prawa -> wynik) | |
-- oraz lewą część operatora i zwraca funkcję, która przyjmuje prawą część operatora i zwraca wynik</code> | |
* **Przykład użycia:** <code haskell>> dodaj3 = generatorOperator (+) 3 | |
> dodaj3 2 | |
5 | |
> podziel100 = generatorOperator (/) 100 | |
> podziel100 8 | |
12.5</code> | |
- Wykorzystując ''fold'' zdefiniuj funkcję odwracającą String. | |
* **Nagłówek funkcji:** <code haskell>reverse' :: String -> String</code> | |
* **Przykład użycia:** <code haskell>> reverse' "Kocham Haskella" | |
"alleksaH mahcoK" | |
> reverse' "kobyla ma maly bok" | |
"kob ylam am alybok"</code> | |
- Napisz funkcję ''policzISumuj'', która przyjmuje trzy argumenty: funkcję, którą ma zaaplikować do każdego z elementów listy oraz pierwszy i ostatni element zakresu dla którego ma zostać zastosowana. W wyniku funkcja zwraca sumę wyników zaaplikowania funkcji do każdego z elementów. | |
* **Nagłówek funkcji:** <code haskell>policzISumuj :: (Int -> Int) -> Int -> Int -> Int</code> | |
* **Przykład użycia:** <code haskell>>policzISumuj (^2) 1 10 | |
385 | |
> policzISumuj (\x -> 42) 123 127 | |
210</code> | |
- Wykorzystując funkcję ''filter'' oraz lambdy, stwórz funkcję wybierającą z zadanej listy liczby pierwsze. | |
* **Nagłówek funkcji:** <code haskell>pierwsze :: [Int] -> [Int]</code> | |
* **Przykład użycia:** <code haskell>> pierwsze [100..110] | |
[101,103,107,109] | |
> take 15 $ pierwsze [1..] | |
[2,3,5,7,11,13,17,19,23,29,31,37,41,43]</code> | |
- Korzystając ze swojej aktualnej wiedzy napisz jeszcze raz funkcję ''conajmniejn'' z poprzedniego laboratorium, ale tym razem jako **jedną funkcję bez wykorzystania funkcji pomocniczych** (__dodatkowe utrudnienie dla zainteresowanych:__ zrób to bez korzystania z funkcji ''[[http://hackage.haskell.org/package/base-4.11.1.0/docs/Data-List.html#v:nub|nub]]''). Dla przypomnienia: | |
* **Nagłówek funkcji:** <code haskell>conajmniejn' :: [Int] -> Int -> [Int]</code> | |
* **Przykład użycia:** <code haskell>> conajmniejn' [4,5,2,5,4,3,1,3,4] 2 | |
[5,3,4] | |
> conajmniejn' [4,5,2,5,4,3,1,3,4] 4 | |
[]</code> | |