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ść 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ęć:
    sum2c :: Int -> Int -> Int
    sum2c m n = m + n

    to co się tutaj tak naprawdę dzieje to:

    sum2c :: Int -> (Int -> Int)

    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:
      dodaj1 :: Int -> Int
      dodaj1 = sum2c 1

      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ę Partial_application

    • Idźmy dalej: pamiętasz funkcję map, którą implementowaliśmy na pierwszych zajęciach? Jaki jest jej typ?
      ghci> :t map
      map :: (a -> b) -> [a] -> [b]

      dodajmy do tego brakujący nawias, aby nie było wątpliwości co tutaj się dzieje:

      map :: (a -> b) -> ([a] -> [b])

      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ć:

      map dodaj1 [1..10]

      nie musisz nawet tworzyć osobnej funkcji dodaj1! map może przyjąć to co jest zwrócone przez sum2c 1:

      map (sum2c 1) [1..10]

      możesz nawet pójść o krok dalej i stworzyć funkcję, która dodaje 1 do wszystkich elementów listy!

      dodaj1doListy :: [Int] -> [Int]
      dodaj1doListy = map dodaj1

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ą:
    map (* 2) (map dodaj1 [1..10])
  • 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):
    map ((* 2) . dodaj1) [1..10]
  • Sprawdź typ operatora . za pomocą:
    :t (.)

    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:
      filter (<5) [1..10]
    • 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:
      foldl (+) 0 [1..10]

      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źć 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 ->
      -- nazwana funkcja:
      f x = x + 1
      -- dokładnie ta sama funkcjonalność zapisana jako funkcja anonimowa:
      f = \x -> x+1
    • 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ą:
      sum2lambda = \x y -> x + y
  • 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 $ :
      sum (filter (> 10) (map (*2) [2..10]))  -- f (g (z x))
    • z wykorzystaniem $ :
      sum $ filter (> 10) $ map (*2) [2..10]  -- f $ g $ z x
  • 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ć tutaj

Zadania

  1. Dane jest wyrażenie
    [f x | x <- lista, p x]

    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ę!

    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
  2. 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:
      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
    • Przykład użycia:
      ghci> dodaj3 = generatorOperator (+) 3
      ghci> dodaj3 2
      5
      ghci> podziel100 = generatorOperator (/) 100
      ghci> podziel100 8
      12.5
  3. Wykorzystując fold zdefiniuj funkcję odwracającą String.
    • Nagłówek funkcji:
      myReverse :: String -> String
    • Przykład użycia:
      ghci> myReverse "Kocham Haskella"
      "alleksaH mahcoK"
      ghci> myReverse "kobyla ma maly bok"
      "kob ylam am alybok"
  4. 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:
      policzISumuj :: (Int -> Int) -> Int -> Int -> Int
    • Przykład użycia:
      ghci>policzISumuj (^2) 1 10
      385
      ghci> policzISumuj (\x -> 42) 123 127
      210
  5. Wykorzystując funkcję filter oraz lambdy, stwórz funkcję wybierającą z zadanej listy liczby pierwsze.
    • Nagłówek funkcji:
      pierwsze :: [Int] -> [Int]
    • Przykład użycia:
      ghci> pierwsze [100..110]
      [101,103,107,109]
      ghci> take 15 $ pierwsze [1..]
      [2,3,5,7,11,13,17,19,23,29,31,37,41,43]
  6. 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 nub). Dla przypomnienia:
    • Nagłówek funkcji:
      conajmniejn2 :: [Int] -> Int -> [Int]
    • Przykład użycia:
      ghci> conajmniejn2 [4,5,2,5,4,3,1,3,4] 2
      [5,3,4]
      ghci> conajmniejn2 [4,5,2,5,4,3,1,3,4] 4
      []
pl/dydaktyka/pp/haskell/lab-more-funcs.txt · ostatnio zmienione: 2018/05/28 18:31 przez kkutt
www.chimeric.de Valid CSS Driven by DokuWiki do yourself a favour and use a real browser - get firefox!! Recent changes RSS feed Valid XHTML 1.0