Cvičení jsme začali rozvičkou, psali jsme ve dvojicích následující funkce:
uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry = _
curry :: ((a, b) -> c) -> a -> b -> c
curry = _
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith = _
Nejprve se o implementaci pokuste sami, níže jsem rozepsal postup, který jsme použili na cvičeních:
- Všimneme si, že tahle funkce má dva argumenty, tedy je musíme převzít:
uncurry = \f -> \ab -> _
f
má typ a -> b -> c
ab
má typ (a, b)
Na to přijdeme buď tím, že se podíváme na sekci "Relevant bindings" v "Typed hole" erroru,
nebo najetím myši v IDE. (Nebo rozmyšlením dle typu funkce...)
- Ze cvičení víme, že
foo = \x -> E
je to samé jakofoo x = E
(syntaktický cukr):
uncurry f ab = _
- Nyní máme vytvořit hodnotu typu
c
. Jediný způsob jak ji vytvořit je pomocí funkcef
, musíme ji tedy použít:
uncurry f ab = f firstArg secondArg
where
firstArg = _
secondArg = _
firstArg
musí být něco typua
asecondArg
má být něco typub
. Jediný způsob jak něco takového získat je zab
, který má typ(a, b)
, tj. dvojice typůa
ab
. Musíme tedy rozbalitab
pomocí pattern matchování:
uncurry f (x, y) = f firstArg secondArg
where
firstArg = _
secondArg = _
x
je typu a
, y
je typu b
- Nyní
firstArg
má být něco typua
a takovou věc máme --x
:
uncurry f (x, y) = f firstArg secondArg
where
firstArg = x
secondArg = _
- Podobně pro
secondArg
ay
:
uncurry f (x, y) = f firstArg secondArg
where
firstArg = x
secondArg = y
- Víme, že v Haskellu můžeme cokoliv inline-ovat, takže inlinujeme
firstArg
asecondArg
(hodily se nám při vytváření, ale teď už nejsou k ničemu):
uncurry f (x, y) = f x y
A máme hotovo! :)
Analogicky k předchozí :)
curry f x y = f (x, y)
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith = _
- Nejprve převezmeme argumenty
f :: (a -> b -> c)
,xs :: [a]
,ys :: [b]
.
zipWith f xs ys = _
- Když najedeme na typed hole (tj. na
_
), Haskell říká, že tam chce dostat něco typu[c]
. Mohli bychom vrátit něco jako prázdný seznam, ale to není úplně košér (rozumné funkce nezahazují svůj vstup).
Všimneme si, že když bychom nějak dostali jednu věc typu a
a jednu věc typu b
,
tak pomocí funkce f
vyrobíme jednu věc typu c
.
- Použijeme pattern matching na
xs
:
zipWith f [] ys = _
zipWith f (x:xs) ys = _
Co vůbec můžeme vrátit když je první seznam prázdný? Všimněme si, že jediná rozumná věc je prázdný seznam!
zipWith f [] ys = []
zipWith f (x:xs) ys = _
- Použijeme pattern matching na
ys
v nedořešeném (druhém) případě:
zipWith f [] ys = []
zipWith f (x:xs) [] = _
zipWith f (x:xs) (y:ys) = _
Opět, dle stejného argumentu jako výše nemáme jak vyrobit cokoliv typu c
.
Takže jediný seznam věcí s typem c
(tj. [c]
) je prázdný seznam.
zipWith f [] ys = []
zipWith f (x:xs) [] = []
zipWith f (xs:xs) (y:ys) = _
- Trochu po sobě uklidíme a zapodtržítkujeme nepoužívané argumenty:
zipWith _ [] _ = []
zipWith _ _ [] = []
zipWith f (x:xs) (y:ys) = _
- Zbývá nám vyřešit jediný případ. Zase bychom mohli vrátit prázdný seznam, ale v tu chvíli bychom nepoužili žádný argument v celé funkci, což asi není to co jsme chtěli.
Máme ale f :: (a -> b -> c)
, x :: a
, xs :: [a]
, y :: b
, ys :: [b]
.
Pomocí f a b
jsme schopni vytvořit něco typu c
a označíme to jako z
.
zipWith f (x:xs) (y:ys) = _
where
z = f x y
- Jak to využijeme? Mohli bychom typed hole (tj.
_
) nahradit něčím jako[ z ]
, ale to asi není úplně fajn, protože v tu chvíli jsme zahodili celý zbytek vstupních seznamů (xs
ays
).
Vrátíme tedy z : zs
, kde zs :: [c]
ještě nevíme...
zipWith f (x:xs) (y:ys) = z : zs
where
z = f x y
zs = _
- Nyní chceme vytvořit
zs
typu[c]
. Jak to uděláme? Mohli bychom zase pattern matchovat naxs
ays
a aplikovat na jeden, ale pak bychom měli nějak hodně případů.
Všimneme si ale, že tohle je skvělé místo kde použít rekurzi!
zs = zipWith f xs ys
- Zase slušně inlinujeme pomocné definice (jsou dostatečně jednoduché):
zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys
Dohromady tedy dostáváme:
zipWith :: (a -> b -> c) -> [a] -> [b] -> [c]
zipWith _ [] _ = []
zipWith _ _ [] = []
zipWith f (x:xs) (y:ys) = f x y : zipWith f xs ys
Tahle funkce se chová jako map
, ale bere dva seznamy a spojuje je po prvcích :)
- Používejte Typed Holes -- pokud nevíte co chcete napsat, plácněte tam
_
a nechte si od GHC/IDE pomoct! :) - Řiďte se pomocí typů, je to taková logická skládačka, ve které je docela těžké selhat :D
- Pište i své funkce co nejobecněji to jde. Všimněte si, že jsme se často spoléhali na argumenty jako "je jen jeden způsob jak vytvořit něco typu
c
". Takové argumenty už ale neplatí proInt
. Například funkce typua -> a
je jen jedna, ale funkcíInt -> Int
je(2^32)^(2^32)
, což je fakt hodně. - Naučte se používat Hoogle -- Haskellový vyhledávač, který umí najít funkci
nejen dle jména, ale i dle typu: https://hoogle.haskell.org/
Je fakt dobrý! Když do něj hodíte něco jako
(Int -> String) -> [Int] -> [String]
, tak pořád pochopí že jde omap
! - Naučte se používat své IDE! Pokud používate Haskell Language Server (což byste měli), tak se s ním naučte pracovat. Konkrétně když jste nad Typed Hole, tak můžete zavolat Code Action (kliknout na 💡).
Akce umí věci jako:
- "introduce lambda", což vyrobí nové argumenty
- "case split on <proměnná>", což vyrobí case-of
- "refine hole", které se snaží upřesnit výsledek
- a dokonce "attempt to fill hole", které vám občas napíše celou funkci samo!
- Jak vyvíjet pomocí typed holes [anglicky] -- https://reasonablypolymorphic.com/blog/typeholes/