Георги Наков, nakov.gl at gmail com
Марин Маринов, marinov.ms+tues at gmail com
Технологично училище "Електронни Системи"
16 Ноември 2016г.
Проблем (голям):
Досега видяхме, че ако ни се налага да работим с различни видове списъци, трябва да пишем функциите по няколко пъти с различни сигнатури, но еднакви дефиниции.
lengthInt :: [Int] -> Int
lengthInt [] = 0
lengthInt (_:xs) = 1 + lengthInt xs
lengthString :: String -> Int
lengthString [] = 0
lengthString (_:xs) = 1 + lengthString xs
Това е ужасно!
Haskell предоставя решение:
Когато не ни интересува конкретния тип може вместо него да използваме т. нар типова променлива.
length :: [a] -> Int
length [] = 0
length (_:xs) = 1 + length xs
Тук a
значи "да е който и да е тип".
Конкретните типове започват с главна буква, типовите променливи започват с малка буква (и по конвенция често са с по една буква).
Функциите с поне една типова променлива се наричат полиморфични.
Вградените функции за манипулация на списъци в Haskell работят с списъци от какъвто и да е тип! Да проверим сигнатурите:
> head [1, 2, 3]
1
> head "Hi"
'H'
ghci> :t head
head :: [a] -> a
ghci> :t reverse
reverse :: [a] -> [a]
ghci> :t (++)
(++) :: [a] -> [a] -> [a]
Не може да предполагаме нищо за аргументите или променливите, които са от тип a
. Не може да извършваме операции с тях.
bad :: a -> Int
bad x = x + 1
-- грешка
-- Couldn't match expected type ‘Int’
-- with actual type ‘a’
В рамките на едно извикване на полиморфичните функции, a
заема конкретен тип, който не се сменя по време на изпълнението на функцията. a
има само един конкретен тип в рамките на извикването (тоест не може при първия аргумент да е Char
, вторият - Int
и тн).
ghci> :t (++)
(++) :: [a] -> [a] -> [a]
ghci> :t ("Hi" ++ " all")
"Hi" ++ " all" :: [Char]
В някои случаи искаме да използваме полиморфична функция, но и някакво свойство на a
.
elem :: a -> [a] -> Bool
elem _ [] = False
elem e (x:xs)
| x == e = True
| otherwise = elem e xs
За съжаление това е невалидна функция в Haskell, защото сме допуснали, че можем да сравним e
с елемент от списъка x
.
x == e
- Невалидно, защото извършваме операции с променливи от тип a
.
Решение: Може да кажем, че a
трябва да поддържа сравнение за равенство с (Eq a) =>
пред сигнатурата.
elem :: (Eq a) => a -> [a] -> Bool
(Eq a) =>
наричаме класови ограничение (class constraint).
Haskell ни дава много готови класови ограничения.
Eq a
- може да сравняваме с==
и/=
equals :: (Eq a) => a -> a -> Bool
equals x y = x == y
> equals 3 3
True
> equals "Hi" "hi"
False
Show a
- типът може да се конвертира към низ сshow
toString :: (Show a) => a -> String
toString x = show x
> toString 4
"4"
> toString [42,0]
"[42,0]"
Вградените стандартни типове (числа, низ, симвoли) поддържат и Eq a
и Show a
.
Как да "полиморфизираме" дадена функция, ако сметнем, че е подходящо?
- Заменете конкретния тип на удачен аргумент с
а
(b
/c
/d
). - Компилирайте. Ако няма грешка, готово!
- Ако има грешка, добавете
(Eq a) =>
и компилирайте пак. - Ако има грешка отново, върнете конкретния тип.
- Преминете на следващия удачен аргумент.
Въпреки, че полиморфичните функции повишават нивото на абстракция, те често правят нещата по-очевидни и ясни. Пример:
Какво може да кажем за func :: Int -> String -> Int
? По колко начина може да я имплементираме?
Не знаем нищо - може да връщаме дължината на низа, може дължината на низа на квардрат, може да връщаме 42, може да взимаме първите n символа и да умножаваме ASCII кодовете
Какво може да кажем за func :: a -> b -> а
? По колко начина може да я имплементираме?
Досещаме се за очевидна имплементация :
func x y = y
- функция, която винаги връща втория си аргумент.