Programowanie funkcyjne · Programowanie funkcyjne w języku Haskell λ Stanisław Wasik Na...
Transcript of Programowanie funkcyjne · Programowanie funkcyjne w języku Haskell λ Stanisław Wasik Na...
Programowanie funkcyjnew języku Haskell
λStanisław Wasik
Na podstawie: St. Wasik, Programowanie funkcyjne w języku Haskell, Politechnika Poznańska, Poznań 2020
Rachunek lambda ● Model obliczeniowy (podstawa języka programowania):
○ Alonzo Church, 1930 → rachunek lambda → prog. funkcyjne (Haskell, OCaml, F#, Erlang, Scala)○ Alan Turing, 1936 → maszyna Turinga → prog. imperatywne (C/C++, C#, Python, Java)
● “Najmniejszy uniwersalny język programowania”:○ zmienna x○ abstrakcja λx.x+1○ aplikacja (λx.x+1)y
● Bazuje na konstruowaniu wyrażeń oraz poddawaniu ich redukcji.● Funkcje nie mają nazw i przyjmują zawsze tylko jeden argument.● Równoważny z maszyną Turinga pod tym względem,
○ że wszystkie algorytmy przedstawione na maszynie Turinga mogą być także zapisane w rachunku lambda oraz na odwrót.
Podstawowe zasady FP● Programowanie deklaratywne → co osiągnąć, nie jak to zrobić.
○ Brak instrukcji sterujących (for, while, switch). ○ Program oparty na rekurencji.
● Zmienne deklaratywne → brak zmiennego stanu.○ Zmienna jako skrót do wartości (nie do miejsca w pamięci):
■ x = 5■ x = 7 -- błąd: próba przypisania wartości 7 do wartości 5■ 5 = 7
● Do obliczenia wyniku używa się tylko argumentów funkcji i globalnych stałych:○ Czysta funkcja (funkcja matematyczna) → łatwiejsze testowanie, łatwiejsze prog. współbieżne.
● Ewaluacja wrażeń zamiast wykonywania instrukcji.○ Programista dostarcza definicje funkcji, nie definiuje jednak kolejności ich wartościowania.○ System wykonawczy wartościuje wybrane funkcje w celu uzyskania wyniku wyrażenia nadrzędnego.○ Nie jest istotne w jakim momencie maszyna dokona wartościowania funkcji (vide czyste funkcje).
Podstawowe zasady FP (2)● Funkcja jako element pierwszej kategorii ( jak obiekt w OOP).
○ Funkcja jest tak samo dobrą wartością jak liczba → można ją np. przypisać do zmiennej:■ uwaga dot. języka Haskell: funkcji używamy bez nawiasów → sqrt 25■ wyrażenie lambda: succ = \x -> x+1 (λx.x+1) succ 12 → 13
■ if condition then log10 15 else log2 15
■ (if condition then log10 else log2) 15
■ funkcja jako argument innej funkcji, tzw. funkcji wyższego rzędu (np. map succ [1,2,3])■ kompozycja funkcji - łącznie funkcji w większe funkcje■ f( g( h(x) ) ) = (f ∘ g ∘ h)(x) ■ (f . g . h) x
○ Spostrzeżenie:■ najważniejszy element w FP: funkcja → sposób przetwarzania danych,■ najważniejszy element w OOP: obiekt → sposób przechowywania danych.
Podstawowe zasady FP (3)● W przypadku programów imperatywnych zakłada się, że wykonanie instrukcji w podanym
przez programistę porządku jest kluczowe do osiągnięcia wyniku.○ Stąd mówimy, że definiujemy strumień sterowania.
● Program funkcyjny jest złożeniem wielu funkcji, programista łączy kolejne bloki przetwarzające dane.○ Wynik jednej funkcji staje się argumentem innej.○ Stąd mówimy, że definiujemy strumień danych, które podlegają przekształceniom.
● Luźna zasada:○ Język funkcyjny stosujemy, gdy dane o małej różnorodności podlegają wielu przekształceniom.○ Język imperatywny stosujemy, gdy wykonujemy mało operacji na różnorodnym zbiorze danych.
● Ciekawostka:○ Klasa w języku Haskell agreguje funkcje (vide język funkcyjny), nie zaś zmienne.○ Instancją klasy jest typ danych, który implementuje zdefiniowane w danej klasie funkcje.
Przykładowe funkcjefib :: Int -> Int
fib 0 = 0
fib 1 = 1
fib n = fib (n-1) + fib (n-2)
add :: Int -> Int -> Int
add a b = a + b
last :: [Int] -> Int
last [h] = h -- [h] jest równoważne z [H]last (h:t) = last t -- (h:t) jest równoważne z [H|T] -- zwyczajowo w języku Haskell zamiast (h:t) używamy oznaczenia (x:xs)
● Jedna z największych zalet programowania deklaratywnego.● Pozwala na przejrzysty wybór sposobu prowadzenia obliczeń w zależności od wartości
argumentów funkcji.○ System wykonawczy przegląda kolejne deklaracje danej funkcji i wybiera pierwszą pasującą.○ Deklaracja funkcji jest uznana za pasującą, gdy podane wartości argumentów zgadzają się z tymi
zapisanymi w deklaracji lub gdy dla podanego argumentu w deklaracji występuje wolna zmienna.○ factorial 0 = 1
○ factorial n = n * factorial (n-1)
● Umożliwia dekonstrukcję wartości złożonych:○ Inaczej: umożliwia związanie poszczególnych wartości składowych wartości złożonej ze zmiennymi.○ [x,y,z] = [1,2,3] → x = 1 y = 2 z = 3
○ (h:t) = [1,2,3] → h = 1 t = [2,3] ponieważ 1:[2,3] → [1,2,3]
○ (a:b:c) = [1,2,3] → a = 1 b = 2 c = [3]
● Zaleta: w programach deklaratywnych nie ma potrzeby używania wyrażeń if.
Dopasowanie do wzorca
Implikacja
(==>) :: Bool -> Bool -> Bool
True ==> True = True
True ==> False = False
False ==> True = True
False ==> False = True
Dopasowanie do wzorca (2)
(==>) :: Bool -> Bool -> Bool
True ==> False = False
_ ==> _ = True
head :: [Int] -> Int
head (h:_) = h
● Jest mechanizmem wyznaczania wartości dopiero w momencie, gdy wartość ta jest wymagana do kontynuacji obliczeń.
● By uzyskać wynik funkcji dokonywana jest analiza wyrażenia. Następnie wartościowane są tylko te funkcje, których wyniki są konieczne do obliczenia wartości końcowej.
nthEvenNatural n = evenNatural !! (n-1)
where infinite = [1..]
evenNatural = [2,4..]
● Mechanizm ten kontroluje obliczenia w taki sposób, by wartość danego wyrażenia była obliczona tylko i wyłącznie raz.
● Pozwala na tworzenie nieskończonych struktur danych.○ Zmienna przechowująca taką strukturę zawiera funkcję (tzw. obietnicę obliczenia wartości), na
podstawie której możliwe jest wyznaczenie zawartości struktury.
Leniwe wartościowanie
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
fibs = [0, 1,···]
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1,···] ← tail fibs
fibs = [0, 1, + ← fibs
[0, 1,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1,···] ← tail fibs
fibs = [0, 1, 1, ← fibs
[0, 1, 1,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1,···] ← tail fibs
fibs = [0, 1, 1, + ← fibs
[0, 1, 1,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1, 2,···] ← tail fibs
fibs = [0, 1, 1, 2, ← fibs
[0, 1, 1, 2,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1, 2,···] ← tail fibs
fibs = [0, 1, 1, 2, + ← fibs
[0, 1, 1, 2,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1, 2, 3,···] ← tail fibs
fibs = [0, 1, 1, 2, 3, ← fibs
[0, 1, 1, 2, 3,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1, 2, 3,···] ← tail fibs
fibs = [0, 1, 1, 2, 3, + ← fibs
[0, 1, 1, 2, 3,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1, 2, 3, 5,···] ← tail fibs
fibs = [0, 1, 1, 2, 3, 5, ← fibs
[0, 1, 1, 2, 3, 5,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1, 2, 3, 5,···] ← tail fibs
fibs = [0, 1, 1, 2, 3, 5, ← fibs
[0, 1, 1, 2, 3, 5,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
[1, 1, 2, 3, 5,···] ← tail fibs
fibs = [0, 1, 1, 2, 3, 5,···] ← fibs
[0, 1, 1, 2, 3, 5,···] ← fibs
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy
fibs = [0, 1, 1, 2, 3, 5,···]
Leniwe wartościowanie (2)
Inna, iteracyjna definicja ciągu Fibonacciego:
fibs :: [Int]
fibs = 0 : 1 : zipWith (+) (tail fibs) fibs
zipWith (++) [“a”, “b”, “c”] [“1”, “2”, “3”] → [“a1”, “b2”, “c3”]
>>> fibs !! 5 -- (!!) to operator indeksowania listy5
fibs = [0, 1, 1, 2, 3, 5,···]
Leniwe wartościowanie (2)
Currying● Funkcje przyjmują zawsze tylko jeden argument.● Currying to proces przekształcania funkcji wieloargumentowej w złożenie funkcji
jednoargumentowych, w którym każda z funkcji przyjmuje jeden argument oraz zwraca funkcję przyjmującą kolejny argument.
● Pochodzi od nazwiska Haskella Curry’ego.
modExp :: Int -> Int -> Int -> Int
modExp a b c = a ^ b `mod` c
modExp :: Int -> Int -> Int -> Int
modExp = \a b c -> a ^ b `mod` c
modExp :: Int -> Int -> Int -> Int
modExp = \a -> \b -> \c -> a ^ b `mod` c
Currying (2)modExp :: Int -> (Int -> (Int -> Int))
modExp = \a -> (\b -> (\c -> a ^ b `mod` c))
x = modExp 12 y = modExp 12 4
map (\x -> max 15 x) [5,10,15,20,25] ⟺ map (max 15) [5,10,15,20,25]
→ [15,15,15,20,25]
toLower :: String -> String
toLower string = map lower string ⟺ toLower = map lower
Currying w Pythonie:curried_mod_exp = lambda a: lambda b: lambda c: a ** b % c
>>> curried_mod_exp(4,3,6)
Currying (2)modExp :: Int -> (Int -> (Int -> Int))
modExp = \a -> (\b -> (\c -> a ^ b `mod` c))
x = modExp 12 y = modExp 12 4
map (\x -> max 15 x) [5,10,15,20,25] ⟺ map (max 15) [5,10,15,20,25]
→ [15,15,15,20,25]
toLower :: String -> String
toLower string = map lower string ⟺ toLower = map lower
Currying w Pythonie:curried_mod_exp = lambda a: lambda b: lambda c: a ** b % c
>>> curried_mod_exp(4,3,6) curried_mod_exp(4)(3)(6)
Monady● Monada (ang. monad) jest rodzajem kontenera przechowującego wartości lub funkcje. ● Pozwala na budowanie sekwencji obliczeń poprzez łączenie bloków (funkcji).
○ Łączenie przebiega z zastosowaniem odpowiedniej strategii.● Każdy monadyczny typ danych definiuje sposób łączenia bloków ze sobą zwalniając
programistę z konieczności ręcznej obsługi kontekstu operacji.● Poszczególne bloki (funkcje) podlegające łączeniu przyjmują argument prosty (np. Int), zaś
zwracają wynik umieszczony w monadzie (np. Maybe Int).○ data Maybe a = Just a | Nothing
■ C++ → new, nullptr ■ Scala → Option (Some, None)
○ Przykładowa funkcja zwracająca indeks podanej liczby na liście będzie mieć sygnaturę indexOf :: Int -> [Int] -> Maybe Int
○ indexOf 12 [10,11,12,13] → Just 2
○ indexOf 12 [13,14,15,16] → Nothing
Monady - przykładPython
def f(x):
if x % 2 == 0: return x + 1
return None
def g(x):
if x % 3 == 0: return x + 2
return None
def h(x):
if x % 5 == 0: return x + 3
return None
def test(): return h(g(f(12)))
Haskell
f x
| x `mod` 2 == 0 = Just (x + 1)
| otherwise = Nothing
g x
| x `mod` 3 == 0 = Just (x + 2)
| otherwise = Nothing
h x
| x `mod` 5 == 0 = Just (x + 3)
| otherwise = Nothing
test = (h . g . f) 12
Monady - przykładPython
def f(x):
if x % 2 == 0: return x + 1
return None
def g(x):
if x % 3 == 0: return x + 2
return None
def h(x):
if x % 5 == 0: return x + 3
return None
def test(): return h(g(f(12)))
Haskell
f x
| x `mod` 2 == 0 = Just (x + 1)
| otherwise = Nothing
g x
| x `mod` 3 == 0 = Just (x + 2)
| otherwise = Nothing
h x
| x `mod` 5 == 0 = Just (x + 3)
| otherwise = Nothing
test = (h . g . f) 12
Monady - przykład (2)Python
def test():
x = f(12)
if x == None:
return None
else:
y = g(x)
if y == None:
return None
else:
z = h(y)
if z == None:
return None
else:
return z
Haskell
test =
case f 12 of
Nothing -> Nothing
Just x ->
case g x of
Nothing -> Nothing
Just y ->
case h y of
Nothing -> Nothing
Just z -> Just z
Monady - przykład (3)Funkcja monadyczna >>= (bind)
a >>= f =
case a of
Nothing -> Nothing
Just x -> f x
test = Just 12 >>= f >>= g >>= h
albo krócej:
test = f 12 >>= g >>= h
Lista jako monada→ funkcja >>= odpowiada flatMap w języku Scala
f x
| x > 1 = [-x, x]
| otherwise = []
map f [1,2,3]
→ [[], [-2, 2], [-3, 3]]
[1,2,3] >>= f
→ [-2, 2, -3, 3]