Георги Наков, 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"
FalseShow 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 - функция, която винаги връща втория си аргумент.