Джоэла Каасинена (Nitor) и Джона Лонга (Университет Хельсинки)
Эта лекция повторяет основные темы первой части курса: типы, значения, сопоставление с образцом, функции и рекурсию.
Помните примитивные типы Хаскелла? Вот они:
| Примеры значений | Тип | Описание |
|---|---|---|
True, False |
Bool |
Логические значения |
0, 1, 20, -37, … |
Int |
Целые числа |
'A', 'a', '!', … |
Char |
Символы |
"", "abcd", … |
String |
Строки, которые в действительности являются списками символов [Char] |
0.0, -3.2, 12.3, … |
Double |
Числа с плавающей точкой |
() |
() |
Единичный тип (unit type) с единственным значением |
Можно объединить эти примитивные типы различными способами, чтобы сформировать более сложные типы. Типы функций, типы кортежей и типы списков являются примерами типов, которые объединяют другие типы.
| Примеры значений | Тип | Описание |
|---|---|---|
(1,2), (True,'c'), … |
(a, b) |
Пара из значений типов a и b |
(1,2,3), (1,2,'c'), … |
(a, b, c) |
Тройка значений (типов a, b и c) |
[], [1,2,3,4], … |
[a] |
Список значений типа a |
not, reverse, \x -> 1, \x -> x, … |
a -> b |
Функция от типа a до типа b |
Есть еще один мощный механизм для создания большего количества типов: Алгебраические типы данных (ADTs). Некоторые примеры включают:
-- Enumeration types
data Bool = True | False
data Color = Red | Green | Blue
-- Record types that contain fields
data Vector2d = MakeVector Double Double
data Person = Person Int String
-- Parameterized types. Note the type parameter `a`
data PairOf a = TwoValues a a
-- Recursive types
data IntList = Empty | Node Int IntList
-- Complex types which combine many of these features
data Maybe a = Nothing | Just a
data Either a b = Left a | Right b
data List a = Nil | Cons a (List a) -- This is equivalent to the built-in [a] type
data Tree a = Leaf a | Node a (Tree a) (Tree a)
data MultiTree a = MultiTree a [MultiTree a] -- Note the listЦенности этих типов включают:
| Значения | Тип |
|---|---|
True, False |
Bool |
Red, Green, Blue |
Color |
MakeVector 1.5 3.2 |
Vector2d |
Person 13 "Bob" |
Person |
TwoValues 1 3 |
PairOf Int |
Empty, Node 3 (Node 4 Empty) |
IntList |
Nothing, Just 3, Just 4, … |
Maybe Int |
Nothing, Just 'c', Just 'd', … |
Maybe Char |
Left "foo", Right 13, … |
Either String Int |
Nil, Cons True Nil, Cons True (Cons False Nil) ... |
List Bool |
Leaf 7, Node 1 (Leaf 0) (Leaf 2), … |
Tree Int |
MultiTree 'a' [MultiTree 'b' [], MultiTree 'c' []]], ... |
MultiTree Char |
Вы можете комбинировать параметризованные типы сложными способами, например, с чем-то вроде Either [String->String] (Maybe String, Int).
Названия конкретных типов начинаются с заглавных букв. Нижние буквы используются для переменных type, которые указывают на параметрический полиморфизм: функции и значения, которые могут иметь несколько типов. Вот несколько примеров типов полиморфных функций:
[a] -> [a] -- function from list of any type, to list of the same type
[a] -> a -- function from list of any type, to the element type
(a,b) -> [a] -- function from tuple to listЛитералы списка могут быть написаны с использованием знакомого синтаксиса [x,y,z]. Тем не менее, эта нотация является лишь сокращением, поскольку списки фактически составлены из списков конструкторов [] и (:). Эти конструкторы также используются при сопоставлении списков шаблонов. Вот некоторые примеры списков:
| Сокращения | Полный список | Тип |
|---|---|---|
[1,2,3] |
1:2:3:[] |
[Int] |
[[1],[2],[3]] |
(1:[]):(2:[]):(3:[]):[] |
[[Int]] |
"foo" |
'f':'o':'o':[] |
[Char], также известный как String |
Существует также синтаксис диапазона для списков:
| диапазон | Результат |
|---|---|
['a' .. 'z'] |
"abcdefghijklmnopqrstuvwxyz" |
[0 .. 9] |
[0,1,2,3,4,5,6,7,8,9] |
[0, 5 .. 25] |
[0,5,10,15,20,25] |
[x .. y] |
Все от x до y |
[9 .. 3] |
[] |
[y, y-1 .. x] |
от всего, что y то x в порядке убывания |
[9,8 .. 3] |
[9,8,7,6,5,4,3] |
List Comprehensions - еще один мощный способ создания списков:
| Понимание | Результат |
|---|---|
[x^3 | x <- [1..3]] |
[1,8,27] |
[x^2 + y^2 | x <- [1..3], y <- [1..2]] |
[2,5,5,8,10,13] |
[y | x <- [1..10], let y = x^2, even x, y<50] |
[4,16,36] |
[c | c <- "Hello, World!", elem c ['a'..'z']] |
"elloorld" |
В общем случае [f x | x <- xs, p x] эквивалентно map f (filter p xs). Аналогично, [y | x <- xs, let y = f x] эквивалентно [f x | x <- xs]. В генераторах списков можно сочетать <-, let и выражение результата [f x | ...].
Еще одно замечание по синтаксису. Напомним, что (:) ассоциируется справа, например, True:False:[] совпадает с True:(False:[]). (На самом деле, (True:False):[] даже не является списком, потому что True:False пытается добавить True перед False, который не является списком.)
Основной формой определения функции является:
functionName :: argumentType -> returnType
functionName argument = returnValueНапример:
repeatString :: String -> String
repeatString s = s ++ sФункции, принимающие несколько аргументов, определяются аналогичным образом. Обратите внимание, как выглядит тип функции с несколькими аргументами.
surroundString :: String -> String -> String
surroundString around s = around ++ s ++ aroundФункции могут быть полиморфными, могут принимать множество аргументов и даже могут принимать функции в качестве аргументов. Вот еще несколько примеров:
id :: a -> a
id x = x
const :: a -> b -> a
const x y = x
flip :: (a -> b -> c) -> b -> a -> c
flip f x y = f y xБолее сложные функции могут быть определены с помощью pattern matching. Мы можем сопоставить шаблоны на конструкторах алгебраических типов данных, таких как Maybe, а также списки конструкторов [] и (:).
swap :: (a,b) -> (b,a)
swap (x,y) = (y,x)
maybe :: b -> (a -> b) -> Maybe a -> b
maybe def _ Nothing = def
maybe _ f (Just x) = f x
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:_) = Just xЕще больше изощренности можно достичь с помощью guards. Охрана позволяет определить функцию в каждом конкретном случае на основе тестов типа Bool. Охрана полезна в ситуациях, когда сопоставление шаблонов не может быть использовано. Конечно, защитные выражения (guards) также могут сочетаться с сопоставлением шаблонов:
myAbs :: Int -> Int
myAbs x
| x < 0 = -x
| otherwise = x
safeDiv :: Double -> Double -> Maybe Double
safeDiv x y
| y == 0 = Nothing
| otherwise = Just (x / y)
buy :: String -> Double -> String
buy "Banana" money
| money < 3.2 = "You don't have enough money for a banana"
| otherwise = "You bought a banana"
buy product _ = "No such product: " ++ productВыражения Case позволяют нам сопоставлять шаблоны внутри функций. Они полезны в ситуациях, когда результат одной функции зависит от результата другой, и мы хотим сопоставить шаблон с выходом другой функции:
divDefault :: Double -> Double -> Double -> Double
divDefault x y def = case safeDiv x y of
Nothing -> def
Just w -> wВыражения позволяют локальные определения где пункты работают аналогично let С. Например:
circleArea :: Double -> Double
circleArea r = let pi = 3.1415926
square x = x * x
in pi * square r
circleArea' :: Double -> Double
circleArea' r = pi * square r
where pi = 3.1415926
square x = x * xLambda-выражения являются еще одним иногда полезным синтаксисом для определения функций. Ламбда-выражения представляют анонимные (неназванные) функции. Они могут использоваться для определения локальных функций, которые обычно используются только один раз.
incrementAll :: [Int] -> [Int]
incrementAll xs = map (\x -> x + 1) xsОтметим, что f x = y - это то же самое, что и f = \x -> y.
Наконец, бинарные операторы имеют sections. Секции частично применяются операторами. Раздел оператора получается путем написания оператора и одного из его аргументов в скобках. Например, (*2) умножает аргумент 2 справа, например, (*2) 5 ==> 5 * 2. Дробное число (например, Double) можно перевернуть с помощью раздела (1/), например (1/) 2 ==> 0.5.
incrementAll' :: [Int] -> [Int]
incrementAll' xs = map (+1) xsHaskell - это функциональный язык программирования, который означает, что функции могут быть переданы в виде аргументов и возвращены из функций. Как парадигма программирования, функциональное программирование направлено на создание программ путем объединения простых функций вместе, чтобы сформировать все большие и большие.
Наиболее часто представленным примером функционального программирования является манипуляция функциональным списком. функции высшего порядка (функции, которые принимают функции в качестве аргументов) map и filter Вот один пример из части 1:
-- a predicate that checks if a string is a palindrome
palindrome :: String -> Bool
palindrome str = str == reverse str
-- palindromes n takes all numbers from 1 to n, converts them to
-- strings using show, and keeps only palindromes
palindromes :: Int -> [String]
palindromes n = filter palindrome (map show [1..n])palindromes 150
==> ["1","2","3","4","5","6","7","8","9",
"11","22","33","44","55","66","77","88","99",
"101","111","121","131","141"]В части 1 мы встретили и другие приёмы функционального программирования, например частичное применение (partial application):
map (take 3) [[1,2,3,4,5],[6,7,8,9,0]]
==> [[1,2,3],[6,7,8]]Кроме того, функциональная композиция:
(map reverse . filter (/="Smith")) ["Jones","Smith","White"]
==> ["senoJ","etihW"]
map (negate . sum) [[1,2,3],[5]]
==> [-6,-5]И, наконец, folds:
foldr (*) 1 [2,3,4] ==> 24
foldr max 0 [1,3,7] ==> 7
foldr (++) "" ["abc","de","f"] ==> "abcdef"Для реализации функции, использующей повторение в Haskell, необходима рекурсия. Haskell не имеет циклов, как другие языки программирования. Вот несколько простых рекурсивных функций в Haskell:
repeatString :: Int -> String -> String
repeatString 0 s = ""
repeatString n s = s ++ repeatString (n-1) s
times :: Int -> Int -> Int
times 0 n = 0
times 1 n = n
times m n = n + times (m - 1) n
safeLast :: [a] -> Maybe a
safeLast [] = Nothing
safeLast [x] = Just x
safeLast (x:xs) = safeLast xsДля того, чтобы составить список, вам часто нужна рекурсия. Вот примеры реализации map и filter в качестве примеров обработки рекурсивных списков:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
filter :: (a -> Bool) -> [a] -> [a]
filter _ [] = []
filter pred (x:xs)
| pred x = x : filter pred xs
| otherwise = filter pred xsИногда требуется рекурсивная функция помощника, если вам нужно отслеживать несколько фрагментов данных.
sumNumbers :: [Int] -> Int
sumNumbers xs = go 0 xs
where go sum [] = sum
go sum (x:xs) = go (sum+x) xsВот последний пример использования охранников, сопоставления шаблонов, функции помощника и рекурсии:
-- split a string into pieces at the given character
mySplit :: Char -> String -> [String]
mySplit c xs = helper [] xs
where helper piece [] = [piece]
helper piece (y:ys)
| c == y = piece : helper [] ys
| otherwise = helper (piece++[y]) ysmySplit '-' "a-bcd-ef" ==> ["a","bcd","ef"]Следующие функции параметрически полиморфны (parametrically polymorphic):
id :: a -> a
id x = x
head :: [a] -> a
head (x:_) = x
fst :: (a,b) -> a
fst (x,y) = xПараметрически полиморфные функции всегда работают одинаково, независимо от того, с какими типами мы работаем. Это означает, что мы не можем определить специальную реализацию id только для типа Int или fst для типа (Bool, String).
Напротив, ad hoc polymorphism позволяет различным типам иметь разные реализации одной и той же функции. Специальный полиморфизм в Haskell может быть достигнут путем определения класса type и последующего объявления instances этого класса для различных типов. Специальный полиморфизм является удобным способом выражения общего набора операций, даже когда реализация операций зависит от типа, на который они действуют.
Функции, использующие специальный полиморфизм, имеют ограничение -класса в своих типах. Вот несколько примеров:
negate :: Num a => a -> a
(==) :: Eq a => a -> a -> Bool
sort :: Ord a => [a] -> [a]Тип Num a => a -> a означает: для любого типа X, который является членом класса Num, эта функция имеет тип X -> X. Другими словами, мы можем вызвать negate на любом типе номера, но не на других типах:
Prelude> negate 1
-1
Prelude> negate 1.0
-1.0
Prelude> negate True
<interactive>:3:1: error:
• No instance for (Num Bool) arising from a use of ‘negate’Вот краткое изложение некоторых полезных классов типов из стандартной библиотеки.
Eq - для сравнения равенства. Содержит оператора ==Ord предназначен для сравнения заказов. Он содержит упорядоченные операторы сравнения, такие как < и =>, и функции, такие как max и min.Num Для всех типов чисел. Содержит +, -, * и negate.Integral предназначен для целых типов номеров. В частности, он содержит целое подразделение div.Fractional для типов номеров, поддерживающих подразделение, /Show содержит функцию show :: Show a => a -> String, которая преобразует значения в строки.Read содержит функцию read :: Read a => String -> a, обратную show.Иногда требуется несколько классовых ограничений. Например, здесь:
sumTwoSmallest :: (Num a, Ord a) => [a] -> a
sumTwoSmallest xs = let (a:b:_) = sort xs
in a+bТеперь, когда мы увидели некоторые классы и типы, давайте посмотрим на синтаксис объявления классов и экземпляров. Вот два определения классов:
class Sized a where
empty :: a -- a thing with size 0
size :: a -> Int
class Eq a where
(==) :: a -> a -> BoolРассмотрим следующие структуры данных:
data Numbers = None | One Int | Two Int Int
data IntList = Nil | ListNode Int IntList
data Tree a = Leaf | Node a (Tree a) (Tree a)Все они имеют размеры, которые мы можем посчитать, но нам нужно выполнить операцию по-другому:
instance Sized Numbers where
empty = None
size None = 0
size (One _) = 1
size (Two _ _) = 2
instance Sized IntList where
empty = Nil
size Nil = 0
size (ListNode _ list) = 1 + size list
instance Sized (Tree a) where
empty = Leaf
size Leaf = 0
size (Node _ left right) = 1 + size left + size rightМы также можем легко объявить экземпляры Eq для Numbers и IntList:
instance Eq Numbers where
None == None = True
(One x) == (One y) = x==y
(Two x y) == (Two z w) = x==z && y==w
_ == _ = False -- to handle cases like None == One 1
instance Eq IntList where
Nil == Nil = True
(ListNode x xs) == (ListNode y ys) = x == y && xs == ys
_ == _ = FalseОднако, поскольку тип данных Tree параметризован по типу элемента a, нам нужен экземпляр Eq a, чтобы иметь экземпляр Eq (Tree a). Это достигается путем добавления классового ограничения к декларации экземпляра. Это называется иерархия instance..
instance Eq a => Eq (Tree a) where
Leaf == Leaf = True
(Node x l r) == (Node x' l' r') = x == x' && l == l' && r == r'
_ == _ = FalseНекоторые классы стандартных типов, в частности Show, Read, Eq и Ord, могут быть derived, то есть вы можете попросить компилятор генерировать автоматические экземпляры для вас. Например, мы могли бы получить все эти классы для нашего более раннего примера Numbers.
data Numbers = None | One Int | Two Int Int
deriving (Show, Read, Eq, Ord)None == One 1 ==> False
Two 1 2 == Two 1 2 ==> True
None < Two 1 2 ==> True
Two 1 3 < Two 1 2 ==> False
show (Two 1 3) ==> "Two 1 3"Что такое тип ('c',not)
[Char][Bool](Char,Bool -> Bool)(Char,Bool)Что такое тип ['c',not]
[Char][Bool](Char,Bool -> Bool)(Char,Bool)Какое из них является значением следующего типа?
data T = X Int | Y String String | Z TX "foo"Y "foo"Z (X 1)X (Z 1)Каков тип этой функции?
f (_:Just x:_) = x
f _ = FalseMaybe a -> a[Maybe a] -> a[Maybe a] -> Bool[Maybe Bool] -> BoolКаков тип этой функции?
f x y = x-y == 0(Num a, Eq a) => a -> a -> BoolNum a => a -> a -> BoolEq a => a -> a -> Boola -> a -> BoolКакой из следующих типов может быть у x, чтобы x (&&) y не было ошибкой типа?
BoolBool -> Bool -> Bool(Bool -> Bool -> Bool) -> Bool -> BoolКратко повторим порядок работы над упражнениями. Он такой же, как в первой части курса.
exercises/stack build, чтобы загрузить зависимостиSet9a.hsstack runhaskell Set9aTest.hsЧистота и лень упоминались как ключевые черты Хаскелла в начале части 1. Давайте рассмотрим их поближе.
Haskell — чистый (pure) функциональный язык. Это означает, что значение f x y при заданных x и y всегда одно и то же. Иными словами, значения x и y однозначно определяют значение f x y. Это свойство также называют ссылочной прозрачностью (referential transparency).
Чистота также означает отсутствие побочных эффектов: вычисление f x y не может, например, прочитать строку от пользователя. При разных вызовах f строка могла бы отличаться и влиять на возвращаемое значение, нарушая ссылочную прозрачность. Разумеется, побочные эффекты необходимы для полезной работы программы. Позже мы разберём, как Haskell с ними справляется.
Haskell — ленивый (lazy) язык: значение не вычисляется, пока оно не понадобится. Лучше всего это видно на примере. Рассмотрим две функции:
f x = f x -- infinite recursion
g x y = xОценка f 1 не прекращается из-за бесконечной рекурсии. Однако это работает:
g 2 (f 1) ==> 2Лень не является проблемой, потому что Хаскелл чист. Только результат функции имеет значение, а не побочные эффекты. Поэтому, если результат функции не используется, мы просто не можем оценить его без изменения значения (семантики) программы. Хорошо, иногда мы получаем программу прекращения вместо той, которая продолжается вечно, но добавление лени никогда не делает функциональную программу Haskell перерывом.
Если вас интересует теория, лежащая в основе этого, ознакомьтесь с теоремой Church-Rosser или статьей Haskell Wiki «Ленивость и нестрогость» (Lazy vs. non-strict).
Референциальная прозрачность, особенность, что выражение всегда возвращает одно и то же значение для одних и тех же входов, является очень мощным свойством, которое мы можем использовать для того, чтобы рассуждать о программах.
В языке C-стиля мы можем написать процедуру, которая не всегда может возвращать одно и то же значение для одних и тех же аргументов.
int c = 0;
int funny(int x) {
return x + c++;
}
Выражение c++ увеличивает значение c и возвращает старое значение c. В следующий раз, когда она будет оценена, значение c увеличилось на единицу. Это означает, что в зависимости от текущего значения c, funny(0) может возвращать 0, 1, 2 или любое другое целое значение. (Это может даже вернуть отрицательные значения, если c переполнится!)
В некоторых ситуациях такое поведение с побочными эффектами может быть полезным, но бывают случаи, когда более важно иметь возможность легко рассуждать о коде. Преимущество чистых функций заключается в том, что их можно анализировать с помощью базовых математических методов. Иногда применение математики к нашим функциям может даже выявить упрощения или оптимизации, о которых мы бы не подумали.
Рассмотрим следующее выражение:
map (+1) . reverse . map (-1)Это выражение можно упростить до reverse. Мы начинаем с установления некоторых полезных фактов (или lemmas). Прежде всего, предположим, что мы знаем
map id === idmap f . map g === map (f.g)reverse . map f === map f . reverseЧетвертый факт, который нам понадобится, заключается в следующем:
(+1) . (-1) === idМы можем доказать факт 4, рассуждая о том, как (+1) . (-1) Поведение для произвольного ввода x:
((+1) . (-1)) x === ((+1) ((-1) x))
=== ((+1) (x - 1))
=== (x - 1) + 1
=== x
=== id xПоскольку мы ничего не предполагали об x, мы можем заключить, что приведенная выше цепочка уравнений удерживает every x. Таким образом,
(+1) . (-1) === idЕсли вы знакомы с доказательством по индукции (proof by induction), попробуйте в качестве упражнения доказать первые три утверждения. Индукционные доказательства не входят в этот курс, поэтому незнание индукции не помешает дальнейшему обучению.
Из фактов 1-4 следует, что
map (+1) . reverse . map (-1)
=== map (+1) . (reverse . map (-1)) -- By associativity of (.)
=== map (+1) . (map (-1) . reverse) -- By fact 3
=== (map (+1) . map (-1)) . reverse -- By associativity of (.)
=== map ((+1) . (-1)) . reverse -- By fact 2
=== map id . reverse -- By fact 4
=== id . reverse -- By fact 1
=== reverse -- By the definition of idЭтот курс не будет вдаваться в подробности о доказательстве вещей о программах, но хорошо знать, что чисто функциональное программирование очень совместимо с таким анализом. [+]
Преимущества лени лучше всего демонстрируют некоторые примеры с участием infinite lists. Начнем с repeat 1, который генерирует бесконечный список 1. Если мы попытаемся сказать GHCi напечатать значение repeat 1, он просто будет продолжать печатать 1 вечно, пока мы не прервем его с помощью Control-C:
Prelude> repeat 1
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1
^CОднако из-за лени мы можем работать с бесконечными списками и писать вычисления, которые заканчиваются. Нам просто нужно использовать конечное число элементов из бесконечного списка. Вот несколько примеров:
Prelude> take 10 $ repeat 1
[1,1,1,1,1,1,1,1,1,1]
Prelude> take 20 $ repeat 1
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]
Prelude> repeat 1 !! 13337
1Бесконечный список, который просто повторяет один элемент, иногда может быть необходим, но это бессмысленно. Рассмотрим несколько более полезных бесконечных списков. Вы можете использовать синтаксис [n..] для создания бесконечного списка чисел, начиная с n:
Prelude> take 20 [0..]
[0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19]
Prelude> take 10 . map (2^) $ [0..]
[1,2,4,8,16,32,64,128,256,512]Функция cycle повторяет элементы из данного списка снова и снова. Это может быть полезно при работе с вращениями или циклами.
Prelude> take 21 $ cycle "asdf"
"asdfasdfasdfasdfasdfa"
Prelude> take 4 . map (take 4) . tails $ cycle "asdf"
["asdf","sdfa","dfas","fasd"]В качестве более конкретного примера того, как cycle полезен, давайте рассмотрим вычисление контрольной цифры финских номеров транзакций банковских переводов (viitenumero). Номер транзакции состоит из произвольного количества цифр и одной контрольной цифры в конце. Для проверки остальные цифры справа налево умножают поочерёдно на 7, 3, 1, 7, 3, 1 и так далее, после чего складывают произведения. Если сумма вместе с контрольной цифрой делится на 10, номер считается корректным.
Вот конкретный пример. 116127 является действительным номером транзакции. Вычисления идут так:
digits: 1 1 6 1 2
* * * * *
multipliers: 3 7 1 3 7
3+ 7+ 6+ 3+14 = 33
check digit is 7, 33+7=40 is divisible by 10, valid
Вот код Haskell для проверки номера транзакции. Обратите внимание, как мы используем бесконечный список cycle [7,3,1] для множителей.
viitenumeroCheck :: [Int] -> Bool
viitenumeroCheck allDigits = mod (checksum+checkDigit) 10 == 0
where (checkDigit:digits) = reverse allDigits
multipliers = cycle [7,3,1]
checksum = sum $ zipWith (*) multipliers digitsviitenumeroCheck [1,1,6,1,2,7] ==> True
viitenumeroCheck [1,1,6,1,2,8] ==> FalseНаконец, вот как вы найдете первую мощность 3, которая больше 100.
Prelude> head . filter (>100) $ map (3^) [0..]
243Давайте рассмотрим, как это работает шаг за шагом. Обратите внимание, как карта и фильтр обрабатывают список лениво, по одному элементу за раз. Это похоже на то, как generators или iterators работают на таких языках, как Python или Java.
head (filter (>100) (map (3^) [0..]))
==> head (filter (>100) (map (3^) (0:[1..]))) -- evaluate first element of the lazy list
==> head (filter (>100) (1 : map (3^) [1..])) -- map processes the element
==> head (filter (>100) (map (3^) [1..])) -- filter drops the element
==> head (filter (>100) (map (3^) (1:[2..]))) -- evaluate second element of the lazy list
==> head (filter (>100) (3 : map (3^) [2..])) -- map processes the element
==> head (filter (>100) (map (3^) [2..])) -- filter drops the element
-- let's take bigger steps now
==> head (filter (>100) (9 : map (3^) [3..])) -- map processes, filter will drop
==> head (filter (>100) (27 : map (3^) [4..])) -- map processes, filter will drop
==> head (filter (>100) (81 : map (3^) [5..])) -- map processes, filter will drop
==> head (filter (>100) (243 : map (3^) [6..])) -- map processes
==> head (243 : filter (>100) (map (3^) [6..])) -- filter lets the value through
==> 243 -- head returns the resultЛень, вероятно, будет чувствовать себя немного волшебным для вас прямо сейчас. Вы можете задаться вопросом, как это можно реализовать. Оценка Haskell удивительно проста, она просто отличается от того, к чему вы привыкли. Давайте покопаемся.
В большинстве других языков программирования (таких как Java, C или Python) оценка продолжается снаружи. Аргументы функций оцениваются перед функцией.
Оценка Haskell происходит снаружи, а не снаружи. Определение внешней функции в выражении применяется без оценки каких-либо аргументов. Вот конкретный пример с игрушечными функциями f и g:
g :: Int -> Int -> Int
g x y = y+1
f :: Int -> Int -> Int -> Int
f a b c = g (a*1000) cВнутренняя (нормальная) оценка:
f 1 (1234*1234) 2
-- evaluate arguments to f
==> f 1 1522756 2
-- evaluate f
==> g (1*1000) 2
-- evaluate arguments to g
==> g 1000 2
-- evaluate g
==> 2+1
==> 3Хаскелл вне оценки:
f 1 (1234*1234) 2
-- evaluate f without evaluating arguments
==> g (1*1000) 2
-- evaluate g without evaluating arguments
==> 2+1
==> 3Обратите внимание, что неиспользованные расчеты 1234*1234 и 1*1000 не были оценены. Вот почему лень часто бывает полезной.
Давайте рассмотрим более сложный пример с сопоставлением шаблонов и более сложными данными (списками). Соответствие шаблона приводит к оценке Haskell очень конкретным образом, как мы увидим. Вот некоторые функции, которые мы будем использовать. Они знакомы из Прелюдии, но я дам им простые определения.
not True = False
not False = True
map f [] = []
map f (x:xs) = f x : map f xs
length [] = 0
length (x:xs) = 1+length xsВот внутренняя оценка выражения:
length (map not (True:False:[]))
==> length (not True : not False : []) -- evaluate call to map
==> length (False:True:[]) -- evaluate calls to not
==> 2Вот как проходит оценка в Хаскелле. Обратите внимание, что это не строго внешне, так как иногда нам нужно оценить внутренние аргументы, чтобы знать, какой шаблон соответствует.
length (map not (True:False:[]))
-- We can't evaluate length since we don't know which equation of length applies,
-- so we look at length's argument. We can apply the second equation of map, so we do.
==> length (not True : map not (False:[]))
-- Now the argument of length has a (:) we can pattern match on, so we apply the
-- second equation of length
==> 1 + length (map not (False:[]))
-- The outermost function is now +, but it can't do anything unless both arguments
-- are numbers. So we need to evaluate length. In order to pick an equation, we need
-- to evaluate the argument of length again. We apply the second equation of map
==> 1 + length (not False : map not ([]))
-- Now we can apply the second equation of length again.
==> 1 + (1 + length (map not []))
-- The outermost + needs a number to be evaluated. The second + also needs a number.
-- We need to evaluate length again, which means we need to pick an equation for length,
-- which means we need to evaluate its argument. This time it is the first equation for
-- map that applies.
==> 1 + (1 + length [])
-- Now we can apply the first equation for length
==> 1 + (1 + 0)
-- The outermost + still can't be evaluated, but the inner one can
==> 1 + 1
-- Finally we evaluate the outer +
==> 2Обратите внимание, что нам не нужно было оценивать какие-либо приложения not.
Введём несколько терминов. Говорят, что сопоставление с образцом форсирует (forces) вычисление. Haskell вычисляет значение до слабой головной нормальной формы (weak head normal form, WHNF). По сути, WHNF — это значение, с которым уже можно сопоставить образец. Выражение находится в WHNF, если его нельзя вычислить дальше на верхнем уровне. Отсюда следует:
1False, Just (1+1), 0:filter f xs(\x -> 1+x)Наиболее заметным классом выражений, который является not в WHNF, являются функциональные приложения. Если выражение состоит из функции (то есть не является конструктором), применяемой к некоторым аргументам, то оно не в WHNF. Мы должны оценить его, чтобы получить что-то сопоставимое.
В предыдущем примере мы не могли выбрать уравнение для length в length (map not (False:[])). Аргумент (map not ...) отсутствует в WHNF, поэтому его нельзя сопоставить с шаблоном. Поэтому мы должны его оценить. Когда мы применяем второе уравнение map, мы получаем length (not False : map not []), и теперь аргумент в пользу длины находится в WHNF, поскольку на верхнем уровне находится конструктор (:). Это немного более очевидно, если мы перейдем от инфикса к префиксной записи и напишем аргумент length как (:) (not False) (map not []).
На практике сопоставление шаблонов — не единственное, что заставляет оценивать. Примитивисты, такие как (+), также приводят свои аргументы.
Вместо того, чтобы заставлять, некоторые источники говорят о строгость Например, мы можем сказать, что (+) это Строго в обоих аргументах.
Есть еще одна вещь об оценке Хаскелла. Каждый раз, когда вы даете значение name, он получает shared. Это означает, что каждое появление имени указывает на одно и то же (потенциально неоцененное) выражение. Когда выражение оценивается, все вхождения имени видят результат.
Давайте рассмотрим очень простой пример.
square x = x*xОсновываясь на предыдущих разделах, вы можете представить себе, что оценка работает следующим образом. Оценка сначала представлена текстуально, а затем визуально, как дерево выражения.
square (2+2)
==> (2+2) * (2+2) -- definition of square
==> 4 * (2+2) -- (*) forces left argument
==> 4 * 4 -- (*) forces right argument
==> 16 -- definition of (*)На самом деле выражение 2+2, которому дали имя x, вычисляется лишь один раз. Затем полученный результат используется в обоих вхождениях x внутри square. Ниже показано корректное вычисление — сначала текстом, затем графически. Теперь вместо дерева выражений получается граф выражений, поэтому модель вычислений Haskell иногда называют уменьшение графа.
square (2+2)
==> (2+2) * (2+2)
==> 4 * 4
==> 16В качестве другого примера рассмотрим функцию f ниже и ее оценку.
f :: Int -> Int
f i = if i>10 then 10 else i _______shared________
| |
f (1+1) ==> if (1+1)>10 then 10 else (1+1)
==> if 2>10 then 10 else 2
==> if False then 10 else 2
==> 2Haskell не вычисляет 1+1 дважды, потому что он был назван, и имя использовалось дважды. Мы можем противопоставить это другой функции, которая принимает два аргумента:
g :: Int -> Int -> Int
g i j = if i>10 then 10 else j ______no sharing_____
| |
g (1+1) (1+1) ==> if (1+1)>10 then 10 else (1+1)
==> if 2>10 then 10 else (1+1)
==> if False then 10 else (1+1)
==> (1+1)
==> 2Здесь у нас есть два разных имени для эквивалентных выражений, и Хаскелл волшебным образом не делится ими. Автоматически совместное использование эквивалентных выражений называется оптимизацией Common Subexpression Elimination (CSE). Узнать немного больше CSE и Haskell можно здесь.
Вы можете назвать вещи через
let ... in ...whereВ сочетании с ленью обмен означает, что Имя оценивается не более одного раза.
Ниже вы найдете немного надуманное рекурсивное определение функции even. Он будет иллюстрировать концепции принуждения и обмена.
not :: Bool -> Bool
not True = False
not False = True
(||) :: Bool -> Bool -> Bool
True || _ = True
_ || x = x
even :: Int -> Bool
even x = (x == 0) || not (even (x-1))Сначала заметим, что || форсирует левый аргумент, но не правый. Иными словами, || строг по левому аргументу (strict in its left argument). Чтобы выбрать подходящее уравнение, достаточно вычислить левый аргумент ||. Следовательно, even форсирует свой первый аргумент:
|| вытесняет x==0x==0 вытесняет xТеперь давайте оценим выражение even 2 по WHNF.
even 2
==> 2 == 0 || not (even (2-1)) -- apply definition of even
==> False || not (even (2-1)) -- || forces its first argument
==> not (even (2-1)) -- second equation of ||
==> not ((2-1) == 0 || not (even ((2-1)-1))) -- not forces its argument: apply definition of even
==> not ( 1 == 0 || not (even ( 1 -1))) -- note sharing!
==> not ( False || not (even (1-1)))
==> not (not (even (1-1)))
==> not (not ((1-1) == 0 || not (even ((1-1)-1))))
==> not (not ( 0 == 0 || not (even ( 0 -1)))) -- (sharing)
==> not (not ( True || not (even (0-1))))
==> not (not True)
==> not False
==> TrueОбратите внимание, что с этим альтернативным определением even не сработало бы. Вы можете сказать почему?
even' x = not (even' (x-1)) || x == 0Теперь мы действительно можем понять, что происходит в бесконечном списке примеров. Давайте используем эти определения:
head (x:_) = x
head [] = -1
filter p [] = []
filter p (x:xs) = if p x
then x : filter p xs
else filter p xs
map f [] = []
map f (x:xs) = f x : map f xs
-- [0..] is syntax sugar for enumFrom 0
enumFrom n = n : enumFrom (n+1)А вот и мы:
head (filter (>100) (map (3^) [0..]))
=== head (filter (>100) (map (3^) (enumFrom 0)))
-- head forces filter, which forces map, which forces enumFrom. We apply the definition of enumFrom.
==> head (filter (>100) (map (3^) (0:[1..])))
-- head forces filter, which forces map. We apply the second equation of map.
==> head (filter (>100) ((3^0) : map (3^) [1..]))
-- head forces filter. We apply the second equation of filter
==> head (if ((3^0)>100)
then (3^0) : filter (>100) (map (3^) [1..])
else filter (>100) (map (3^) [1..]))
-- head forces if, if forces >, > forces ^. Note sharing!
==> head (if (1>100)
then 1 : filter (>100) (map (3^) [1..])
else filter (>100) (map (3^) [1..]))
-- head forces if, if forces >
==> head (if False
then 1 : filter (>100) (map (3^) [1..])
else filter (>100) (map (3^) [1..]))
-- apply definition of if
==> head (filter (>100) (map (3^) [1..]))
-- let's take slightly bigger steps now
==> head (filter (>100) (map (3^) (1:[2..])))
==> head (filter (>100) ((3^1) : map (3^) [2..]))
==> head (filter (>100) (3 : map (3^) [2..]))
==> head (filter (>100) (map (3^) [2..]))
-- and even bigger steps now
==> head (filter (>100) (9 : map (3^) [3..]))
==> head (filter (>100) (27 : map (3^) [4..]))
==> head (filter (>100) (81 : map (3^) [5..]))
==> head (filter (>100) (243 : map (3^) [6..]))
==> head (243 : filter (>100) (map (3^) [6..]))
==> 243Вуу.
Функции, которые работают со списками, часто имеют лучшую производительность, когда они написаны таким образом, что они используют лень. Один из способов сделать это — написать функции обработки списков, которые хорошо работают с бесконечными списками.
Чтобы написать функцию, которая преобразует бесконечный список, нужно написать функцию, которая смотрит только на ограниченный префикс списка ввода, затем выводит конструктор (:), а затем повторяется. Вот первый пример.
everySecond :: [a] -> [a]
everySecond [] = []
everySecond (x:y:xs) = x : everySecond xstake 10 (everySecond [0..]) ==> [0,2,4,6,8,10,12,14,16,18]Хорошая эвристика для написания функций, которые хорошо работают с бесконечными списками, заключается в следующем: может ли head результата быть оценено дешево? Вот два примера функций, которые не работают с бесконечными входами. В случае с mapTailRecursive проблема заключается в том, что перед тем, как войти в WHNF, он должен обработать весь вход. В случае с myDrop проблема заключается в том, что он использует функцию length, которая не работает для бесконечных списков.
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
mapTailRecursive :: (a -> b) -> [a] -> [b]
mapTailRecursive f xs = go xs []
where go (x:xs) res = go xs (res++[f x])
go [] res = reshead (map succ [0..]) ==> head (succ 0 : map succ [1..]) ==> succ 0 ==> 1
head (mapTailRecursive succ [0..])
==> head (go [0..] [])
==> head (go [1..] ([]++[succ 0]))
==> head (go [2..] ([]++[succ 0]++[succ 1]))
==> head (go [3..] ([]++[succ 0]++[succ 1]++[succ 2]))
-- never terminatesdrop :: Int -> [a] -> [a]
drop 0 xs = xs
drop _ [] = []
drop n (x:xs) = drop (n-1) xs
myDrop :: Int -> [a] -> [a]
myDrop 0 xs = xs
myDrop n xs = if n > length xs then [] else myDrop (n-1) (tail xs)head (drop 2 [0..]) ==> head (drop 1 [1..]) ==> head (drop 0 [2..]) ==> head [2..] ==> 2
head (myDrop 2 [0..])
==> head (if n > length [0..] then [] else myDrop (n-1) (tail [0..]))
==> head (if n > 1+length [1..] then [] else myDrop (n-1) (tail [0..]))
==> head (if n > 1+1+length [2..] then [] else myDrop (n-1) (tail [0..]))
==> head (if n > 1+1+1+length [3..] then [] else myDrop (n-1) (tail [0..]))
-- never terminatesПрактически все функции списка в стандартной библиотеке написаны в таком виде, например:
head (takeWhile (>=0) [0..]) ==> 0
head (concat (repeat [1,2,3])) ==> 1
head (zip [0..] [2..]) ==> (0,2)
head (filter even [3..]) ==> 4Помните foldr из части 1? Рассмотрим родственную функцию foldl. Ниже приведено определение foldl для списков; на самом деле она входит в класс типов Foldable и работает также с другими структурами. foldr обрабатывает список справа налево, а foldl — слева направо. Точнее, foldr правоассоциативна, тогда как foldl левоассоциативна. Сравните:
foldr (+) 0 [1,2,3] ==> 1+(2+(3+0))
foldl (+) 0 [1,2,3] ==> ((0+1)+2)+3Вот определения foldl и foldr:
foldl :: (a -> b -> a) -> a -> [b] -> a
foldl f z [] = z
foldl f z (x:xs) = foldl f (f z x) xsfoldr :: (a -> b -> b) -> b -> [a] -> b
foldr f y [] = y
foldr f y (x:xs) = f x (foldr f y xs)Как foldr f y (x:xs) ==> f x (foldr f y xs), он позволяет ленивым оценкам сосредоточиться на f на втором этапе. Таким образом, foldr хорошо работает с ленивыми или короткозамыкающими операциями:
foldr (&&) True [False,False,False]
==> False && (foldr (&&) True [False,False])
==> False head (foldr (++) [] ["Hello","World","lorem","ipsum"])
==> head ("Hello" ++ (foldr (++) [] ["World","lorem","ipsum"]))
==> head ('H':("ello" ++ (foldr (++) [] ["World","lorem","ipsum"])))
==> 'H'Однако foldl необходимо обработать весь список, чтобы получить значение WHNF. Причина в том, что foldl остается в самой левой позиции до тех пор, пока его аргументы в списке остаются пустыми. Это делает foldl приоритетом для ленивых оценок. Только после того, как список становится пустым, оценка переходит к упрощению сложенных значений.
foldl (&&) True [False,False,False]
==> foldl (&&) (True&&False) [False,False]
==> foldl (&&) ((True&&False)&&False) [False]
==> foldl (&&) (((True&&False)&&False)&&False) []
==> ((True&&False)&&False)&&False
==> ( False &&False)&&False
==> False &&False
==> False head (foldl (++) [] ["Hello","World","lorem","ipsum"])
==> head (foldl (++) ([]++"Hello") ["World","lorem","ipsum"])
==> head (foldl (++) (([]++"Hello")++"World") ["lorem","ipsum"])
==> head (foldl (++) ((([]++"Hello")++"World")++"lorem") ["ipsum"])
==> head (foldl (++) (((([]++"Hello")++"World")++"lorem")++"ipsum") [])
==> head (((([]++"Hello")++"World")++"lorem")++"ipsum")
-- head forces the last ++, which forces the next-to-last ++, and so on
==> head ((("Hello"++"World")++"lorem")++"ipsum")
-- same happens again
==> head ((('H':("ello"++"World"))++"lorem")++"ipsum")
-- for clarity, let's drop the "ello"++"World" expression which isn't needed
==> head ((('H':__)++"lorem")++"ipsum")
-- now the next-to-last ++ can operate
==> head (('H':(__++"lorem"))++"ipsum")
-- let's drop the __++"lorem" expression
==> head (('H':__)++"ipsum")
-- now the last ++ can operate
==> head ('H':(__++"ipsum"))
==> 'H'Так зачем вообще использовать foldl? Вернемся к нашему первому примеру. Теперь, поскольку + является строгой операцией, оба типа сгиба должны создавать выражение с большим количеством +. Реализация Haskell должна отслеживать это выражение в памяти, поэтому такая проблема называется space leak.
foldr (+) 0 [1,2,3]
==> 1 + foldr (+) 0 [2,3]
==> 1 + (2 + foldr (+) 0 [3])
==> 1 + (2 + (3 + foldr (+) 0 []))
==> 1 + (2 + (3 + 0))
==> 1 + (2 + 3)
==> 1 + 5
==> 6 foldl (+) 0 [1,2,3]
==> foldl (+) (0+1) [2,3]
==> foldl (+) ((0+1)+2) [3]
==> foldl (+) (((0+1)+2)+3) []
==> ((0+1)+2)+3
==> ( 1 +2)+3
==> 3 +3
==> 6Теперь давайте посмотрим, что происходит, когда мы используем foldl', версию foldl, которая выдвигает свой второй аргумент!
foldl' (+) 0 [1,2,3]
==> foldl' (+) (0+1) [2,3]
-- force second argument
==> foldl' (+) 1 [2,3]
==> foldl' (+) (1+2) [3]
-- force second argument
==> foldl' (+) 3 [3]
==> foldl' (+) (3+3) []
-- force second argument
==> foldl' (+) 6 []
==> 6Теперь работа выполняется постепенно при сканировании списка. Никакой утечки! Иногда слишком много лени может вызвать утечку пространства, и немного строгости может исправить их.
Вы можете найти foldl' в Data.List Модуль, и он работает именно так. Но как можно реализовать foldl' Мы точно знаем, как это сделать для определенного типа, скажем. Int Мы просто добавляем совпадение шаблонов на второй аргумент, который не изменяет семантику функции.
foldl'Int :: (Int -> Int -> Int) -> Int -> [Int] -> Int
foldl'Int f z [] = z
foldl'Int f 0 (x:xs) = foldl'Int f (f 0 x) xs
foldl'Int f z (x:xs) = foldl'Int f (f z x) xs foldl'Int (+) 0 [1,2,3]
==> foldl'Int (+) (0+1) [2,3]
-- to be able to pick between the second and third equations, (0+1) is forced
==> foldl'Int (+) 1 [2,3]
-- the third equation applies
==> foldl'Int (+) (1+2) [3]
-- again, we need to pick between the second and third equations
==> foldl'Int (+) 3 [3]
==> foldl'Int (+) (3+3) []
==> 3+3
==> 6Для написания общей реализации foldl' необходимо ввести новую встроенную функцию seq. Звонок seq a b оценивает b, но вынуждает a войти в WHNF. Вот несколько примеров использования seq в GHCi. Чтобы продемонстрировать то, что оценивается, мы используем специальное значение undefined, которое вызывает ошибку, если что-то пытается оценить его в WHNF.
Prelude> seq (not True) 3
3
Prelude> seq undefined 3
*** Exception: Prelude.undefined
Prelude> (seq (not True) 3) + 7
10
Prelude> (seq undefined 3) + 7
*** Exception: Prelude.undefined
Prelude> let f x = f x in seq (f 3) 3
-- ...infinite recursionВ качестве примера использования seq в функции приведена версия head, которая не работает для бесконечных списков (поскольку она оценивает последний элемент списка):
strictHead :: [a] -> a
strictHead xs = seq (last xs) (head xs)Давайте поиграем с ним в GHCi:
Prelude> head [1,2,3]
1
Prelude> strictHead [1,2,3]
1
Prelude> head (1:2:undefined)
1
Prelude> strictHead (1:2:undefined)
*** Exception: Prelude.undefined
Prelude> head [1..]
1
Prelude> strictHead [1..]
-- ...infinite recursionНаконец, вот определение для foldl'. Обратите внимание, как нам нужно ввести совместное использование новой переменной, z', чтобы seq мог оценить новое значение, а затем использовать его в рекурсивном вызове. Новое определение также используется в более подробной оценке foldl' (+) 0 [1,2,3] ниже.
foldl' :: (a -> b -> a) -> a -> [b] -> a
foldl' f z [] = z
foldl' f z (x:xs) = let z' = f z x
in seq z' (foldl' f z' xs) foldl' (+) 0 [1,2,3]
==> seq (0+1) (foldl' (+) (0+1) [2,3]) -- seq forces first argument
| |
+-----sharing-----'
| |
==> seq 1 (foldl' (+) 1 [2,3]) -- first argument to seq in WHNF, seq disappears
==> foldl' (+) 1 [2,3]
==> seq (1+2) (foldl' (+) (1+2) [3])
==> seq 3 (foldl' (+) 3 [3])
==> foldl' (+) 3 [3]
==> seq (3+3) (foldl' (+) (3+3) [])
==> seq 6 (foldl' (+) 6 [])
==> foldl' (+) 6 []
==> 6Мы не будем углубляться в эту тему на этом курсе, но важно, чтобы вы знали, что seq существует. Подробнее об seq можно узнать на Haskell Wiki, а о том, когда нужна строгость, — в книге Real World Haskell. Часто лучше использовать шаблоны bang вместо seq, как обсуждается FPComplete и Real World Haskell.
Вспомните лекцию 7. Иногда нужны коробочные типы. Есть специальное ключевое слово newtype Можно использовать вместо data Когда нужен тип коробки. newtype Мы ожидаем одного строителя, с одним полем. Например,
newtype Money = Cents IntТем не менее, следующие не будут работать, вам нужно data:
-- the compiler won't accept these!
newtype Currency = Dollars Int | Euros Int
newtype Money = Money Int IntТак в чем разница? С точки зрения написания кода, ничего. Вы работаете с newtype точно так же, как с data. Однако макет памяти отличается. С помощью data вводится косвенный слой (конструктор), а с помощью newtype – нет. Опосредование для data необходимо для поддержки нескольких конструкторов и нескольких полей. Иллюстрация:
code: memory:
data Money = Cents Int x --> Cents --> 100
x = Cents 100
newtype Money = Cents Int x --> 100
x = Cents 100Эта разница имеет много последствий. Прежде всего, newtype более эффективен: при компиляции тип можно сказать «исчезает». Тип все еще проверяется, поэтому вы получаете безопасность типа без какого-либо воздействия на производительность. Во-вторых, новинками являются strict. Конкретно это означает, что Money x находится в слабой нормальной форме головы, только если x находится в WHNF. Это можно увидеть в GHCi:
-- if we use data, Cents undefined is in WHNF
Prelude> data Money = Cents Int
Prelude> seq (Cents undefined) True
True
-- if we use newtype, Cents undefined isn't in WHNF, and trying
-- to make it so trips up in undefined
Prelude> newtype Money = Cents Int
Prelude> seq (Cents undefined) True
*** Exception: Prelude.undefinedКогда следует использовать newtype? В общем, лучше всего использовать newtype, когда у вас есть однопользовательский одноконструкторный тип данных. Однако ничто не пойдет катастрофически неправильно, если вы всегда используете data. Модель newtype также часто используется, когда вам нужно определить другой экземпляр класса типа для типа. Вот пример, который определяет тип числа с перевернутым заказом.
newtype Inverted = Inverted Int
deriving (Show, Eq)
instance Ord Inverted where
compare (Inverted i) (Inverted j) = compare j iPrelude Data.List> sort [1,2,3]
[1,2,3]
Prelude Data.List> sort [Inverted 1,Inverted 2,Inverted 3]
[Inverted 3,Inverted 2,Inverted 1]Теперь, разобравшись с совместным использованием и копированием пути, мы можем создать собственную циклическую структуру данных. Помните примеры с функцией cycle?
Prelude> take 21 $ cycle "asdf"
"asdfasdfasdfasdfasdfa"Вот как это выглядит в памяти:
Ранее сообщалось, что формы данных Haskell направляли графы в память. Это пример направленного графа с циклом.
Как можно определить такие структуры? Мы просто даем значение имени и ссылаемся на это имя в самой стоимости. То есть значение recursive или self-referential. Этот трюк известен как tying knot. Простой пример:
code memory
let xs = 1:2:xs xs -> (1:) -> (2:) -+
in xs ^ |
+------------+Обратите внимание, как мы используем название xs в определении xs. Когда мы делаем такое рекурсивное определение, совместное использование вызывает его превращение в циклическую структуру памяти.
Более забавный пример: простая приключенческая игра, в которой мир является самореферентной структурой. Обратите внимание, как циклическая структура построена с местными определениями, которые относятся друг к другу.
data Room = Room String [(String,Room)]
describe :: Room -> String
describe (Room s _) = s
move :: Room -> String -> Maybe Room
move (Room _ directions) direction = lookup direction directions
world :: Room
world = meadow
where
meadow = Room "It's a flowery meadow next to a cliff." [("Stay",meadow),("Enter cave",cave)]
cave = Room "You are in a cave" [("Exit",meadow),("Go deeper",tunnel)]
tunnel = Room "This is a very dark tunnel. It seems you can either go left or right."
[("Go back",cave),("Go left",pit),("Go right",treasure)]
pit = Room "You fall into a pit. There is no way out." []
treasure = Room "A green light from a terminal fills the room. The terminal says <<loop>>."
[("Go back",tunnel)]
play :: Room -> [String] -> [String]
play room [] = [describe room]
play room (d:ds) = case move room d of Nothing -> [describe room]
Just r -> describe room : play r dsPrelude> play world ["Stay","Enter cave","Go deeper","Go back","Go deeper","Go right"]
["It's a flowery meadow next to a cliff.",
"It's a flowery meadow next to a cliff.",
"You are in a cave",
"This is a very dark tunnel. It seems you can either go left or right.",
"You are in a cave",
"This is a very dark tunnel. It seems you can either go left or right.",
"A green light from a computer terminal floods the room. The terminal says <<loop>>."]Вот как выглядит world игры в памяти:
,-----------------,
v |
+-----------------------|-----------------+
meadow-->|Room "It's..." ["Stay" o, "Enter cave" o]|
+---------------------------------------|-+
^ v
+--------------------------|----------------+
cave---->|Room "You are..." ["Exit" o, "Go deeper" o]|
+-----------------------------------------|-+
^ v
+-----------------------------|----------------------------+
tunnel-->|Room "This is..." ["Go back" o, "Go left" o, "Go right" o]|<--------,
+------------------------------------------|-------------|-+ |
| | |
,------------------------------' | |
v v |
+---------------------+ +-----------------------------|-+
pit----->|Room "You fall..." []| treasure--->|Room "A green..." ["Go back" o]|
+---------------------+ +-------------------------------+
Сейчас мы наблюдаем три типа рекурсий. Рекурсивные функции называют себя. Рекурсивные типы позволяют выражать произвольно большие структуры. Рекурсивные значения являются одним из способов реализации бесконечных структур.
Несмотря на то, что Haskell является чистым языком программирования, мы иногда можем получить представление о нем, посыпав немного примеси.
Мы можем использовать функцию trace :: String -> a -> a из модуля Debug.Trace, чтобы заглянуть в оценку Haskell. Выражение trace "message" x такое же, как и x, но печатает message при его оценке. Мы можем использовать trace, чтобы засвидетельствовать лень оператора ||:
Prelude> import Debug.Trace
Prelude Debug.Trace> trace "a" True
a
True
Prelude Debug.Trace> trace "a" False || trace "b" True
a
b
True
Prelude Debug.Trace> trace "a" True || trace "b" True
a
TrueМы также можем посмотреть, когда оцениваются элементы списка. Обратите внимание, что length не нуждается в оценке элементов списка, и sum должен оценить их все. (Чтобы быть точным, head xs фактически не оценивает первый элемент xs, но возвращает его в GHCi, который оценивает его, чтобы показать его.)
Prelude Debug.Trace> head [trace "first" 1, trace "second" 2, trace "third" 3]
first
1
Prelude Debug.Trace> last [trace "first" 1, trace "second" 2, trace "third" 3]
third
3
Prelude Debug.Trace> length [trace "first" 1, trace "second" 2, trace "third" 3]
3
Prelude Debug.Trace> sum [trace "first" 1, trace "second" 2, trace "third" 3]
third
second
first
6Модуль Debug.Trace предлагает и полезные варианты trace. Особенно удобна функция traceShowId x: она печатает show x, а результатом оставляет x. Проверим вычисление предыдущего примера с head, filter и map с помощью traceShowId. Хотя traceShowId отображается на бесконечный список [0..], фактически вычисляются только шесть значений. Последнее число 243 — возвращаемый результат, а не сообщение трассировки.
Prelude Debug.Trace> head (filter (>100) (map (\x -> traceShowId (3^x)) [0..]))
1
3
9
27
81
243
243Debug.Trace особенно полезен, когда у вас есть бесконечная рекурсионная ошибка. Вот пример:
-- computes sums like 7+5+3+1
sumEverySecond :: Int -> Int
sumEverySecond 0 = 0
sumEverySecond n = n + sumEverySecond (n-2)sumEverySecond 6 ==> 12
sumEverySecond 7 ==> doesn't terminateМы можем отладить это, добавив trace, чтобы обернуть весь рекурсивный случай.
sumEverySecond :: Int -> Int
sumEverySecond 0 = 0
sumEverySecond n = trace ("sumEverySecond "++show n) (n + sumEverySecond (n-2))Prelude Debug.Trace> sumEverySecond 6
sumEverySecond 6
sumEverySecond 4
sumEverySecond 2
12
Prelude Debug.Trace> sumEverySecond 7
sumEverySecond 7
sumEverySecond 5
sumEverySecond 3
sumEverySecond 1
sumEverySecond -1
sumEverySecond -3
sumEverySecond -5
-- and so onХа! Проблема в том, что нашего базового случая рекурсии sumEverySecond 0 недостаточно, чтобы остановить рекурсию.
Наконец, слово осторожности. Использование trace, и особенно traceShowId, может вызвать вещи, которые иначе не были бы оценены, чтобы получить оценку. Например:
Prelude Debug.Trace> traceHead xs = head (traceShowId xs)
Prelude Debug.Trace> traceHead [0..]
-- never terminates since it's trying to show an infinite listПоэтому не стесняйтесь использовать Debug.Trace при работе над Упражнениями, но постарайтесь оставить trace звонки из ваших окончательных ответов. Некоторые наборы упражнений проверяют ваш импорт и запрещают Debug.Trace.
В следующей лекции мы увидим более принципиальный способ борьбы с побочными эффектами!
Какое из этих утверждений верно?
reverse . reverse . reverse === reversereverse . reverse === reversereverse . id === idКакой из них является бесконечным списком, который начинается с [0,1,2,1,2,1,2...]?
cycle [0,1,2]0:repeat [1,2]0:cycle [1,2]0:[1,2..]Каков следующий шаг при оценке этого выражения?
head (map not (True:False:[]))head (False : True : [])head (not True)head (False : map not (False:[]))head (not True : map not (False:[]))Какое из этих значений not в слабой нормальной форме головы?
mapf 1 : map f (2 : [])Just (not False)(\x -> x) TrueКакое из этих утверждений о следующей функции верно?
f 0 x = 1+x
f _ x = 2+xf строго придерживается левого аргументаf строга в своих аргументахf приводит оба аргументаРаботает ли эта функция с бесконечными списками? Почему?
f [] = []
f (x:xs) = x : map not xs[], который так и не был достигнут.map, который оценивает весь список.map, который работает с бесконечными списками.Что насчет этого?
f xs = map (+(sum xs)) xsmap, который оценивает весь список.sum результата требует всего списка входных данных.[]map, который работает с бесконечными списками.RealWorld -> (a,RealWorld)На минуту забудьте всё сказанное о функциональном программировании и чистоте. Haskell — лучший императивный язык программирования в мире! Начнём:
questionnaire = do
putStrLn "Write something!"
s <- getLine
putStrLn ("You wrote: "++s)Prelude> questionnaire
Write something!
Haskell!
You wrote: Haskell!Чтение ввода и запись вывода было достаточно легко. Мы также можем читать по сети. Вот полная программа Haskell, которая извлекает некоторые слова из URL-адреса с помощью HTTP и печатает их.
import Network.HTTP
import Control.Monad
main = do
rsp <- simpleHTTP (getRequest "http://httpbin.org/base64/aGFza2VsbCBmb3IgZXZlcgo=")
body <- getResponseBody rsp
forM_ (words body) $ \w -> do
putStr "word: "
putStrLn wВы можете найти эту программу в репозитории курса как exercises/Examples/FetchWords.hs, и вы можете запустить его следующим образом:
$ cd exercises/Examples
$ stack runhaskell FetchWords.hs
word: haskell
word: for
word: ever
Что здесь происходит? Давайте посмотрим на типы:
Prelude> :t putStrLn
putStrLn :: String -> IO ()
Prelude> :t getLine
getLine :: IO StringЗначение типа IO a — это операция (operation), которая производит значение типа a. Например, getLine — операция ввода-вывода, производящая строку. Тип () называется единичным типом (unit type), и его единственное значение — (). Он используется, когда операция ввода-вывода не возвращает содержательного результата, но выполняет побочные эффекты.
Сравнение с типами Java может помочь:
| тип Haskell | Тип Java |
|---|---|
doIt :: IO () |
void doIt() |
getSomething :: IO Int |
int getSomething() |
force :: a -> b -> IO () |
void force(a arg0, b arg1) |
mogrify :: c -> IO d |
d mogrify(c arg) |
Операции ввода-вывода могут быть объединены в более крупные операции с использованием do-notation.
do operation
operation arg
variable <- operationThatReturnsStuff
let var2 = expression
operationThatProducesTheResult var2Полезные операции ввода-вывода можно найти в стандартных библиотечных модулях Prelude и System. IO
Вот операция ввода-вывода, которая запрашивает у пользователя строку и распечатывает длину строки.
query :: IO ()
query = do
putStrLn "Write something!" -- run an operation, ignore produced value
s <- getLine -- run an operation, capture produced value
let n = length s -- run a pure function
putStrLn ("You wrote "++show n++" characters") -- run an operation, passing on the produced valuePrelude> query
Write something!
lorem ipsum
You wrote 11 charactersЗначение, произведенное последней линией блока do, является значением, произведенным целым блоком. Обратите внимание, что askForALine имеет тот же тип, что и getLine, IO String:
askForALine :: IO String
askForALine = do
putStrLn "Please give me a line"
getLineВ дополнение к операциям ввода-вывода, таким как query, вы также можете запускать операции ввода-вывода, которые производят значения, такие как askForALine, в GHCi. Вы можете использовать <- для захвата результата операции в переменную, если хотите.
Prelude> askForALine
Please give me a line
this is a line
"this is a line"
Prelude> line <- askForALine
Please give me a line
this is a line
Prelude> :t line
line :: String
Prelude> line
"this is a line"Если операции нужны параметры, можно определить функцию, возвращающую операцию. Обратите внимание: тип ask, как у обычной функции, содержит ->. Обычный синтаксис определения функции используется и для имени параметра question.
ask :: String -> IO String
ask question = do
putStrLn question
getLinePrelude> ask "What is love?"
What is love?
Baby don't hurt me!
"Baby don't hurt me!"
Prelude> response <- ask "Who are you?"
Who are you?
The programmer.
Prelude> response
"The programmer."
Prelude> :t response
response :: String
Prelude> :t ask
ask :: String -> IO String
Prelude> :t ask "Who are you?"
ask "Who are you?" :: IO StringreturnНазвание функции Haskell return немного сбивает с толку. В других языках return — встроенное ключевое слово, а в Haskell это обычная функция. return :: a -> IO a принимает значение и превращает его в операцию, производящую это значение.
produceThree :: IO Int
produceThree = return 3
printThree :: IO ()
printThree = do
three <- produceThree
putStrLn (show three)Это звучит не очень полезно, не так ли? В сочетании с нотацией это так. Здесь мы возвращаем boolean в зависимости от того, ответил ли пользователь Y или N:
yesNoQuestion :: String -> IO Bool
yesNoQuestion question = do
putStrLn question
s <- getLine
return (s == "Y")Prelude> yesNoQuestion "Fire the missiles?"
Fire the missiles?
Y
True
Prelude> answer <- yesNoQuestion "Are you sure?"
Are you sure?
N
Prelude> :t answer
answer :: Bool
Prelude> answer
FalseЗаписка! Это означает, что возвращение Не прекращается исполнение Операция (в отличие от Java или C). Помните, что в do-блоках последняя строка определяет, какую стоимость производить. Это означает, что данная операция производит 2:
produceTwo :: IO Int
produceTwo = do return 1
return 2Prelude> produceTwo
2Давайте посмотрим на это по-другому. Нотация do позволяет вызвать последовательность побочных эффектов и, наконец, получить значение.
produceThree = do putStrLn "1" -- side effect, produces (), which is ignored
return 2 -- no side effect, produces 2, which is ignored
getLine -- side effect, produces a String, which is ignored
return 3 -- no side effect, produces 3, which is passed on
Prelude> final <- produceThree
1
this line is ignored
Prelude> final
3
Также обратите внимание, что это одна и та же операция:
do ...
x <- op
return xdo ...
opПоскольку return является функцией, следует помнить о скобках любых сложных выражений:
return (f x : xs)
-- alternatively:
return $ f x : xsdo и типыДавайте рассмотрим типизацию донотации более подробно. Do-блок строит значение типа IO <something>. Например, в
foo = do
...
lastOplastOp должен быть типа IO X (для некоторых X). Типом foo также будет IO X. Рассмотрим следующий пример с параметрами:
bar x y = do
...
lastOp arglastOp должен быть типа Y -> IO X (так что lastOp arg имеет тип IO X). Тип bar будет A -> B -> IO X (а внутри bar будут x :: A и y :: B).
Если вы используете return:
quux x = do
...
return valueФункция quux будет иметь тип A -> IO B, где x :: A и value :: B.
Давайте посмотрим на типизацию <- дальше. Если op :: IO X и у вас есть var <- op, var будет иметь тип X. Мы видели это во многих примерах GHCi.
Последняя линия do не может быть foo <- bar. Это также не может быть let foo = bar. Последняя линия определяет, что производит вся операция, поэтому она должна быть операцией (например, return something).
Вот работающий пример:
alwaysFine :: IO Bool
alwaysFine = do
putStrLn "What?" -- :: IO ()
return 2 -- :: IO Int, produced value is discarded
s <- getLine -- getLine :: IO String, thus s :: String
putStrLn s -- putStrLn :: String -> IO (), thus putStrLn s :: IO ()
let b = True -- b :: Bool
return b -- :: IO Bool
-- Thus, alwaysFine :: IO BoolСистема типов не позволяет «сбежать» из IO. Хотя <- извлекает X из IO X, использовать <- можно только внутри do, а весь блок do снова имеет тип IO Y. Иными словами, коробку IO можно ненадолго открыть, но затем необходимо в неё вернуться. Что происходит в IO, остаётся в IO.
Позже мы поговорим о том, что это значит. На данный момент достаточно знать, что если у вас есть функция с типом не-IO, например, myFunction :: Int -> [String] -> String, внутри нее не может происходить IO. Это чистая функция.
Для следующих примеров нам понадобятся две новые операции.
print :: Show a => a -> IO () -- print a value using the show function
readLn :: Read a => IO a -- get a line and convert it to a value using the read functionОбычные инструменты рекурсии, защитные выражения (guards) и if-then-else также работают в мире IO. Вот операция IO, которая определяется с помощью охранника:
printDescription :: Int -> IO ()
printDescription n
| even n = putStrLn "even"
| n==3 = putStrLn "three"
| otherwise = print nPrelude> printDescription 2
even
Prelude> printDescription 3
three
Prelude> printDescription 5
5Вот операция, которая печатает все числа в списке с использованием рекурсии и сопоставления шаблонов:
printList :: [Int] -> IO ()
printList [] = return () -- do nothing
printList (x:xs) = do print x
printList xs -- recursionPrelude> printList [1,2,3]
1
2
3Вот два немного более сложных примера рекурсивных операций ввода-вывода. Они используют значение, создаваемое рекурсивным вызовом. Операция readAndSum n считывает номера n от пользователя и печатает их сумму. Операция ask questions показывает пользователю каждую строку в questions, считывает ответ и возвращает список всех ответов.
readAndSum :: Int -> IO Int
readAndSum 0 = return 0
readAndSum n = do
i <- readLn -- read one number
s <- readAndSum (n-1) -- recursion: read and sum rest of numbers
return (i+s) -- produce resultPrelude> s <- readAndSum 3
2
4
5
Prelude> s
11ask :: [String] -> IO [String]
ask [] = return []
ask (question:questions) = do
putStr question
putStrLn "?"
answer <- getLine -- get one answer
answers <- ask questions -- recursion: get rest of answers
return (answer:answers) -- produce resultPrelude> replies <- ask ["What is your name","How old are you"]
What is your name?
Yog-Sothoth
How old are you?
The question is meaningless
Prelude> replies
["Yog-Sothoth","The question is meaningless"]Кроме того, у нас есть некоторые IO -специфические структуры управления, а точнее, функции. Они взяты из модуля Control.Monad.
-- when b op performs op if b is true
when :: Bool -> IO () -> IO ()
-- unless b op performs op if b is false
unless :: Bool -> IO () -> IO ()
-- do something many times, collect results
replicateM :: Int -> IO a -> IO [a]
-- do something many times, throw away the results
replicateM_ :: Int -> IO a -> IO ()
-- do something for every list element
mapM :: (a -> IO b) -> [a] -> IO [b]
-- do something for every list element, throw away the results
mapM_ :: (a -> IO b) -> [a] -> IO ()
-- the same, but arguments flipped
forM :: [a] -> (a -> IO b) -> IO [b]
forM_ :: [a] -> (a -> IO b) -> IO ()Используя их, мы можем переписать наши предыдущие примеры:
printList :: [Int] -> IO ()
printList xs = mapM_ print xsreadAndSum n = do
numbers <- replicateM n readLn
return (sum numbers)ask :: [String] -> IO [String]
ask questions = do
forM questions askOne
askOne :: String -> IO String
askOne question = do
putStr question
putStrLn "?"
getLinedo и отступахЛегко столкнуться со странными проблемами углубления при использовании нотации. Вот некоторые эмпирические правила, которые помогут вам сделать это правильно.
Главное правило отступов в do: все операции блока должны начинаться в одном столбце.
Некоторые примеры этого правила:
-- This is not OK, putStrLn is way too left
foo = do y <- getLine
putStrLn y
-- This is not OK either
foo = do y <- getLine
putStrLn y
-- This is OK
foo = do y <- getLine
putStrLn y
-- This is also OK: putting a line break after do
foo = do
y <- getLine
putStrLn yСвязанное правило: если операция занимает несколько строк, последующие строки должны иметь дополнительный отступ. Иначе компилятор воспримет их как начало новой операции.
-- This is not OK, the string starts a new operation
quux = do putStrLn
"this long string"
print 1
-- This is OK
quux = do putStrLn
"this long string"
print 1Вот еще один пример, с вложенными блоками и двумя различными действительными углублениями.
-- This is OK
foo x = do quux
y <- blorg
when y (do thing
otherThing)
return 3
-- This is also OK: starting putting a line break after do, using $
foo x = do
quux
y <- blorg
when y $ do
thing
otherThing
return 3После всех этих коротких разовых примеров давайте обратимся к чему-то более длинному. Давайте напишем программу для получения аннотаций всех типов из всех файлов .hs. Мы используем операции ввода-вывода, такие как readFile и listDirectory, для чтения и поиска файлов, а также чистый код, такой как map и filter, для фактической обработки. Во-первых, вот краткий обзор библиотечных операций, которые мы используем:
-- split string into lines
lines :: String -> [String]
-- `isSuffixOf suf list` is true if list ends in suf
Data.List.isSuffixOf :: Eq a => [a] -> [a] -> Bool
-- `isInfixOf inf list` is true if inf occurs inside list
Data.List.isInfixOf :: Eq a => [a] -> [a] -> Bool
-- FilePath is just an alias for String
type FilePath = String
-- get entire contents of file
readFile :: FilePath -> IO String
-- list files in directory
System.Directory.listDirectory :: FilePath -> IO [FilePath]
-- is the given file a directory?
System.Directory.doesDirectoryExist :: FilePath -> IO BoolА вот и сама программа. Вы также можете найти его в хранилище курса как exercises/Examples/ReadTypes.hs.
module Examples.ReadTypes where
import Control.Monad (forM)
import Data.List (isInfixOf, isSuffixOf)
import System.Directory (listDirectory, doesDirectoryExist)
-- a line is a type signature if it contains :: but does not contain =
isTypeSignature :: String -> Bool
isTypeSignature s = not (isInfixOf "=" s) && isInfixOf "::" s
-- return list of types for a .hs file
readTypesFile :: FilePath -> IO [String]
readTypesFile file
| isSuffixOf ".hs" file = do content <- readFile file
let ls = lines content
return (filter isTypeSignature ls)
| otherwise = return []
-- list children of directory, prepend directory name
qualifiedChildren :: String -> IO [String]
qualifiedChildren path = do childs <- listDirectory path
return (map (\name -> path++"/"++name) childs)
-- get type signatures for all entries in given directory
-- note mutual recursion with readTypes
readTypesDir :: String -> IO [String]
readTypesDir path = do childs <- qualifiedChildren path
typess <- forM childs readTypes
return (concat typess)
-- recursively read types contained in a file or directory
-- note mutual recursion with readTypesDir
readTypes :: String -> IO [String]
readTypes path = do isDir <- doesDirectoryExist path
if isDir then readTypesDir path else readTypesFile path
-- main is the IO action that gets run when you run the program
main :: IO ()
main = do ts <- readTypes "."
mapM_ putStrLn tsМы можем запустить эту программу, перейдя в каталог exercises/Examples и запустив:
$ stack runhaskell ReadTypes.hs
deposit :: String -> Int -> Bank -> Bank
withdraw :: String -> Int -> Bank -> (Int,Bank)
runBankOp :: BankOp a -> Bank -> (a,Bank)
... and so onТочный вывод будет варьироваться в зависимости от содержимого каталога, конечно.
Вернёмся в функциональный мир. Как согласовать операции IO с чистотой и ленивостью Haskell? Значение вроде putStrLn :: String -> IO () — чистая функция, возвращающая операцию: putStrLn x всегда одинаково при одинаковом x. Иными словами, операция является чистым описанием последовательности побочных эффектов; сами эффекты возникают только при выполнении операции. При запуске программы Haskell среда выполняет одну операцию — main :: IO (). Остальные операции выполняются лишь тогда, когда связаны с main.
Когда в GHCi выражение, которое вы вводите в оценку операции, GHCi выполняет эту операцию для вас. Вот демонстрация чистоты print:
Prelude> x = print 1 -- creates operation, doesn't run it
Prelude> x -- runs the operation
1
Prelude> x -- runs it again!
1Операции — это значения, как числа, списки и функции. Поэтому можно писать код, работающий с операциями. Следующая функция принимает две операции, a и b, и возвращает новую операцию, которая спрашивает пользователя, какую из них выполнить.
choice :: IO x -> IO x -> IO x
choice a b =
do putStr "a or b? "
x <- getLine
case x of "a" -> a
"b" -> b
_ -> do putStrLn "Wrong!"
choice a bPrelude> choice (putStrLn "A!!!!") (putStrLn "B!!!!")
a or b? z
Wrong!
a or b? a
A!!!!Использование операций, заданных в качестве параметров, позволяет нам записывать такие функции, как mapM_, с которыми мы встречались ранее. Реализация представляет собой рекурсивную операцию ввода-вывода, которая принимает другую операцию ввода-вывода в качестве параметра. Концептуально сложный, но простой при чтении кода:
mapM_ :: (a -> IO b) -> [a] -> IO ()
mapM_ op [] = return () -- do nothing for an empty list
mapM_ op (x:xs) = do op x -- run operation on first element
mapM_ op xs -- run operation on rest of list, recursivelyPrelude> mapM_ print [1,2,3]
1
2
3До сих пор мы использовали только терминальный ввод-вывод (getLine, print) и работу с файлами (readFile, listDirectory). В императивных программах на Java, Python или C встречаются и другие побочные эффекты, которые нельзя выразить чистой функцией Haskell. Один из них — изменяемое состояние (mutable state). Чистая функция не может читать изменяемое состояние: иначе два вызова одной функции с одинаковыми аргументами могли бы вернуть разные значения.
Haskell типа IORef a из модуля Data.IORef является изменяемой ссылкой на значение типа a
newIORef :: a -> IO (IORef a) -- create a new IORef containing a value
readIORef :: IORef a -> IO a -- produce value contained in IORef
writeIORef :: IORef a -> a -> IO () -- set value in IORef
modifyIORef :: IORef a -> (a -> a) -> IO () -- modify value contained in IORef with a pure functionВот несколько примеров использования IORef в GHCi:
Prelude> :m +Data.IORef
Prelude Data.IORef> myRef <- newIORef "banana"
Prelude Data.IORef> readIORef myRef
"banana"
Prelude Data.IORef> writeIORef myRef "apple"
Prelude Data.IORef> readIORef myRef
"apple"
Prelude Data.IORef> modifyIORef myRef reverse
Prelude Data.IORef> readIORef myRef
"elppa"Вот пример использования IORef для суммирования значений в списке. Обратите внимание на сходство с императивной петлей.
sumList :: [Int] -> IO Int
sumList xs = do r <- newIORef 0 -- initialize r to 0
forM_ xs (\x -> modifyIORef r (x+)) -- for every xs, add it to r
readIORef r -- get last value of rИспользование IORef не требуется большую часть времени. Хаскелл предпочитает рекурсию, аргументы и обратные значения. Однако программам реального мира иногда может потребоваться один или два IORef.
Значение типа IO X — это операция IO, которая при выполнении производит значение типа X. Сами операции являются чистыми значениями; побочные эффекты возникают только при их выполнении (running).
Операции ввода-вывода могут быть объединены с использованием do -нотации:
op :: X -> IO Y
op arg = do operation -- run operation
operation2 arg -- run operation with argument
result <- operation3 arg -- run operation with argument, store result
let something = f result -- run a pure function f, store result
finalOperation -- last operation produces the return valueОперация return x - это операция, которая всегда производит значение x. Когда x :: a, return x :: IO a.
Полезные операции IO:
-- printing & reading
putStr :: String -> IO ()
putStrLn :: String -> IO ()
print :: Show a => a -> IO ()
getLine :: IO String
readLn :: Read a => IO a
-- control structures from Control.Monad
when :: Bool -> IO () -> IO () -- when b op performs op if b is true
unless :: Bool -> IO () -> IO () -- unless b op performs op if b is false
replicateM :: Int -> IO a -> IO [a] -- do something many times, collect results
replicateM_ :: Int -> IO a -> IO () -- do something many times, throw away the results
mapM :: (a -> IO b) -> [a] -> IO [b] -- do something for every list element
mapM_ :: (a -> IO b) -> [a] -> IO () -- do something for every list element, throw away the results
forM :: [a] -> (a -> IO b) -> IO [b] -- the same, but arguments flipped
forM_ :: [a] -> (a -> IO b) -> IO ()
-- files
readFile :: FilePath -> IO StringКаков тип этой операции IO?
foo x = do putStrLn x
y <- getLine
return (length y)String -> IO StringIO IntString -> IO IntIO String -> IO IntКакие из них можно использовать вместо ????
quux :: String -> IO [String]
quux q = do y <- getLine
z <- getLine
putStrLn (y++z)
????q <- getLinereturn (y++z)return [q]ans <- return [y,z]Какие значения печатает blorg [1,2,3]? То есть, какие значения x он называет print x для. Значение, произведенное blorg, не учитывается.
blorg [] = return 0
blorg (x:xs) = do m <- blorg xs
print x
return (m+x)1, 2, 31, 2, 3, 63, 2, 13, 2, 1, 6Какую из них может выполнять функция типа Int -> IO Int?
Какую из них может выполнять функция типа IO Int -> Int?
Помните функцию map для списков? Вот опять определение:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map g (x:xs) = g x : map g xsФункция g :: a -> b применяется к каждому элементу списка типа [a], а результатом становится список типа [b]; именно это делает map. Её тип можно также записать как (a -> b) -> ([a] -> [b]). Это тот же тип, поскольку оператор -> правоассоциативен. Дополнительные скобки подчёркивают, что map превращает функцию g :: a -> b в функцию map g :: [a] -> [b]. Значит, map — функция высшего порядка, преобразующая функции в функции.
Поскольку map является параметрически полиморфным, его определение не зависит от типа значений, хранящихся в списке. Таким образом, каждая функция типа a -> b преобразуется в функцию типа [a] -> [b] с использованием точно такой же логики. Используя приведенное выше определение, мы видим, что:
map (|| True) [True, True, False]
==> [True || True, True || True, False || True]
==> [True, True, True]
map (+1) [1,2,3]
==> [1 + 1, 2 + 1, 3 + 1]
==> [2, 3, 4]
map (++"1") ["1", "2", "3"]
==> ["1" ++ "1", "2" ++ "1", "3" ++ "1"]
==> ["11", "21", "31"]Важно, что map сохраняет структуру списка: его длина и взаимное расположение элементов не меняются. Общая идея показана на рисунке ниже.
Попробуем найти другие похожие функции. Значение типа Maybe a напоминает список длиной не более одного элемента. Определим отображение над Maybe. Видите сходство с определением map?
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapMaybe f Nothing = Nothing
mapMaybe f (Just x) = Just (f x)Здесь также сохраняется структура стоимости. Nothing превращается в Nothing, а Just превращается в Just. Здесь мы также можем думать о типе как (a -> b) -> (Maybe a -> Maybe b), преобразуя (или «поднимая») нормальную функцию в функцию, которая работает на Maybes.
Еще один пример: двоичные деревья.
data Tree a = Leaf | Node a (Tree a) (Tree a)
mapTree :: (a -> b) -> Tree a -> Tree b
mapTree f Leaf = Leaf
mapTree f (Node val left right) = Node (f val) (mapTree f left) (mapTree f right)Бинарное дерево может выглядеть так:
После mapTree g дерево будет выглядеть так:
FunctorТеперь у нас есть три различные функции сохранения структуры. Три похожие операции над разными типами. Можем ли мы написать класс типов, чтобы уловить это сходство?
map :: (a -> b) -> [a] -> [b]
mapMaybe :: (a -> b) -> Maybe a -> Maybe b
mapTree :: (a -> b) -> Tree a -> Tree bНаивная попытка написать класс типа сталкивается с проблемами. Если мы попытаемся абстрагироваться от Maybe c, то, похоже, мы не сможем написать правильный тип для операции с картой. Нам нужно как-то изменить параметр типа c.
class Mappable m where
mapThing :: (a -> b) -> m -> m
instance Mappable (Maybe c) where
mapThing :: (a -> b) -> Maybe c -> Maybe c
mapThing = ...К счастью, классы типа Haskell имеют функцию, которую мы не рассматривали ранее. Вы можете писать классы для конструкторов type в дополнение к типам. Что это значит? Давайте просто посмотрим на стандартный класс типа Functor, который делает то, что мы пытались сделать с нашим Mappable.
class Functor f where
fmap :: (a -> b) -> f a -> f bОбратите внимание, что параметр типа f является конструктором типа: он передается аргументами a и b в разных частях типа fmap. Рассмотрим пример с Maybe.
instance Functor Maybe where
-- In this instance, the type of fmap is:
-- fmap :: (a -> b) -> Maybe a -> Maybe b
fmap f Nothing = Nothing
fmap f (Just x) = Just (f x)Теперь у fmap нужный тип, поэтому её можно реализовать как mapMaybe. Обратите внимание: мы объявили instance Functor Maybe, а не instance Functor (Maybe a). Конкретный тип Maybe a не является функтором; им является конструктор типов Maybe.
Конструктор типа списка записывается как []; это специальный синтаксис, как и остальные списочные обозначения. Если бы тип [a] записывался как List a, конструктору [] соответствовало бы имя List.
instance Functor [] where
fmap = mapВот последний из наших примеров, например, Functor.
data Tree a = Leaf | Node a (Tree a) (Tree a)
instance Functor Tree where
fmap _ Leaf = Leaf
fmap f (Node val left right) = Node (f val) (fmap f left) (fmap f right)Примечание: термин «функтор» происходит от раздела математики под названием Теория категорий . Однако для работы с Haskell вам не нужно знать теорию категорий. По мере того, как вы прогрессируете в изучении Хаскелла, вас может заинтересовать теория категорий, и она может стать ценным источником новых идей для программирования. Теория категорий может показаться пугающей, поэтому хорошо знать, что вы можете прекрасно обходиться без нее. Теперь, когда вы видите функтор, вы можете просто подумать «что-то, что я могу нанести на карту», или, возможно, «контейнер».
Давайте немного увеличим масштаб. Когда у нас есть экземпляр Functor MyFun, мы знаем, что мы можем нанести на карту тип X в новый тип MyFun X (поскольку MyFun является конструктором типа), но также мы можем поднять функцию f, которая принимает аргумент X в функцию fmap f, которая принимает аргумент MyFun X! Таким образом, вы можете сказать, что мы отображаем как на уровне типа, так и на уровне значения.
Да, еще кое-что. После того, как вы получили название fmap, вы можете использовать его совсем немного. Для кода, который сильно использует fmap, может быть неплохо использовать его псевдоним <$>. Рассмотрим симметрию между $ и <$> в следующих примерах:
(+1) <$> [1,2,3] ==> [2,3,4]
not <$> Just False ==> Just True
reverse . tail $ "hello" ==> "olle"
reverse . tail <$> Just "hello" ==> Just "olle"
-- which is the same as
fmap (reverse . tail) (Just "hello") ==> Just "olle"Что же это за «сохранение структуры», о которой говорилось выше? Следующие два законы о функторах Ожидается, что для любого Functor пример f (хотя, к сожалению, компиляторы Haskell не могут их обеспечить):
fmap id === idfmap (f . g) === fmap f . fmap gНе волнуйтесь, если это прозвучит абстрактно! Первый закон гласит, что функтор отображает id :: a -> a в id :: f a -> f a. (id является функцией идентификации, что означает id x = x.) Давайте будем конкретны и посмотрим, как это работает для списка [1,2,3]:
fmap id [1,2,3] ==> map id [1,2,3]
==> map id (1:[2,3])
==> id 1 : map id [2,3]
==> 1 : map id [2,3]
==> 1 : id 2 : map id [3]
==> 1 : 2 : id 3 : map id []
==> 1 : 2 : 3 : []
=== [1,2,3]С другой стороны,
id [1,2,3] ==> [1,2,3]Следовательно, результат fmap id [1,2,3] То же самое, что результат id [1,2,3] Таким образом, в данном случае действует первый закон функтора. Нетрудно показать, что первый закон о функторах относится к любому списку.
Первый закон функтора очень прост. Если передать в fmap функцию id, которая ничего не меняет, то и полученная функция fmap id ничего не изменит. Иными словами, применение fmap сохраняет структуру функтора.
Как насчет второго закона о функторах? Для списков рассмотрим, что происходит, если мы fmap функция negate.(*2) (помните, negate карты x на -x и (*2) умножает свой аргумент на 2):
fmap (negate.(*2)) [1,2,3] ==> map (negate.(*2)) [1,2,3]
==> (negate.(*2)) 1 : map (negate.(*2)) [2,3]
==> negate (1 * 2) : map (negate.(*2)) [2,3]
==> -2 : map (negate.(*2)) [2,3]
==> -2 : (negate.(*2)) 2 : map (negate.(*2)) [3]
==> -2 : -4 : map (negate.(*2)) [3]
==> -2 : -4 : (negate.(*2)) 3 : map (negate.(*2)) []
==> -2 : -4 : -6 : []
==> [-2,-4,-6]Рассмотрим правую сторону второго закона функтора в данном случае:
(fmap negate . fmap (*2)) [1,2,3] ==> (map negate . map (*2)) [1,2,3]
==> map negate (map (*2) [1,2,3])
==> map negate [2,4,6]
==> [-2,-4,-6]Второй закон функтора оказывается в данном конкретном случае. На самом деле, он действует во всех случаях (Упражнения!).
В общем, второй закон функтора гласит, что сначала составляя две функции, а затем применяя fmap должен давать тот же результат, что и fmap на этих функциях и затем составление результирующей функции. Другими словами, порядок применения fmap Сочинение не имеет значения. (Об этих двух операциях говорится: ездить.)
Существуют также функции высшего порядка, которые не удовлетворяют законам функтора. Рассмотрим функцию badMap:
badMap :: (a -> b) -> [a] -> [b]
badMap f [] = []
badMap f (x:y:xs) = f x : badMap f xs
badMap f (x:xs) = f x : badMap f xsЭта функция нарушает первый закон функтора. Например:
badMap id [1,2,3] ==> badMap id (1:2:[3])
==> id 1 : badMap id [3]
==> 1 : badMap id [3]
==> 1 : badMap id (3:[])
==> 1 : id 3 : badMap id []
==> 1 : 3 : []
==> [1,3]применять badMap id В список [1,2,3] Изменить список как элемент 2 Его бросили.
Как уже упоминалось, компиляторы Haskell не могут определить, подчиняется ли функтор своим законам или нет. Компилятор Haskell с радостью примет экземпляр Functor [], который использовал badMap вместо map в качестве реализации fmap. Это ограничение типа системы Haskell. Существуют такие технологии, как LiquidHaskell или языки с зависимой типизацией, такие как Agda, Idris, Coq или Lean, которые могут на самом деле обеспечивать соблюдение законов о функторах, чтобы незаконные функторные экземпляры не компилировались. Однако эти технологии выходят за рамки этого курса.
Напомним: Functor — класс конструкторов типов. Если попытаться определить экземпляр Functor для обычного типа, мы получим ошибку:
Prelude> instance Functor Int where
<interactive>:1:18: error:
• Expected kind ‘* -> *’, but ‘Int’ has kind ‘*’
• In the first argument of ‘Functor’, namely ‘Int’
In the instance declaration for ‘Functor Int’В сообщении об ошибке упомянуты роды типов (kinds) — своего рода «типы типов». Типы, способные содержать значения, например Int, Bool или Maybe Int, имеют род *. Род конструктора типа похож на тип функции: например, род Maybe — * -> *. Это означает, что конструктор Maybe нужно применить к типу рода *, чтобы получить тип рода *.
Мы можем попросить GHCi для типов:
Prelude> :kind Int
Int :: *
Prelude> :kind Maybe
Maybe :: * -> *
Prelude> :kind Maybe Int
Maybe Int :: *Если мы попросим GHCi предоставить нам информацию о классе Functor, мы увидим, что экземпляры Functor должны иметь вид * -> *:
Prelude> :info Functor
class Functor (f :: * -> *) where
fmap :: (a -> b) -> f a -> f b
...Вот некоторые примеры еще более сложных видов.
-- multiple type parameters
Prelude> :kind Either
Either :: * -> * -> *
Prelude> data Either3 a b c = Left a | Middle b | Right c
Prelude> :kind Either3
Either3 :: * -> * -> * -> *
-- a type parameter of kind *->*
Prelude> data IntInside f = IntInside (f Int)
Prelude> :kind IntInside
IntInside :: (* -> *) -> *В программировании Haskell вы не столкнетесь с видами, но иногда вы увидите сообщения об ошибках, которые говорят о видах, поэтому хорошо знать, что они собой представляют.
FoldableМы кратко рассмотрели класс Foldable, который встречается во многих типах подписей основных функций, в части 1. Например:
length :: Foldable t => t a -> Int
sum :: (Foldable t, Num a) => t a -> a
minimum :: (Foldable t, Ord a) => t a -> a
foldMap :: (Foldable t, Monoid m) => (a -> m) -> t a -> mИз этих сигнатур видно, что Foldable, как и Functor, — класс конструкторов типов (то есть сущностей рода * -> *). По сути, Foldable описывает структуры, которые можно свернуть. Минимальное определение класса могло бы выглядеть так:
class Foldable (t :: *->*) where
foldr :: (a -> b -> b) -> b -> t a -> bТем не менее, по причинам производительности, класс содержит много методов (вы можете увидеть их сами, проверив :info Foldable в GHCi!), но когда мы определяем экземпляр для Foldable, достаточно определить только foldr.
На Foldable можно смотреть как на класс структур, элементы которых обрабатываются слева направо (left-to-right). Иными словами, если Functor — класс контейнеров, то Foldable — класс упорядоченных контейнеров.
В качестве примера давайте реализуем Functor и Foldable для нашего собственного парного типа.
data Pair a = Pair a a
deriving Show
instance Functor Pair where
-- fmap f applies f to all values
fmap f (Pair x y) = Pair (f x) (f y)
instance Foldable Pair where
-- just like applying foldr over a list of length 2
foldr f initialValue (Pair x y) = f x (f y initialValue)
-- an example function that uses both instances
doubleAndCount :: (Functor f, Foldable f) => f Int -> Int
doubleAndCount = sum . fmap (*2)Теперь мы можем использовать Pair практически везде, где мы можем использовать список:
fmap (+1) (Pair 3 6) ==> Pair 4 7
fmap (+1) [3,6] ==> [4,7]
foldr (*) 1 (Pair 3 6) ==> 18
foldr (*) 1 [3,6] ==> 18
length (Pair 3 6) ==> 2
length [3,6] ==> 2
minimum (Pair 3 6) ==> 3
minimum [3,6] ==> 3
doubleAndCount (Pair 3 6) ==> 18
doubleAndCount [3,6] ==> 18Другие типы Foldable включают Data.Map и Data.Array.
Таким образом, подытоживая, функтор представляет собой конструктор типа f и соответствующий экземпляр Functor f, такой, что fmap удовлетворяет двум законам функтора. Эти законы утверждают, что fmap должен сохранять функцию идентификации и распределять по функциональному составу. Более неофициально, fmap поднимает функцию g :: a -> b, работающую на значениях, до одной, работающей на контейнерах: fmap g :: f a -> f b. В основном все хорошо функционирующие структуры данных в Haskell являются функторами.
Что такое fmap?
a -> b -> f a -> f b(a -> b) -> f a -> f bFunctor f => a -> b -> f a -> f bFunctor f => (a -> b) -> f a -> f bКакой фрагмент кода завершает следующий экземпляр Functor?
data Container x = Things x [x]
instance Functor Container where
????fmap f (Things x ys) = Things (f x) [f x]fmap f (Things x ys) = Things (f x) (map f ys)fmap f (Things x ys) = Things (f x) ysfmap f (Things x ys) = f (Things x ys)Что такое [a]?
** -> *[a]Что такое Foo?
data Foo x = FooConst** -> *FooЧто такое Bar?
data Bar = Baz | Qux Int** -> *BarКакова стоимость foldr (-) 1 (Just 2)?
Just -1Just 1Какой фрагмент кода завершает следующий экземпляр Foldable?
data Container x = Things x [x]
instance Foldable Container where
????foldr f z (Things x ys) = f x zfoldr f z (Things x ys) = foldr f x ysfoldr f z (Things x ys) = f x (foldr f z ys)foldr f z (Things x ys) = foldr f z (x:ys)В этой лекции мы рассмотрим концепцию monad на нескольких примерах. Теперь вы должны быть знакомы со всеми функциями Haskell, необходимыми для понимания монад.
Монады — это очень сложная тема в программировании, которая частично связана со странной терминологией, частично с плохими учебными пособиями, а частично с попыткой понять монады слишком рано при изучении Хаскелла. Монады вводятся в конце курса в попытке облегчить их понимание.
Если вы находите эту лекцию трудной, не отчаивайтесь, многие другие нашли эту тему трудно. Есть много продуктивных программистов Haskell, которым удалось понять монады, поэтому задача не безнадежна.
И последнее предостережение: монады, как и функторы, происходят из раздела математики, называемого теорией категорий. Однако важно подчеркнуть: чтобы понять монады в программировании на Haskell, не нужно знать теорию категорий и даже интересоваться ею. С объектно-ориентированным или функциональным программированием можно работать без математической теории объектов или функций; так же можно использовать монады, не зная лежащей за ними математики. Теория категорий может быть полезна функциональному программисту, но не обязательна.
При работе со многими значениями Maybe код имеет тенденцию становиться немного беспорядочным. Давайте рассмотрим некоторые примеры. Во-первых, мы объединяем некоторые функции возврата Maybe String. Обратите внимание на вложенный case, который нам нужен в stealSecret: писать не весело.
-- Try to login with a password.
-- `Just username` on success, `Nothing` otherwise.
login :: String -> Maybe String
login "f4bulous!" = Just "unicorn73"
login "swordfish" = Just "megahacker"
login _ = Nothing
-- Get a secret associated with a user.
-- Not all users have secrets.
secret :: String -> Maybe String
secret "megahacker" = Just "I like roses"
secret _ = Nothing
-- Login and return the user's secret, if any
stealSecret :: String -> Maybe String
stealSecret password =
case login password of
Nothing -> Nothing
Just user -> case secret user of
Nothing -> Nothing
Just s -> Just ("Stole secret: "++s)stealSecret "swordfish" ==> Just "Stole secret: I like roses"
stealSecret "f4bulous!" ==> Nothing
stealSecret "wrong_password" ==> NothingДалее мы изменим список пар. Мы используем Maybe - функция возврата lookup Из прелюдии. Здесь у нас есть "если" внутри дела, а не вложенный случай.
-- Get the value corresponding to a key from a key-value list.
lookup :: (Eq a) => a -> [(a, b)] -> Maybe b-- Set the value of key to val in the given key-value list,
-- but only if val is larger than the current value!
increase :: Eq a => a -> Int -> [(a,Int)] -> Maybe [(a,Int)]
increase key val assocs =
case lookup key assocs
of Nothing -> Nothing
Just x -> if (val < x)
then Nothing
else Just ((key,val) : delete (key,x) assocs)Этот тип кода довольно распространен и обычно повторяет один и тот же шаблон: если какой-либо промежуточный результат является Nothing, то весь результат является Nothing. Давайте попробуем упростить написание кода, определив оператора chaining ?>. Оператор цепей принимает результат и следующий шаг вычисления, и выполняет следующий шаг только в том случае, если результатом было значение Just.
(?>) :: Maybe a -> (a -> Maybe b) -> Maybe b
-- if we failed, don't even bother running the next step:
Nothing ?> _ = Nothing
-- otherwise run the next step:
Just x ?> f = f xОператор цепей красиво обтекает наши примеры. Обратите внимание, как мы можем определить простые вспомогательные функции, которые выполняют один шаг вычислений вместо написания одного большого выражения.
stealSecret :: String -> Maybe String
stealSecret password =
login password ?>
secret ?>
decorate
where decorate s = Just ("Stole secret: "++s)increase :: Eq a => a -> Int -> [(a,Int)] -> Maybe [(a,Int)]
increase key val assocs =
lookup key assocs ?>
check ?>
buildResult
where check x
| val < x = Nothing
| otherwise = Just x
buildResult x = Just ((key,val) : delete (key,x) assocs)Вот еще один пример: индексация безопасного списка, построенная из safeHead и safeTail:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x
safeTail :: [a] -> Maybe [a]
safeTail [] = Nothing
safeTail (x:xs) = Just xs
safeThird :: [a] -> Maybe a
safeThird xs = safeTail xs ?> safeTail ?> safeHead
safeNth :: Int -> [a] -> Maybe a
safeNth 0 xs = safeHead xs
safeNth n xs = safeTail xs ?> safeNth (n-1)safeThird [1,2,3,4]
==> Just 3
safeThird [1,2]
==> Nothing
safeNth 5 [1..10]
==> Just 6
safeNth 11 [1..10]
==> NothingP.S. Обратите внимание: ?> левоассоциативен, как и большинство операторов Haskell по умолчанию. Поэтому op ?> f ?> g означает (op ?> f) ?> g. Выражение op ?> (f ?> g) даже не пройдёт проверку типов.
Примечание: этот оператор ?> выражает шаблон if-result, который очень распространен в других языках. Вот как можно написать op val ?> f на Python и Java.
# Python
x = op(val)
if x:
f(x)// Java
Object x = op(val);
if (x != null) {
f(x);
}Разница между шаблоном if-result и нашей ?> заключается в том, что мы используем значение Nothing для явного сбоя сигнала, вместо того, чтобы полагаться на тот факт, что любая переменная может быть None (или False) в Python, или что любая ссылка Object может быть null в Java.
Рассмотрим связывание ещё на одном примере — логировании. Тип Logger хранит значение и список сообщений журнала, созданных вычислением этого значения.
-- Logger definition
data Logger a = Logger [String] a deriving Show
getVal :: Logger a -> a
getVal (Logger _ a) = a
getLog :: Logger a -> [String]
getLog (Logger s _) = s
-- Primitive operations:
nomsg :: a -> Logger a
nomsg x = Logger [] x -- a value, no message
annotate :: String -> a -> Logger a
annotate s x = Logger [s] x -- a value and a message
msg :: String -> Logger ()
msg s = Logger [s] () -- just a messageВот функция login, которая регистрирует некоторые детали об именах пользователей и паролях, которые она обрабатывает. Обратите внимание, как мы сталкиваемся со сложным кодом в login, когда нам нужно обрабатывать несколько значений Logger.
validateUser :: String -> Logger Bool
validateUser "paul.atreides" = annotate "Valid user" True
validateUser "ninja" = nomsg True
validateUser u = annotate ("Invalid user: "++u) False
checkPassword :: String -> String -> Logger Bool
checkPassword "paul.atreides" "muad'dib" = annotate "Password ok" True
checkPassword "ninja" "" = annotate "Password ok" True
checkPassword _ pass = annotate ("Password wrong: "++pass) False
login :: String -> String -> Logger Bool
login user password =
let validation = validateUser user
in if (getVal validation)
then let check = checkPassword user password
in Logger (getLog validation ++ getLog check) (getVal check)
else validationlogin "paul.atreides" "muad'dib"
==> Logger ["Valid user","Password ok"] True
login "paul.atreides" "arrakis"
==> Logger ["Valid user","Password wrong: arrakis"] False
login "ninja" ""
==> Logger ["Password ok"] True
login "leto.atreides" "paul"
==> Logger ["Invalid user: leto.atreides"] FalseДавайте попробуем упростить этот код, определив оператора цепей для Logger. При выполнении нескольких операций Logger важно сохранить все журналы. Вот оператор цепей #> и пример того, как его можно использовать для регистрации некоторых арифметических вычислений.
(#>) :: Logger a -> (a -> Logger b) -> Logger b
Logger la a #> f = let Logger lb b = f a -- feed value to next step
in Logger (la++lb) b -- bundle result with all messages-- square a number and log a message about it
square :: Int -> Logger Int
square val = annotate (show val ++ "^2") (val^2)
-- add 1 to a number and log a message about it
add :: Int -> Logger Int
add val = annotate (show val ++ "+1") (val+1)
-- double a number and log a message about it
double :: Int -> Logger Int
double val = annotate (show val ++ "*2") (val*2)
-- compute the expression 2*(x^2+1) with logging
compute :: Int -> Logger Int
compute x =
square x
#> add
#> doublecompute 3
==> Logger ["3^2","9+1","10*2"] 20Мы можем немного упростить login, используя #>. Обратите внимание, что нам не нужно беспокоиться о объединении журналов вместе. Также обратите внимание, как мы используем выражение лямбда вместо определения функции помощника.
login :: String -> String -> Logger Bool
login user password =
validateUser user
#>
\valid -> if valid then checkPassword user password
else nomsg FalseЧтобы немного улучшить ситуацию, давайте используем Logger в функции обработки рекурсивных списков. Здесь представлена версия filter. Обратите внимание, как код цепляет сообщение журнала перед рекурсивным вызовом, чтобы сохранить порядок записей журнала приятным.
-- sometimes you don't need the previous value:
(##>) :: Logger a -> Logger b -> Logger b
Logger la _ ##> Logger lb b = Logger (la++lb) b
filterLog :: (Eq a, Show a) => (a -> Bool) -> [a] -> Logger [a]
filterLog f [] = nomsg []
filterLog f (x:xs)
| f x = msg ("keeping "++show x) ##> filterLog f xs #> (\xs' -> nomsg (x:xs'))
| otherwise = msg ("dropping "++show x) ##> filterLog f xsfilterLog (>0) [1,-2,3,-4,0]
==> Logger ["keeping 1","dropping -2","keeping 3","dropping -4","dropping 0"] [1,3]В предыдущем примере мы просто написали некоторое состояние (журнал). Иногда нам нужны вычисления, которые меняют какое-то общее состояние. Рассмотрим счета в небольшом банке. Сначала мы определим тип данных для состояния банка: остатки всех счетов, как карту от имени счета до баланса.
import qualified Data.Map as Map
data Bank = Bank (Map.Map String Int)
deriving ShowВот как мы можем внести деньги на счет. Для изменения карты мы используем функцию adjust от Data.Map.
-- Apply a function to one value in a map
Map.adjust :: Ord k => (a -> a) -> k -> Map.Map k a -> Map.Map k adeposit :: String -> Int -> Bank -> Bank
deposit accountName amount (Bank accounts) =
Bank (Map.adjust (\x -> x+amount) accountName accounts)Вывод денег немного сложнее, так как мы хотим иметь дело с некоторыми особыми случаями, такими как счет, который не существует, или счет, на котором не хватает денег. Мы используем функцию библиотеки findWithDefault, чтобы помочь нам.
-- Fetch the value corresponding to a key from a map,
-- or a default value in case the key does not exist
Map.findWithDefault :: Ord k => a -> k -> Map.Map k a -> awithdraw :: String -> Int -> Bank -> (Int,Bank)
withdraw accountName amount (Bank accounts) =
let -- balance is 0 for a nonexistant account
balance = Map.findWithDefault 0 accountName accounts
-- can't withdraw over balance
withdrawal = min amount balance
newAccounts = Map.adjust (\x -> x-withdrawal) accountName accounts
in (withdrawal, Bank newAccounts)Наконец, напишите функцию, которая берет не более 100 денег с одного счета, делит деньги пополам и вносит их на два счета. Обратите внимание на то, как мы должны тщательно обработать различные версии банка, bank, bank1, bank2 и bank3, чтобы убедиться, что все транзакции происходят в правильном порядке.
share :: String -> String -> String -> Bank -> Bank
share from to1 to2 bank =
let (amount,bank1) = withdraw from 100 bank
half = div amount 2
-- carefully preserve all money, even if amount was an odd number
rest = amount-half
bank2 = deposit to1 half bank1
bank3 = deposit to2 rest bank2
in bank3share "wotan" "siegfried" "brunhilde"
(Bank (Map.fromList [("brunhilde",0),("siegfried",0),("wotan",1000)]))
==> Bank (Map.fromList [("brunhilde",50),("siegfried",50),("wotan",900)])
share "wotan" "siegfried" "brunhilde"
(Bank (Map.fromList [("brunhilde",0),("siegfried",0),("wotan",91)]))
==> Bank (Map.fromList [("brunhilde",46),("siegfried",45),("wotan",0)])Такой код часто появляется в Haskell, когда вы делаете последовательные обновления до одного значения, а также выполняете некоторые другие вычисления на стороне. Это легко сделать ошибку, и система типа не поможет вам, если вы, например, повторно использовать bank1 ценность. Давайте перепишем share Для этого не нужно обращаться к самому банку. Мы можем снова использовать ту же самую цепную идею для достижения этой цели.
-- `BankOp a` is an operation that transforms a Bank value,
-- while returning a value of type `a`
data BankOp a = BankOp (Bank -> (a,Bank))
-- running a BankOp on a Bank
runBankOp :: BankOp a -> Bank -> (a,Bank)
runBankOp (BankOp f) bank = f bank
-- Running one BankOp after another
(+>>) :: BankOp a -> BankOp b -> BankOp b
op1 +>> op2 = BankOp combined
where combined bank = let (_,bank1) = runBankOp op1 bank
in runBankOp op2 bank1
-- Running a parameterized BankOp, using the value returned
-- by a previous BankOp. The implementation is a bit tricky
-- but it's enough to understand how +> is used for now.
(+>) :: BankOp a -> (a -> BankOp b) -> BankOp b
op +> parameterized = BankOp combined
where combined bank = let (a,bank1) = runBankOp op bank
in runBankOp (parameterized a) bank1
-- Make a BankOp out of deposit.
-- There is no return value so we use ().
depositOp :: String -> Int -> BankOp ()
depositOp accountName amount = BankOp depositHelper
where depositHelper bank = ((), deposit accountName amount bank)
-- Make a BankOp out of withdraw. Note how
-- withdraw accountName amount :: Bank -> (Int,Bank)
-- is almost a BankOp already!
withdrawOp :: String -> Int -> BankOp Int
withdrawOp accountName amount = BankOp (withdraw accountName amount)Давайте посмотрим, как работает цепочка с этими банковскими операциями.
Prelude> bank = Bank (Map.fromList [("edsger",10),("grace",50)])
-- Running a number of operations using +>>
Prelude> runBankOp (depositOp "edsger" 1) bank
((),Bank (fromList [("edsger",11),("grace",50)]))
Prelude> runBankOp (depositOp "edsger" 1 +>> depositOp "grace" 1) bank
((),Bank (fromList [("edsger",11),("grace",51)]))
Prelude> runBankOp (depositOp "edsger" 1 +>> depositOp "grace" 1 +>> withdrawOp "edsger" 11) bank
(11,Bank (fromList [("edsger",0),("grace",51)]))
-- Using +> to implement a transfer from one account to the other:
Prelude> runBankOp (withdrawOp "edsger" 5 +> depositOp "grace") bank
((),Bank (fromList [("edsger",5),("grace",55)]))
Prelude> runBankOp (withdrawOp "edsger" 100 +> depositOp "grace") bank
((),Bank (fromList [("edsger",0),("grace",60)]))Значение типа BankOp описывает процесс, преобразующий состояние банка. Начальное состояние передаётся через runBankOp. Это разумно: преобразования BankOp можно компоновать, в отличие от самих состояний Bank. Явный вызов runBankOp также помогает различать описание операций и их выполнение.
Теперь, когда мы знакомы с манипулированием значениями BankOp, мы можем реализовать share как BankOp. Мы реализуем помощника distributeOp, чтобы сделать код немного аккуратнее.
-- distribute amount to two accounts
distributeOp :: String -> String -> Int -> BankOp ()
distributeOp to1 to2 amount =
depositOp to1 half
+>>
depositOp to2 rest
where half = div amount 2
rest = amount - half
shareOp :: String -> String -> String -> BankOp ()
shareOp from to1 to2 =
withdrawOp from 100
+>
distributeOp to1 to2runBankOp (shareOp "wotan" "siegfried" "brunhilde")
(Bank (Map.fromList [("brunhilde",0),("siegfried",0),("wotan",1000)]))
==> ((),Bank (Map.fromList [("brunhilde",50),("siegfried",50),("wotan",900)]))
runBankOp (shareOp "wotan" "siegfried" "brunhilde")
(Bank (Map.fromList [("brunhilde",0),("siegfried",0),("wotan",91)]))
==> ((),Bank (Map.fromList [("brunhilde",46),("siegfried",45),("wotan",0)]))Это было довольно чисто, не так ли? Нам вообще не нужно упоминать банк, мы можем почти программировать как на императивном языке, оставаясь при этом полностью чистыми.
Вы можете найти весь этот код в репозитории курса под exercises/Examples/Bank.hs.
Теперь мы видели три различных типа с цепной операцией:
(?>) :: Maybe a -> (a -> Maybe b) -> Maybe b
(#>) :: Logger a -> (a -> Logger b) -> Logger b
(+>) :: BankOp a -> (a -> BankOp b) -> BankOp bКак и ранее с map и Functor, есть класс типов, который захватывает этот шаблон. Отметим, что Monad является классом для конструкторов type, как и Functor.
class Monad m where
(>>=) :: m a -> (a -> m b) -> m bСуществуют дополнительные операции в Monad также:
-- lift a normal value into the monad
return :: a -> m a
-- simpler chaining (like our ##>)
(>>) :: m a -> m b -> m b
a >> b = a >>= \_ -> b -- remember: _ means ignored argumentНапомним, класс Functor обобщает операцию map. Аналогично, класс Monad обобщает операцию связывания в цепочку.
fmap :: Functor f => (a->b) -> f a -> f b
(>>=) :: Monad m => m a -> (a -> m b) -> m bВыражение operation >>= next принимает монадическую операцию operation :: m a и делает некоторые дальнейшие вычисления со значением, которое он производит с использованием next :: a -> m b. Если это кажется слишком абстрактным, просто вспомните, как работает цепь для Maybe:
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
-- if we failed, don't even bother running the next step
Nothing >>= _ = Nothing
-- otherwise run the next step
Just x >>= f = f xВот полный экземпляр Monad для Maybe и некоторые примеры.
instance Monad Maybe where
(Just x) >>= k = k x
Nothing >>= _ = Nothing
(Just _) >> k = k
Nothing >> _ = Nothing
return x = Just xJust 1 >>= \x -> return (x+1)
==> Just 2
Just "HELLO" >>= (\x -> return (length x)) >>= (\x -> return (x+1))
==> Just 6
Just "HELLO" >>= \x -> Nothing
==> Nothing
Just "HELLO" >> Just 2
==> Just 2
Just 2 >> Nothing
==> NothingВот примеры stealSecret и increase, переписанные с операциями монады. Изменения коснулись ?> в >>= и Just в return.
stealSecret :: String -> Maybe String
stealSecret password =
login password >>=
secret >>=
decorate
where decorate s = return ("Stole secret: "++s)-- Set the value of key to val in the given key-value list,
-- but only if val is larger than the current value!
increase :: Eq a => a -> Int -> [(a,Int)] -> Maybe [(a,Int)]
increase key val assocs =
lookup key assocs >>=
check >>=
buildResult
where check x
| val < x = Nothing
| otherwise = return x
buildResult x = return ((key,val) : delete (key,x) assocs)doВот пример того, как может выглядеть сложная операция монады.
f = op1 >>= continue
where continue x = op2 >> op3 >>= continue2 x
continue2 x y = op4 >> op5 x yДавайте посмотрим, что происходит, когда мы немного трансформируем этот код. Прежде всего, давайте введем определения.
f = op1 >>= (\x ->
op2 >>
op3 >>= (\y ->
op4 >>
op5 x y))Из-за лямбда-выражений, продолжающихся до конца выражения, мы можем опустить скобки. Давайте также по-другому отступим.
f = op1 >>= \x ->
op2 >>
op3 >>= \y ->
op4 >>
op5 x yТеперь можно заметить сходство с обозначением do. Блок do ниже на самом деле тот же код!
f = do x <- op1
op2
y <- op3
op4
op5 x yДля уточнения, нотация do является просто более приятным синтаксисом для операций монады (>>= и >>) и лямбда. Вот как нотация трансформируется в монадные операции. Записка! Определение является рекурсивным.
do x <- op a ~~~> op a >>= \x -> do ...
...do op a ~~~> op a >> do ...
...do let x = expr ~~~> let x = expr in do ...
...do finalOp ~~~> finalOpВот safeNth с помощью обозначения:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x
safeTail :: [a] -> Maybe [a]
safeTail [] = Nothing
safeTail (x:xs) = Just xs
safeNth :: Int -> [a] -> Maybe a
safeNth 0 xs = safeHead xs
safeNth n xs = do t <- safeTail xs
safeNth (n-1) tВот increase в последний раз, сейчас с нотацией
-- Set the value of key to val in the given key-value list,
-- but only if val is larger than the current value!
increase :: Eq a => a -> Int -> [(a,Int)] -> Maybe [(a,Int)]
increase key val assocs =
do oldVal <- lookup key assocs
check oldVal
return ((key,val) : delete (key,oldVal) assocs)
where check x
| val < x = Nothing
| otherwise = return xМы должны быть в состоянии написать Monad Например, Logger себя, устанавливая >>= то #> Однако из-за Последние изменения в языке Haskell Мы должны реализовать Functor и Applicative инстанциям разрешается осуществлять Monad Например. Functor Мы уже встречались, но что Applicative Мы узнаем позже. Давайте реализуем примеры:
import Control.Monad
data Logger a = Logger [String] a
deriving Show
msg :: String -> Logger ()
msg s = Logger [s] ()
-- The Functor instance just maps over the stored value
instance Functor Logger where
fmap f (Logger log x) = Logger log (f x)
-- This is an Applicative instance that works for any
-- monad, you can just ignore it for now. We'll get back
-- to Applicative later.
instance Applicative Logger where
pure = return
(<*>) = ap
-- Finally, the Monad instance
instance Monad Logger where
return x = Logger [] x
Logger la a >>= f = Logger (la++lb) b
where Logger lb b = f aНам больше не нужна операция nomsg, потому что это просто return. Мы также можем реализовывать операцию annotate с использованием операций монады.
nomsg :: a -> Logger a
nomsg x = return x
annotate :: String -> a -> Logger a
annotate s x = msg s >> return xВот примеры compute и filterLog, переписанные с помощью донотации. Обратите внимание, насколько хорош filterLog с донотацией.
compute x = do
a <- annotate "^2" (x*x)
b <- annotate "+1" (a+1)
annotate "*2" (b*2)
filterLog :: (Show a) => (a -> Bool) -> [a] -> Logger [a]
filterLog f [] = return []
filterLog f (x:xs)
| f x = do msg ("keeping "++show x)
xs' <- filterLog f xs
return (x:xs')
| otherwise = do msg ("dropping "++show x)
filterLog f xscompute 3
==> Logger ["^2","+1","*2"] 20
filterLog (>0) [1,-2,3,-4,0]
==> Logger ["keeping 1","dropping -2","keeping 3","dropping -4","dropping 0"] [1,3]Монада Haskell State является обобщенной версией нашего типа BankOp. Тип State параметризируется двумя типами, первый из которых является типом состояния, а второй типом произведенной стоимости. State Bank a будет эквивалентен нашему BankOp a. Монаду State можно найти в модуле Control.Monad.Trans.State пакета transformers. Вот упрощенная реализация State.
data State s a = State (s -> (a,s))
runState (State f) s = f s
-- operation that overwrites the state (and produces ())
put :: s -> State s ()
put state = State (\oldState -> ((),state))
-- operation that produces the current state
get :: State s s
get = State (\state -> (state,state))
-- operation that modifies the current state with a function (and produces ())
modify :: (s -> s) -> State s ()
modify f = State (\state -> ((), f state))
-- Functor and Applicative instances skipped
instance Monad (State s) where
return x = State (\s -> (x,s))
op >>= f = State h
where h state0 = let (val,state1) = runState op state0
op2 = f val
in runState op2 state1Обратите внимание на объявление экземпляра Monad (State s). Здесь используется частично применённый конструктор типа, поскольку экземпляр Monad можно объявить только для конструктора, принимающего ещё один параметр типа. Это станет понятнее, если сравнить роли m, Maybe и State в приведённых ниже типах >>=.
class Monad m where
(>>=) :: m a -> (a -> m b) -> m b
instance Monad Maybe where
(>>=) :: Maybe a -> (a -> Maybe b) -> Maybe b
instance Monad (State s) where
(>>=) :: State s a -> (a -> State s b) -> State s bРассмотрим несколько примеров работы с State. Для начала рассмотрим вычисления типа State Int a, которые представляют собой работу с простым счетчиком.
-- adds i to the value of the counter
add :: Int -> State Int ()
add i = do old <- get
put (old+i)runState (add 1 >> add 3 >> add 5 >> add 6) 0
==> ((),15)example :: State Int Int
example = do add 3 -- increment state by 3
value <- get -- value is current state, i.e. initial+3
add 1000 -- increment state by 1000
put (value + 1) -- overwrite state with value+1, i.e. initial+4
return value -- produce value, i.e. intial+3runState example 1
==> (4,5) -- initial is 1, state is initial+4=5, produces initial+3=4Обратите внимание: значение типа State s a представляет процесс, преобразующий состояние, — подобно BankOp. Начальное состояние передаётся через runState. Необходимость явно вызывать runState помогает различать описание операций и их выполнение.
Состояние может заменить параметр аккумулятора при обработке списка. Вот два примера: нахождение самого большого элемента списка и нахождение значений в списке, которые происходят непосредственно после 0.
findLargest :: Ord a => [a] -> State a ()
findLargest [] = return ()
findLargest (x:xs) = do
modify (\y -> max x y) -- update state with max of current value and previous largest value
findLargest xs -- process rest of listrunState (findLargest [1,2,7,3]) 0 ==> ((),7)-- store the given value in the state list
remember :: a -> State [a] ()
remember x = modify (x:)
valuesAfterZero :: [Int] -> ((),[Int])
valuesAfterZero xs = runState (go xs) []
where go :: [Int] -> State [Int] ()
go (0:y:xs) = do remember y
go (y:xs)
go (x:xs) = go xs
go [] = return ()valuesAfterZero [0,1,2,3,0,4,0,5,0,0,6]
==> ((),[6,0,5,4,1])Записка! Таким образом, фактическое осуществление State Монада не имеет State Конструктор, как и наш упрощенный пример. Если вы хотите превратить функцию в State Вместо этого используйте этот помощник:
state :: (s -> (a, s)) -> State s a
mapMУправляющие конструкции из лекции об IO работают во всех монадах. Ниже приведены их настоящие типы.
when :: Monad m => Bool -> m () -> m () -- conditional operation
unless :: Monad m => Bool -> m () -> m () -- same, but condition is flipped
replicateM :: Monad m => Int -> m a -> m [a] -- do something many times
replicateM_ :: Monad m => Int -> m a -> m () -- same, but ignore the results
mapM :: Monad m => (a -> m b) -> [a] -> m [b] -- do something on a list's elements
mapM_ :: Monad m => (a -> m b) -> [a] -> m () -- same, but ignore the results
forM :: Monad m => [a] -> (a -> m b) -> m [b] -- mapM but arguments reversed
forM_ :: Monad m => [a] -> (a -> m b) -> m () -- same, but ignore the resultsКак мы видим здесь, мы можем использовать mapM по всем монадам, которые мы встречали до сих пор:
mapM (\x -> if (x>0) then Just (x-1) else Nothing) [1,2,3] ==> Just [0,1,2]
mapM (\x -> if (x>0) then Just (x-1) else Nothing) [1,0,3] ==> Nothing
mapM (\x -> msg "increment" >> msg (show x) >> return (x+1)) [1,2,3]
==> Logger ["increment","1","increment","2","increment","3"] [2,3,4]
runState (mapM (\x -> modify (x+) >> return (x+1)) [1,2,3]) 0
==> ([2,3,4],6)Еще несколько примеров:
safeHead :: [a] -> Maybe a
safeHead [] = Nothing
safeHead (x:xs) = Just x
firsts :: [[a]] -> Maybe [a]
firsts xs = forM xs safeHeadfirsts [[1,2,3],[4,5],[6]] ==> Just [1,4,6]
firsts [[1,2,3],[],[6]] ==> Nothing-- an abbreviated version of an example from the last section
findLargest :: Ord a => [a] -> State a ()
findLargest xs = mapM_ update xs
where update x = modify (\y -> max x y)runState (findLargest [1,2,7,3]) 0 ==> ((),7)let increment = modify (+1) >> get
ops = replicateM 4 increment
in runState ops 0
==> ([1,2,3,4],4)Вот filter, реализуемый с использованием монады State:
rememberElements :: (a -> Bool) -> [a] -> State [a] ()
rememberElements f xs = mapM_ maybePut xs
where maybePut x = when (f x) (modify (++[x]))
sfilter :: (a -> Bool) -> [a] -> [a]
sfilter f xs = finalState
where (_, finalState) = runState (rememberElements f xs) []sfilter even [1,2,3,4,5]
==> [2,4]Мы можем писать свои собственные операции, которые работают для всех монад. Это стало возможным благодаря классам, как мы видели раньше. Если вы используете только монадные операции, такие как return и do-notation, система типов выведет общий тип для вашей функции.
mywhen b op = if b then op else return ()
mymapM_ op [] = return ()
mymapM_ op (x:xs) = do op x
mymapM_ op xs*Main> :t mywhen
mywhen :: (Monad m) => Bool -> m () -> m ()
*Main> :t mymapM_
mymapM_ :: (Monad m) => (t -> m a) -> [t] -> m ()Мы можем использовать эти общие операции в каждом из наших примеров монад:
perhapsDecrease :: Int -> Maybe Int
perhapsDecrease x = do
mywhen (x<=0) Nothing
return (x-1)perhapsDecrease 2 ==> Just 1
perhapsDecrease 0 ==> Nothingsearch :: (Show a, Eq a) => a -> [a] -> Logger ()
search x ys = mymapM_ look ys
where look y = mywhen (x==y) (msg ("Found "++show y))search 3 [1,2,3,4,3,2] ==> Logger ["Found 3","Found 3"] ()sumPositive :: [Int] -> State Int ()
sumPositive xs = mymapM_ f xs
where f x = when (x>0) (modify (x+))runState (sumPositive [1,-4,2,3]) 0 ==> ((),6)Еще не введена одна полезная операция: liftM.
liftM :: Monad m => (a->b) -> m a -> m b
liftM f op = do x <- op
return (f x)Операция liftM позволяет легко писать код с чистыми и монадическими частями.
liftM negate (Just 3)
==> Just (-3)
liftM sort $ firsts [[4,6],[2,1,0],[3,3,3]]
==> Just [2,3,4]
runState (liftM negate get) 3
==> (-3,3)Выглядит ли тип liftM знакомым? Это как fmap! На самом деле, легко определить функтор для монады: просто установите fmap = liftM. Поскольку каждый Monad должен быть Functor в наши дни, современный стиль Haskell предпочитает fmap по сравнению с liftM.
fmap :: Functor f => (a->b) -> f a -> f bfmap negate (Just 3)
==> Just (-3)
fmap sort $ firsts [[4,6],[2,1,0],[3,3,3]]
==> Just [2,3,4]
runState (fmap negate get) 3
==> (-3,3)Список монад (то есть, это Monad Например, []) представляет собой вычисления с множественные значения возврата Это полезно для поиска альтернатив. Вот первый пример. Для каждого x Мы производим оба x и -x:
[1,2,3] >>= \x -> [-x,x]
==> [-1,1,-2,2,-3,3]Мы можем отфильтровать неподходящие значения, создав пустой список:
[1,2,3] >>= \x -> if x>1 then [x] else []
==> [2,3]Если мы используем донотацию, список монад начинает больше напоминать петлевую конструкцию:
do word <- ["Blue", "Green"]
number <- [1,2,3]
return (word ++ show number)
==> ["Blue1","Blue2","Blue3","Green1","Green2","Green3"]Более интересный пример: найдите все пары в списке, который суммируется в k. (Один и тот же элемент дважды считается парой.)
findSum :: [Int] -> Int -> [(Int,Int)]
findSum xs k = do a <- xs
b <- xs
if (a+b==k) then [(a,b)] else []findSum [1,2,3,4,5] 5
==> [(1,4),(2,3),(3,2),(4,1)]Последний, более сложный пример. Мы находим все палиндромы из строки, используя список монад, а затем находим самую длинную.
import Data.List (sortBy)
substrings :: String -> [String]
substrings xs = do start <- [0..length xs - 1]
end <- [start+1..length xs - 1]
return $ drop start $ take end $ xs
palindromesIn :: String -> [String]
palindromesIn xs = do s <- substrings xs
if (s==reverse s) then return s else []
longestPalindrome xs = head . sortBy f $ palindromesIn xs
where f s s' = compare (length s') (length s) -- longer is smallerpalindromesIn "aabbacddcaca"
==> ["a","aa","a","abba","b","bb","b","a","acddca","c","cddc","d","dd","d","c","cac","a","c"]
longestPalindrome "aabbacddcaca"
==> "acddca"Вот удивительно простая реализация Монады списка:
instance Monad [] where
return x = [x] -- an operation that produces one value
lis >>= f = concat (map f lis) -- compute f for all values, combine the resultsНа самом деле, мы видели список монады ранее под видом понимания списка. Сравните эту реконструкцию findSum с предыдущей, в которой используется нотация do.
findSum :: [Int] -> Int -> [(Int,Int)]
findSum xs k = [(a,b) | a <- xs, b <- xs, a+b==k ]Как вы уже, наверное, догадались, IO — это монада. Однако реализации типа IO и instance Monad IO являются встроенными компиляторами. Вы не можете реализовать монаду IO только с использованием стандарта Haskell, в отличие от монады Maybe, монады State и других монад, которые мы видели.
Настоящие побочные эффекты подчиняются монадическому шаблону так же, как State и Maybe. Как и с другими монадами, мы отделяем чистое описание операций от их выполнения. Кроме того, с IO можно использовать общие монадические функции, включая mapM и родственные ей.
Вот несколько примеров написания IO с использованием операций монады.
printTwoThings :: IO ()
printTwoThings = putStrLn "One!" >> print 2
echo :: IO ()
echo = getLine >>= putStrLn
verboseEcho :: IO ()
verboseEcho = getLine >>= \s -> putStrLn ("You wrote: " ++ s)
query :: String -> IO String
query question = putStrLn question >> getLine
confirm :: String -> IO Bool
confirm question = putStrLn question >> fmap interpret getLine
where interpret "Y" = True
interpret _ = FalsePrelude> printTwoThings
One!
2
Prelude> verboseEcho
The Iliad
You wrote: The Iliad
Prelude> answer <- query "Why am I here?"
Why am I here?
Good question!
Prelude> answer
"Good question!"
Prelude> b <- confirm "Fire warheads?"
Fire warheads?
no no no no
Prelude> b
False
Prelude> b <- confirm "Make love, not war?"
Make love, not war?
Y
Prelude> b
TrueКак только вы познакомитесь с концепцией монады, вы начнете видеть монадоподобные вещи и на других языках. Наиболее известными примерами этого являются Option типов, Java Streams и JavaScript обещаний. Если вы знаете эти языки или концепции, вы можете найти этот раздел освещающим. Если нет, не стесняйтесь пропустить это.
Многие языки имеют option типа . Этот тип называется Optional<T> на Java, std::optional<T> на C++, Nullable<T> на C# и так далее. Эти типы часто имеют поведение, напоминающее монаду Haskell Maybe, например:
Optional.flatMap соответствует >>=: он позволяет применять Function<T,Optional<U>> к Optional<T> и получить Optional<U>.Nullable. Например, a + null становится null.Java Streams также имеет monadlike API. Потоки создают много значений постепенно. Как и в случае с Optional, метод Stream.flatMap позволяет взять Stream<T>, объединить его с Function<T,Stream<U>> и получить Stream<U>.
Например, если lines является Stream<String>, words берет String и возвращает Stream<String> и readInt берет String и возвращает Integer, мы можем написать:
Stream<Integer> parseNumbers(Stream<String> lines) {
return lines.flatMap(words).map(read);
}Это соответствует следующему коду монады списка Haskell:
parseNumbers :: [String] -> [Int]
parseNumbers strings = fmap read (strings >>= words)parseNumbers ["123 456","7 89"] ==> [123,456,7,89]Существует much disagreement о том, являются ли обещания в JavaScript really монадами или нет. Однако некоторые сходства очевидны.
Сначала рассмотрим сходство между Promise.then и >>=. Обе операции принимают действие — обещание или монадическую операцию — и связывают его с функцией, возвращающей новое действие.
function concatPromises(promise1, promise2) {
return promise1.then(value1 => promise2.then(value2 => value1+value2));
}>> concatPromises(Promise.resolve("abc"), Promise.resolve("def")).then(console.log)
abcdefconcatMonadic :: Monad m => m String -> m String -> m String
concatMonadic op1 op2 = op1 >>= (\value1 -> op2 >>= (\value2 -> return (value1++value2)))Prelude> concatMonadic (Just "abc") (Just "def")
Just "abcdef"Давайте рассмотрим сходство между асинхронизацией / ожиданием и нотацией. Оба являются более приятными синтаксисами для работы с необработанными механизмами Promise.then или >>=. Мы реализуем concatPromises с использованием асинхронизации / ожидания и concatMonadic с использованием донотации. Их поведение остается прежним.
async function concatPromises(promise1, promise2) {
let value1 = await promise1;
let value2 = await promise2;
return value1+value2;
}concatMonadic :: Monad m => m String -> m String -> m String
concatMonadic op1 op2 = do
value1 <- op1
value2 <- op2
return (value1++value2)Monad позволяет представлять разные способы выполнения последовательностей действий
Maybe)Monad (>>=, >>) напрямуюdoM — монада, то значения типа M a представляют операции, производящие результат типа amapM и т.д.State проще, чем расшифровать сложную рекурсию с состояниемЭта и предыдущая лекция охватывали многие части, в которых версия Haskell GHC отличается от стандартной Haskell 2010. Вот краткий список изменений, внесенных GHC, чтобы вы знали:
length, sum, foldr и др. обобщены для работы на Foldable вместо списковFunctor и Applicative являются суперклассами Monad.fail был перенесен из класса типа Monad в собственный класс MonadFail.Какое выражение эквивалентно следующему блоку?
do y <- z
s y
return (f y)z >> \y -> s y >> return (f y)z >>= \y -> s y >> return (f y)z >> \y -> s y >>= return (f y)Какой тип \x xs -> return (x : xs)?
Monad m => a -> [a] -> m [a]Monad m => a -> [m a] -> [m a]a -> [a] -> Monad [a]Какой тип \x xs -> return x : xs?
Monad m => a -> [a] -> m [a]Monad m => a -> [m a] -> [m a]a -> [a] -> Monad [a]Какой тип (\x xs -> return x) : xs?
Monad m => a -> [a] -> m [a]Monad m => a -> [m a] -> [m a]a -> [a] -> Monad [a]Теперь, когда вы знаете монады, вы почти все знаете о Haskell, чтобы начать писать реальные программы, которые используют библиотеки для полезных вещей. В этой лекции будут рассмотрены некоторые примеры библиотек, которые обычно используются в таких реальных программах. Использование этих библиотек также является хорошим способом практики использования монад, чтения документов и понимания ошибок типов.
Note! При чтении документации для библиотек не забудьте обратить внимание на библиотечную версию. Вы можете увидеть версии, используемые на курсе в файле tests.cabal. Ссылки в материале курса всегда приводят вас к правильной версии, как и команда stack haddock --open <package>. См. также раздел «Чтение документации» в части 1.
Text и ByteStringДо сих пор мы использовали тип Haskell String для работы со строками. Однако String — это просто [Char], связанный список персонажей. Это ужасно неэффективно как с точки зрения памяти, так и с точки зрения времени. Как только мы выходим за рамки обработки коротких строк и начинаем обрабатывать целые файлы или сетевые запросы, более эффективный по времени тип строк становится обязательным.
Есть два типа, которые используются в качестве замены для String, с немного различной семантикой:
Data.Text представляет собой a последовательность символов Unicode, как и String, только более эффективная. Используется при работе с текстом.Data.ByteString представляет собой последовательность a байтов. Используется при работе с двоичными данными.Кроме того, оба этих типа поставляются в вариантах lazy и strict. docs для Data.Text хорошо суммируют разницу:
Строгий тип
Textтребует, чтобы целая строка вписывалась в память сразу. Ленивый типTextспособен передавать потоковые строки, которые больше, чем память, с использованием небольшого объема памяти. Каждый модуль обеспечивает практически идентичный API.
Все эти типы (Text и ByteString, строгие и ленивые) предлагают функции pack и unpack для преобразования из и в простые String. Типы также поставляются со специализированными версиями знакомых функций списка, таких как reverse, take, map и так далее.
TextДавайте пройдемся по короткому сеансу GHCi, демонстрирующему использование GHCi. Data.Text Как документация говорит, Data.Text Модуль предназначен для импорта квалифицированный Мы можем преобразовать String входить в Text с функцией T.pack Обратите внимание, как значение типа Text Печатается так же, как и String.
Prelude> import qualified Data.Text as T
Prelude T> :t T.pack
T.pack :: String -> T.Text
Prelude T> phrase = T.pack "brevity is the soul of wit"
Prelude T> :t phrase
phrase :: T.Text
Prelude T> phrase
"brevity is the soul of wit"Мы можем использовать функции Data.Text оперировать значениями Text Многие из них названы так же, как и их коллеги. String s или списки из Prelude.
Prelude T> :t T.length
T.length :: T.Text -> Int
Prelude T> T.length phrase
26
Prelude T> T.head phrase
'b'
Prelude T> T.take 4 phrase
"brev"
Prelude T> :t T.words
T.words :: T.Text -> [T.Text]
Prelude T> T.words phrase
["brevity","is","the","soul","of","wit"]
Prelude T> :t T.map
T.map :: (Char -> Char) -> T.Text -> T.Text
Prelude T> T.map (\c -> if c=='o' then '0' else c) phrase
"brevity is the s0ul 0f wit"Полезной деталью является то, что Text имеет экземпляр Monoid, который склеивает значения Text вместе. Также можно использовать функции T.append и T.concat.
Prelude T> phrase <> phrase
"brevity is the soul of witbrevity is the soul of wit"
Prelude T> T.append phrase phrase
"brevity is the soul of witbrevity is the soul of wit"
Prelude T> T.concat [phrase,phrase,phrase]
"brevity is the soul of witbrevity is the soul of witbrevity is the soul of wit"Если вы хотите написать рекурсивную функцию, которая соответствует шаблону на Text, как на String, вы можете использовать функцию T.uncons :: T.Text -> Maybe (Char, T.Text), чтобы разделить Text на голову и хвост. Вот простой пример:
countLetter :: Char -> T.Text -> Int
countLetter c t =
case T.uncons t of
Nothing -> 0
Just (x,rest) -> (if x == c then 1 else 0) + countLetter c restPrelude T> countLetter 't' phrase
3Обратите внимание, что Data.Text Реализует строгий Text Тип. Вы должны использовать Data.Text.Lazy Для ленивого варианта. Как упоминалось ранее, одно различие между этими двумя типами заключается в том, что строгий тип не работает для бесконечных струн:
Prelude T> T.head (T.pack (repeat 'x'))
-- never returns
Prelude T> import qualified Data.Text.Lazy as TL
Prelude T TL> TL.head (TL.pack (repeat 'x'))
'x'Еще одна практическая проблема заключается в том, что при использовании библиотек можно получить несоответствие между строгими и ленивыми Text. Обычно это можно исправить, используя toStrict или fromStrict по мере необходимости.
Prelude T TL> lazyPhrase = TL.pack "brevity is the soul of wit"
Prelude T TL> :t lazyPhrase
lazyPhrase :: TL.Text
Prelude T TL> :t phrase
phrase :: T.Text
Prelude T TL> lazyPhrase == phrase
<interactive>: error:
• Couldn't match expected type ‘TL.Text’
with actual type ‘T.Text’
NB: ‘T.Text’ is defined in ‘Data.Text.Internal’
‘TL.Text’ is defined in ‘Data.Text.Internal.Lazy’
• In the second argument of ‘(==)’, namely ‘phrase’
In the expression: lazyPhrase == phrase
In an equation for ‘it’: it = lazyPhrase == phrase
Prelude T TL> :t TL.toStrict
TL.toStrict :: TL.Text -> T.Text
Prelude T TL> :t TL.fromStrict
TL.fromStrict :: T.Text -> TL.Text
Prelude T TL> TL.toStrict lazyPhrase == phrase
TrueByteStringМы можем пройти через почти тот же сеанс GHCi, используя ByteString вместо Text. Однако обратите внимание, что ByteString построен из значений Word8, а не значений Char. Char может представлять собой произвольную однокодовую кодовую точку, как для символа, такого как 'Å', но Word8 представляет собой байт: число от 0 до 255. К сожалению и несколько запутанно, значения ByteString печатаются как String.
Prelude> import Data.Word
Prelude Data.Word> import qualified Data.ByteString as B
Prelude Data.Word B> binary = B.pack [99,111,102,102,101,101]
Prelude Data.Word B> :t binary
binary :: B.ByteString
Prelude Data.Word B> :t B.pack
B.pack :: [Word8] -> B.ByteString
Prelude Data.Word B> binary
"coffee"
Prelude Data.Word B> :t B.length
B.length :: B.ByteString -> Int
Prelude Data.Word B> B.length binary
6
Prelude Data.Word B> :t B.head
B.head :: B.ByteString -> Word8
Prelude Data.Word B> B.head binary
99
Prelude Data.Word B> B.take 4 binary
"coff"
Prelude Data.Word B> :t B.map
B.map :: (Word8 -> Word8) -> B.ByteString -> B.ByteString
Prelude Data.Word B> B.map (+1) binary
"dpggff"Те же предостережения относятся и к различиям между строгим и ленивым. ByteString как для Text:
Prelude B Data.Char> B.head (B.pack (repeat 99))
-- never returns
Prelude Data.Word B> import qualified Data.ByteString.Lazy as BL
Prelude Data.Word B BL> BL.head (BL.pack (repeat 99))
99
Prelude Data.Word B BL> binary == BL.pack [99]
<interactive>: error:
• Couldn't match expected type ‘B.ByteString’
with actual type ‘BL.ByteString’
NB: ‘BL.ByteString’ is defined in ‘Data.ByteString.Lazy.Internal’
‘B.ByteString’ is defined in ‘Data.ByteString.Internal’
• In the second argument of ‘(==)’, namely ‘BL.pack [99]’
In the expression: binary == BL.pack [99]
In an equation for ‘it’: it = binary == BL.pack [99]
Prelude Data.Word B BL> :t BL.toStrict
BL.toStrict :: BL.ByteString -> B.ByteString
Prelude Data.Word B BL> :t BL.fromStrict
BL.fromStrict :: B.ByteString -> BL.ByteString
Prelude Data.Word B BL> binary == BL.toStrict (BL.pack [99])
FalseВы, наверное, задаетесь вопросом, почему у нас есть и текст, и струна. Разница тонкая, но реальная. Когда мы работаем с Text, мы работаем с персонажами, независимо от того, что это за символы и как они закодированы. Когда мы работаем на ByteString, мы работаем на байтах, независимо от того, что эти байты представляют.
Персонажи, числа и структуры данных — это абстракции, которые помогают людям решать сложные задачи программирования. Память компьютера — это, по сути, просто огромная последовательность байтов. Машине все равно, как мы интерпретируем эти байты. Существенное различие между Text и ByteString заключается в том, как байты группируются и интерпретируются.
Чтобы проиллюстрировать эту разницу, мы рассмотрим текстовое кодирование UTF-8. Кодирование текста представляет собой способ представления characters как bytes. UTF-8 может представлять все миллионы символов, определенных Unicode. Поскольку байты могут хранить только значения от 0 до 255, это означает, что символ может быть закодирован в несколько байтов. Биты и байты строки UTF-8 «Ha thekeλ!» можно интерпретировать по-разному:
(Если вы видите разные символы на картинке и в «Ha.keλ!», это означает, что ваш браузер либо неправильно интерпретирует кодирование, либо используемый вами шрифт не поддерживает все символы.)
Если мы прочитаем один и тот же поток битов с помощью другого кодирования, мы увидим другие символы. Например, приведенная выше строка будет интерпретирована как «Haââ.«keλ!» с использованием текста Latin-1, кодирующего .
Кстати, при работе с необработанными двоичными данными часто удобно использовать шестидесятичную систему счисления, которая использует один символ 0, 1, ..., 9, A, B, ..., F для представления всех шестнадцати возможных комбинаций из четырех битов. Мы не нуждаемся в шестнадцатеричных в этом курсе, но вы можете проверить Wikipedia, если вы хотите узнать больше о шестнадцатеричных.
Мы можем использовать тот же пример, используя код. Функция Data.Text.Encoding.encodeUtf8 :: Text -> ByteString кодирует символы в Тексте в байты в ByteString с помощью UTF-8.
Prelude> import qualified Data.Text as T
Prelude T> import qualified Data.ByteString as B
Prelude T B> T.length (T.pack "haskell")
7
Prelude T B> T.length (T.pack "Ha∫keλ!")
7
Prelude T B> import Data.Text.Encoding
Prelude T B Data.Text.Encoding> encodeUtf8 (T.pack "haskell")
"haskell"
Prelude T B Data.Text.Encoding> encodeUtf8 (T.pack "Ha∫keλ!")
"Ha\226\136\171ke\206\187!"
Prelude T B Data.Text.Encoding> B.length (encodeUtf8 (T.pack "haskell"))
7
Prelude T B Data.Text.Encoding> B.length (encodeUtf8 (T.pack "Ha∫keλ!"))
10Если мы обрабатываем ASCII text, то есть символы, которые могут быть представлены с помощью одиночных байтов, мы можем использовать Text и ByteString взаимозаменяемо. Существуют пространства имен Data.ByteString.Char8 и Data.ByteString.Lazy.Char8, которые предлагают функции, которые работают на ByteString, используя значения Char вместо Word8. Тем не менее, необходимо позаботиться о том, чтобы все персонажи действительно были простыми персонажами ASCII, иначе произойдут удивительные вещи.
Prelude T B> import qualified Data.ByteString.Char8 as B8
Prelude T B B8> B8.pack "abc"
"abc"
Prelude T B B8> :t B8.pack
B8.pack :: String -> B.ByteString
Prelude T B B8> :t B.pack
B.pack :: [Word8] -> B.ByteString
Prelude T B B8> B8.cons 'a' (B8.pack "bc")
"abc"
Prelude T B B8> putStrLn (B8.unpack (B8.pack "€λ훈")) -- non-ASCII characters get truncated
¬»È
Prelude T B B8> putStrLn (T.unpack (T.pack "€λ훈"))
€λ훈В следующих библиотеках мы рассмотрим работу внутри монады IO. Вот краткий обзор того, что мы узнали о монадах в последней лекции.
M — монада, значения типа M X представляют операции, которые можно выполнить, чтобы получить значение типа X.Monad (return, >>=, >>),do - примечание,mapM.return — не ключевое слово и не завершает выполнение операции. Выражение return x создаёт операцию, которая возвращает x и больше ничего не делает.do:foo y = do
operation1 -- run an operation
val <- operation2 -- run an operation and keep the produced value
operation3 val y -- run an operation with parameters
mapM_ (\x -> operation4 val x) things -- use a generic monad operation and a lambda
operation5 val -- the final line of the do decides which value the whole block producesИногда кажется, что всё в мире работает через HTTP и веб-API. Веб-браузер, приложения на смартфоне, ваш банк, кофеварка и даже дверной звонок — все они общаются с серверами по протоколу HTTP.
Давайте рассмотрим, как настроить простой HTTP-сервер в Haskell. Стандартные низкоуровневые компоненты для этого называются WAI и Warp. WAI, интерфейс веб-приложений дает нам способ определить, как обрабатываются HTTP-запросы. Warp - это простой HTTP-сервер, который запускает логику, которую мы определили с помощью WAI. Возможно, сейчас это звучит немного абстрактно, но простой пример поможет.
В файле exercises/Examples/HelloServer.hs реализован HTTP-сервер, который всегда отвечает «Hello World!». Вы можете попробовать его, перейдя в каталог exercises/Examples и запустив его с stack runhaskell HelloServer.hs. После этого вы можете посетить http://localhost:3421 в вашем браузере, чтобы увидеть ответ от сервера.
module Examples.HelloServer where
import qualified Data.ByteString.Lazy.Char8 as BL
import Network.HTTP.Types.Status (status200)
import Network.Wai (Application, responseLBS)
import Network.Wai.Handler.Warp (run)
port :: Int
port = 3421
main :: IO ()
main = run port application
-- type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceived
application :: Application
application request respond =
respond (responseLBS status200 [] (BL.pack "Hello World!"))Рассмотрим типы из этого примера. Здесь происходит немало. Прежде всего, Application — псевдоним типа (type alias) для сущности, реализующей логику веб-сервера. Функция run из Warp запускает Application:
run :: Port -> Application -> IO ()
type Application = Request -> (Response -> IO ResponseReceived) -> IO ResponseReceivedСкоро мы подробнее разберём Request и Response, а пока из сигнатуры видно, что Application — операция IO. Она принимает запрос типа Request и операцию respond :: Response -> IO ResponseReceived. Аргументы вроде respond во многих контекстах называют функциями обратного вызова (callbacks): с их помощью приложение вызывает передавшую их библиотеку. Операция Application должна вернуть то же специальное значение типа ResponseReceived, которое возвращает respond. Этот тип можно считать свидетельством того, что respond действительно была вызвана операцией Application.
Это может показаться пугающим, но если посмотреть на код, все относительно просто: наш server является Application и принимает два параметра: request и respond.
WAI использует множество типов, таких как Port, Request, Response, Status для представления концепций HTTP. Полезно искать их в документации, когда вы сталкиваетесь с ними. Например, Port является псевдонимом для Int. В качестве другого примера можно увидеть функцию responseLBS имеет тип
responseLBS :: Status -> ResponseHeaders -> ByteString -> Responseгде Status определено в Network.HTTP.Types.Status, ResponseHeaders является псевдонимом типа для [Header] от Network.HTTP.Types.Header, ByteString является ленивым ByteString, а результат типа Response определяется Network.WAI.
Наконец, обратите внимание, что мы используем функцию Data.ByteString.Lazy.Char8.pack для преобразования String в ByteString. Это работает только для текста ASCII.
Веб-сервер, который всегда отвечает одним и тем же текстом, не так уж интересен. Давайте посмотрим, как мы можем дать разные ответы на разные запросы. В HTTP-запросе есть много частей, но для этой лекции мы сосредоточимся на path. В URL-адресе, таком как http://example.com/abcd/ef/file, часть /abcd/ef/file - это путь. WAI выполняет функцию
pathInfo :: Request -> [Text]Это дает нам путь запрошенного URL, разделенного на символы /.
Файл exercises/Examples/PathServer.hs реализует веб-сервер, который имеет три разные страницы:
Как и раньше, вы можете запустить сервер, зайдя в каталог exercises/Examples и запустив stack runhaskell PathServer.hs.
После внедрения HTTP-сервера мы можем участвовать в глобальном графике приложений, разговаривающих друг с другом. Но что толку говорить, если мы не можем вспомнить? Реальное приложение должно иметь возможность persist data даже при его перезапуске. Распространенным способом достижения этой цели является использование базы данных.
Существует много различных типов баз данных, но, возможно, наиболее широко используемой простой базой данных является SQLite. SQLite - это библиотека, которая позволяет хранить данные в файле и обрабатывать его с помощью SQL, языка структурированных запросов . С SQLite нет необходимости запускать отдельный сервер базы данных a la PostgreSQL или MySQL.
Если вы не знакомы с SQL, не волнуйтесь, вам не нужно будет писать свои собственные запросы в Упражнениях. Если вы хотите изучить немного SQL сейчас, в Интернете есть много учебных пособий. См. W3Schools, SQL Zoo или Codecademy.
Есть много библиотек для Haskell для использования SQLite, но мы рассмотрим одну под названием sqlite-simple здесь. Давайте немного познакомимся с библиотекой GHCi.
Все функции находятся внутри Database.SQLite.Simple. Вы можете открыть базу данных, указав имя файла в open, которая является операцией ввода-вывода, которая производит Connection.
Prelude> import Database.SQLite.Simple
Prelude Database.SQLite.Simple> :t open
open :: String -> IO Connection
Prelude Database.SQLite.Simple> db <- open "example.sqlite"Для запуска SQL-запроса можно использовать операцию IO query_, которая принимает Connection и Query и производит список результатов. Тип Query - это просто newtype вокруг Text. Тип результата query_ является полиморфным: из базы данных можно прочитать любой тип, который удовлетворяет классу типа FromRow. Если это кажется запутанным, сравните его с типом read: Read a => String -> a. Класс FromRow похож на Read для этой базы данных: он представляет типы, которые могут быть считаны из базы данных. В любом случае, давайте прочитаем номер 1 из базы данных:
Prelude Database.SQLite.Simple> :t query_
query_ :: FromRow r => Connection -> Query -> IO [r]
Prelude Database.SQLite.Simple> :info Query
newtype Query = Query {fromQuery :: Data.Text.Internal.Text}
-- Defined in ‘Database.SQLite.Simple.Types’
-- ... rest of output omitted
Prelude Database.SQLite.Simple> import qualified Data.Text as T
Prelude Database.SQLite.Simple T> q = Query (T.pack "SELECT 1;")
Prelude Database.SQLite.Simple T> res <- query_ db q :: IO [[Int]]
Prelude Database.SQLite.Simple T> res
[[1]]Кстати, все эти начальные примеры используют простые запросы SELECT x, y, z;, которые просто возвращают постоянные данные. О реальных таблицах в базе данных мы поговорим позже.
Без подписи типа мы получаем ошибку от GHCi, которая не может решить, какой тип мы хотим прочитать из базы данных:
Prelude Database.SQLite.Simple T> res <- query_ db q
<interactive>:17:8: error:
• Ambiguous type variable ‘r0’ arising from a use of ‘query_’
prevents the constraint ‘(FromRow r0)’ from being solved.
Probable fix: use a type annotation to specify what ‘r0’ should be.
-- rest of error omittedПрежде чем мы продолжим, давайте более подробно рассмотрим FromRow. Если вы сталкивались с SQL раньше, вы знаете, что SQL-запрос возвращает ряд rows, и каждая строка состоит из ряда values (также называемый columns). Чтобы интерпретировать результат SQL-запроса в данные Haskell, нам нужен способ интерпретировать эти значения и строки. Таким образом, sqlite-simple определяет два класса, FromField и FromRow, и кучу примеров, таких как: (Вы можете найти эти экземпляры от , docs или запросив GHCi с :info FromRow и т. Д.)
instance FromField Int
instance FromField Bool
instance FromField String
instance FromField Text
instance FromField a => FromRow [a]
instance (FromField a, FromField b) => FromRow (a,b)
instance (FromField a, FromField b, FromField c) => FromRow (a,b,c)По сути, базовые типы данных Haskell соответствуют классу FromField, а различные коллекции Haskell соответствуют классу FromRow. В нашем предыдущем примере мы использовали экземпляры FromRow [a] и FromField Int, чтобы получить [[Int]] из query_. Вот простой запрос, который использует некоторые другие типы данных:
Prelude Database.SQLite.Simple T> q = Query (T.pack "SELECT 1, true, 'string';")
Prelude Database.SQLite.Simple T> query_ db q :: IO [(Int,Bool,String)]
[(1,True,"string")]Что делать, если SQL и Haskell не совпадают? Ну, вы получаете ошибку времени выполнения, как если бы вы попытались вызвать read "True" :: Int.
Prelude Database.SQLite.Simple T> query_ db q :: IO [(Int,Int,Int)]
*** Exception: ConversionFailed {errSQLType = "TEXT", errHaskellType = "Int", errMessage = "need an int"}В дополнение к классам FromRow и FromField библиотека sqlite-simple определяет классы ToRow и ToField для записи в базу данных. Ниже показан тип функции query, позволяющей использовать параметризованные запросы.
query :: (ToRow q, FromRow r) => Connection -> Query -> q -> IO [r]Вот некоторые примеры для ToRow и ToField:
instance ToField Int
instance ToField Bool
instance ToField String
instance ToField Text
instance ToField Int
instance ToField a => ToRow [a]
instance (ToField a, ToField b) => ToRow (a, b)
instance (ToField a, ToField b, ToField c) => ToRow (a, b, c)
instance ToField a => ToRow (Only a)Параметризованные запросы используют символ ? для обозначения слотов, в которых параметры могут быть переданы. Вот простой пример:
Prelude Database.SQLite.Simple T> input = (1,"hello") :: (Int,String)
Prelude Database.SQLite.Simple T> parameterized = Query (T.pack "SELECT ?+1, true, ?;")
Prelude Database.SQLite.Simple T> query db parameterized input :: IO [(Int,Bool,String)]
[(2,True,"hello")]Note! При выполнении запроса с одним параметром можно использовать два экземпляра ToRow: ToField a => ToRow [a] и ToField a => Only a. Тип данных Only определен в Data.Tuple.Only и является своего рода обходным путем для того, что у Haskell нет одноэлементных кортежей. Кроме того, работает список размера 1. То же самое относится к запросам, которые возвращают строки только с одной колонкой: вы можете использовать [[X]] или [Only X] в качестве типа возврата. Вот пример:
Prelude Database.SQLite.Simple T> q = Query (T.pack "SELECT lower(?);")
Prelude Database.SQLite.Simple T> query db q (Only "HELLO") :: IO [Only String]
[Only {fromOnly = "hello"}]
Prelude Database.SQLite.Simple T> query db q ["HELLO"] :: IO [[String]]
[["hello"]]Это почти все, что вам нужно знать о sqlite-simple: open, query_, query, FromRow, ToRow. Да, еще кое-что. Есть функции execute и execute_, если вам не нужен результат запроса. Например, они полезны для вставки вещей в базу данных.
execute_ :: Connection -> Query -> IO ()
execute :: ToRow q => Connection -> Query -> q -> IO ()Вы найдете пример программы, которая использует sqlite-simple для поддержания телефонной книги под exercises/Examples/Phonebook.hs. Программа хранит телефонную книгу в файле под названием phonebook.db и работает так (запускается из каталога exercises/Examples в репозитории курса):
$ stack runhaskell Phonebook.hs
(a)dd or (q)uery?
a
Name?
bob
Phone?
1234
$ stack runhaskell Phonebook.hs
(a)dd or (q)uery?
a
Name?
bob
Phone?
5678
$ stack runhaskell Phonebook.hs
(a)dd or (q)uery?
a
Name?
samantha
Phone?
1357
$ stack runhaskell Phonebook.hs
(a)dd or (q)uery?
q
Name?
bob
2 numbers:
["1234"]
["5678"]
PS. Если вы опустошены отсутствием проверки типа компиляции для запросов SQL, вы можете взглянуть на некоторые из более продвинутых библиотек SQL для Haskell, таких как Beam или Opaleye. Этот курс использует sqlite-простой для простоты и чтобы не зацикливаться на деталях SQL.
Класс типа Applicative - это середина между Functor (с которой вы не можете так много сделать) и Monad (что в значительной степени позволяет писать произвольные программы). Причины использования Applicative вместо Monad включают:
Applicative позволяет меньше операций, его можно оптимизировать лучше, чем Monad.Applicative проще обдумать.Monad для вашего типа, но есть экземпляр Applicative. Это редкость.Что такое Applicative? Рассмотрим определение.
class Functor f => Applicative f where
pure :: a -> f a
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
-- other operations omitted for nowТаким образом, Applicative - это Functor, который позволяет нам создавать однотонные значения через pure и объединять два значения в одно с использованием liftA2. Это добавляет много энергии по сравнению с голым функтором. Вычисления с использованием функторов обязательно линейны: fmap :: (a -> b) -> f a -> f b принимает одно функторное значение и выводит другое. В отличие от этого, pure не принимает никакого функторного значения и выводит одно, а liftA2 берет два и возвращает одно.
Термин Applicative происходит от термина Applicative Functor, который звучит так, как будто он происходит из Теории Категории, но на самом деле был введен в статью по программированию.
На данный момент достаточно абстрактного мумбо-джамбо. Давайте посмотрим, какие вычисления мы можем выразить с помощью аппликативных операций (и fmap). Начнем с приложения Maybe. Вот упрощенное определение:
instance Applicative Maybe where
pure x = Just x
liftA2 f (Just x) (Just y) = Just (f x y)
liftA2 f _ _ = NothingВы увидите, что определение использует тот же тип распространения отказов, что и экземпляр Monad Maybe. Давайте использовать это при разборе денежных ценностей:
data Currency = EUR | USD
deriving (Show, Eq)
data Money = Money Int Currency
deriving (Show, Eq)
parseCurrency :: String -> Maybe Currency
parseCurrency "e" = pure EUR
parseCurrency "€" = pure EUR
parseCurrency "$" = pure USD
parseCurrency _ = Nothing
parseAmount :: String -> Maybe Int
parseAmount = readMaybe
parseMoney :: String -> String -> Maybe Money
parseMoney amountString currencyString =
liftA2 Money (parseAmount amountString) (parseCurrency currencyString)parseMoney "123" "€" ==> Just (Money 123 EUR)
parseMoney "45" "$" ==> Just (Money 45 USD)
parseMoney "4x" "€" ==> Nothing
parseMoney "45" "£" ==> NothingЭто хорошо сработало. Однако, если мы попытаемся расширить это, мы скоро войдем в пределы Applicative. Например, рассмотрим функцию sumMoney, которая суммирует значения Money, но не работает, если они не находятся в одной валюте:
sumMoney :: Money -> Money -> Maybe Money
sumMoney (Money a c) (Money b c')
| c == c' = Just (Money (a+b) c)
| otherwise = NothingМы не можем применить его к двум значениям Maybe Money с использованием операций Applicative. Для этого нужна монада Maybe:
example :: Maybe Money
example = do x <- parseMoney "123" "€"
y <- parseMoney "45" "$"
sumMoney x yЕсли мы попытаемся использовать liftA2, мы застрянем с типом Maybe (Maybe Money). Кроме того, теперь мы можем получить два разных типа сбоев: Nothing и Just Nothing, в зависимости от того, на каком уровне происходит ошибка. Это был бы явный пример перехода на экземпляр Monad.
liftA2 sumMoney (parseMoney "123" "e") (parseMoney "45" "€")
==> Just (Just (Money 168 EUR))
liftA2 sumMoney (parseMoney "123" "e") (parseMoney "45" "$")
==> Just Nothing
liftA2 sumMoney (parseMoney "123" "e") (parseMoney "xxx" "e")
==> Nothing
Название liftA2 звучит немного громоздко, но это аналогия с liftM, liftM2 и так далее. Напомним, что liftM был просто fmap, поэтому, возможно, liftA2 должен был называться fmap2.
Давайте посмотрим на примеры применения еще одного Functor, который мы видели. Экземпляр Applicative для функтора списка проходит через все возможные комбинации значений (как и монада списка). Вот пример:
instance Applicative [] where
pure x = [x]
liftA2 f xs ys = [f x y | x <- xs, y <- ys]Вот вам пример: генерация фраз.
things :: [String]
things = ["tangerine","bandit","diamond"]
fruits :: [String]
fruits = ["apple", "tangerine"]
phrases :: [String]
phrases = liftA2 combine things fruits
where combine t f = "a " ++ t ++ " the size of a " ++ f
bunches = liftA2 copy [1,2,3] fruits
where copy n f = unwords (replicate n f)phrases ==> ["a tangerine the size of a apple",
"a tangerine the size of a tangerine",
"a bandit the size of a apple",
"a bandit the size of a tangerine",
"a diamond the size of a apple",
"a diamond the size of a tangerine"]
bunches ==> ["apple","tangerine",
"apple apple","tangerine tangerine",
"apple apple apple","tangerine tangerine tangerine"]Есть несколько операторов для приложений, которые довольно удобны. Это <$>, <*>, <* и *>.
Начнем с <$>, который представляет собой просто инфиксную версию fmap:
(<$>) :: Functor f => (a -> b) -> f a -> f b
f <$> x = fmap f xnot <$> Just True ==> Just False
not <$> Nothing ==> Nothing
negate <$> [1,2,3] ==> [-1,-2,-3]Это довольно мило само по себе, но это действительно сияет в сочетании с этим оператором:
(<*>) :: Applicative f => f (a -> b) -> f a -> f bТип говорит вам, что делает <*>: это функциональное приложение lifted для приложения. Вот несколько отдельных примеров:
Just not <*> Just True ==> Just False
Nothing <*> Just True ==> Nothing
Just not <*> Nothing ==> Nothing
[(+1),(*2)] <*> [10,100] ==> [11,101,20,200]Настоящая магия начинается, когда мы объединяем <$> и <*>: так можно поднять в аппликативный контекст функцию с любым количеством аргументов.
say :: String -> Int -> String -> String
say x i y = x ++ " has " ++ show i ++ " " ++ ysay <$> Just "haskell" <*> Just 99 <*> Just "operators"
==> Just "haskell has 99 operators"
say <$> Nothing <*> Just 99 <*> Just "operators"
==> Nothing
say <$> ["bob","jake"] <*> [2,3] <*> ["bananas","cars"]
==> ["bob has 2 bananas",
"bob has 2 cars",
"bob has 3 bananas",
"bob has 3 cars",
"jake has 2 bananas",
"jake has 2 cars",
"jake has 3 bananas",
"jake has 3 cars"]Что здесь происходит? Давайте пройдем через оценку. Ключ в том, что каждый <*> частично применяет еще один аргумент к функции.
say <$> Just "haskell" <*> Just 99 <*> Just "operators"
=== ((say <$> Just "haskell") <*> Just 99) <*> Just "operators"
=== (fmap say (Just "haskell") <*> Just 99) <*> Just "operators"
==> (Just (say "haskell") <*> Just 99) <*> Just "operators"
==> Just (say "haskell" 99) <*> Just "operators"
==> Just (say "haskell" 99 "operators")
==> Just "haskell has 99 operators"Возможно, просмотр типов сделает его более ясным:
say <$> Just "haskell" :: Maybe (Int -> String -> String)
say <$> Just "haskell" <*> Just 99 :: Maybe ( String -> String)
say <$> Just "haskell" <*> Just 99 <*> Just "operators" :: Maybe ( String)Следующие два оператора немного проще:
(*>) :: Applicative f => f a -> f b -> f b
x *> y = liftA2 (\a b -> b) x y
(<*) :: Applicative f => f a -> f b -> f a
x <* y = liftA2 (\a b -> a) x yВы можете сравнить типы с более знакомым оператором:
(>>) :: Monad m => m a -> m b -> m bОператоры <* и *> имеют в виду: выполняют обе эти операции, но сохраняют только один результат. Стрелка указывает на результат, который сохраняется:
Just 1 *> Just 2 ==> Just 2
Just 1 <* Just 2 ==> Just 1
Just 1 <* Nothing ==> Nothing
Nothing <* Just 2 ==> NothingЭти операторы могут показаться тривиальными, но они полезны при объединении проверок. Например:
decrease :: Int -> Maybe Int
decrease i = if i>0 then Just (i-1) else Nothing
small :: Int -> Maybe Int
small i = if i<10 then Just i else Nothing
decreaseSmall :: Int -> Maybe Int
-- do what decrease does, but fail if small fails
decreaseSmall i = decrease i <* small idecreaseSmall 4 ==> Just 3
decreaseSmall 0 ==> Nothing
decreaseSmall 11 ==> NothingТеперь, когда мы увидели всех этих операторов, мы можем понять полное определение Applicative. Все операторы имеют определения в терминах liftA2, поэтому достаточно определить liftA2 и pure при реализации экземпляра Applicative.
class Functor f => Applicative f where
pure :: a -> f a
liftA2 :: (a -> b -> c) -> f a -> f b -> f c
(<*>) :: f (a -> b) -> f a -> f b
(*>) :: f a -> f b -> f b
(<*) :: f a -> f b -> f aДавайте посмотрим на приложение, которое немного интереснее, чем «Может быть» или списки. Часто в программировании нам нужны validate некоторые входы от пользователя. В этих случаях полезно собрать все ошибки, которые могут быть у входа. Файл exercises/Examples/Validation.hs реализует тип данных Validation:
data Validation a = Ok a | Errors [String]
deriving (Show,Eq)Пример Applicative для Validation работает следующим образом:
liftA2 (+) (Ok 1) (Ok 2)
==> Ok 3
liftA2 (+) (Errors ["oh no"]) (Errors ["boom"])
==> Errors ["oh no","boom"]Обратите внимание, что в отличие от Maybe Applicative, у нас есть много различных типов отказов.
Вот пример, который вводит некоторых помощников, а затем использует их, чтобы поздравить кого-то с днем рождения:
invalid :: String -> Validation a
invalid err = Errors [err]
check :: Bool -> String -> a -> Validation a
check b err x
| b = pure x
| otherwise = invalid err
birthday :: String -> Int -> Validation String
birthday name age = liftA2 congratulate checkedName checkedAge
where checkedName = check (length name < 10) "Name too long" name
checkedAge = check (age < 99) "Too old" age
congratulate n a = "Happy "++show a++"th birthday "++n++"!"birthday "Guy" 31
==> Ok "Happy 31th birthday Guy!"
birthday "Guybrush Threepwood" 31
==> Errors ["Name too long"]
birthday "Yog-sothoth" 10000
==> Errors ["Name too long","Too old"]Вот примеры Functor и Applicative для Validation:
instance Functor Validation where
fmap f (Ok x) = Ok (f x)
fmap _ (Errors e) = Errors e
instance Applicative Validation where
pure x = Ok x
liftA2 f (Ok x) (Ok y) = Ok (f x y)
liftA2 f (Errors e1) (Ok y) = Errors e1
liftA2 f (Ok x) (Errors e2) = Errors e2
liftA2 f (Errors e1) (Errors e2) = Errors (e1++e2)Определение liftA2 для Validation показывает, что ошибки собираются вместе слева направо. Это можно увидеть в приведенном выше примере, где выражение liftA2 congratulate checkedName checkedAge выводит ошибку ("Name too long") из checkedName первой, а ошибку ("Too old") из checkedAge последней.
traverseДо сих пор мы имели дело с вещами фиксированного размера и приложениями: мы применили функцию двух или трех аргументов к некоторым вещам. Что, если у нас есть произвольное количество входов? Что, если нам нужно проверить список?
Давайте рассмотрим некоторые способы реализации такой функции:
allPositive [1,2,3]
==> Ok [1,2,3]
allPositive [1,2,3,-4]
==> Errors ["Not positive: -4"]
allPositive [1,-2,3,-4]
==> Errors ["Not positive: -2","Not positive: -4"]Как всегда, при работе со списками, сопоставление шаблонов и рекурсия, как правило, идут своим чередом. Вот рекурсивное решение:
allPositive :: [Int] -> Validation [Int]
allPositive [] = Ok []
allPositive (x:xs) = liftA2 (:) checkThis checkRest
where checkThis = check (x>=0) ("Not positive: "++show x) x
checkRest = allPositive xsЭто немного хлопотно, чтобы всегда излагать рекурсию, как это. Если бы мы работали в Monad, мы могли бы просто использовать помощника, такого как mapM:
mapM (\x -> if x>=0 then Just x else Nothing) [1,2,3]
==> Just [1,2,3]
mapM (\x -> if x>=0 then Just x else Nothing) [1,2,3,-4]
==> NothingАналог mapM для Applicative называется traverse и является методом класса типов Traversable:
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)Это одна чертова подпись типа, поэтому давайте немного упростим ее. Списки Traversable, поэтому мы можем специализировать этот тип на:
traverse :: Applicative f => (a -> f b) -> [a] -> f [b]Похоже на то, что нам нужно! Для аппликантов, которые также являются монадами, traverse - это просто еще одно название для mapM:
traverse (\x -> if x>=0 then Just x else Nothing) [1,2,3]
==> Just [1,2,3]
traverse (\x -> if x>=0 then Just x else Nothing) [1,2,3,-4]
==> NothingНо для нашей Validation, которая не является Monad, traverse - это именно то, что мы хотим:
allPositive :: [Int] -> Validation [Int]
allPositive xs = traverse checkNumber xs
where checkNumber x = check (x>=0) ("Not positive: "++show x) xallPositive [1,2,3]
==> Ok [1,2,3]
allPositive [1,2,3,-4]
==> Errors ["Not positive: -4"]
allPositive [1,-2,3,-4]
==> Errors ["Not positive: -2","Not positive: -4"]Обратите внимание, как traverse для Validation собирает все ошибки вместе, в порядке их возникновения в исходном списке.
PS. Фактически, Validation является одним из немногих примеров Applicative, который не может быть Monad. Ты можешь понять почему?
TraversableЧто такое Traversable? Много знакомых структур. Вот несколько примеров:
decrease :: Int -> Maybe Int
decrease i = if i>0 then Just (i-1) else Nothing-- Lists are Traversable
traverse decrease [1,2,3] ==> Just [0,1,2]
traverse decrease [1,0,3] ==> Nothing
-- Arrays are Traversable
traverse decrease (array (1,3) [(1,10),(2,11),(3,12)])
==> Just (array (1,3) [(1,9),(2,10),(3,11)])
-- Maps are Traversable
traverse decrease (M.fromList [("a",1),("b",2)])
==> Just (M.fromList [("a",0),("b",1)])
traverse decrease (M.fromList [("a",1),("b",0)])
==> Nothing
-- Either is Traversable
traverse decrease (Left "abc") ==> Just (Left "abc")
traverse decrease (Right 3) ==> Just (Right 2)
traverse decrease (Right 0) ==> NothingИтак, Traversable — класс типов для разнообразных контейнеров, подобный Foldable. Из определения видно, что Traversable является подклассом Foldable, а traverse и mapM — его методы.
class (Functor t, Foldable t) => Traversable t where
traverse :: Applicative f => (a -> f b) -> t a -> f (t b)
mapM :: Monad m => (a -> m b) -> t a -> m (t b)Может быть трудно держать типы прямо здесь. Вернемся к типу traverse:
traverse :: (Traversable t, Applicative f) => (a -> f b) -> t a -> f (t b)Здесь у нас есть два функтора: t и f. Функтор t также является Foldable и Traversable, а функтор f также является Applicative. Функция traverse позволяет запускать f -операции внутри контейнера t.
Не волнуйтесь, если это кажется абстрактным. На практике вы почти всегда используете traverse в списках.
AlternativeЕсли вы немного поиграете с приложениями, вы начнете замечать некоторые ограничения их силы. Например, при написании парсеров, как мы сделали в примере parseMoney, было бы неплохо попробовать несколько разных парсеров и взять любой результат без сбоев. Это достаточно легко написать для конкретного приложения, такого как Maybe.
data Answer = Yes | No
deriving (Show, Eq)
parseYes :: String -> Maybe Answer
parseYes "y" = Just Yes
parseYes "yes" = Just Yes
parseYes "maybe" = Just Yes
parseYes _ = Nothing
parseNo :: String -> Maybe Answer
parseNo "n" = Just No
parseNo "no" = Just No
parseNo "maybe" = Just No
parseNo _ = Nothing
eitherOf :: Maybe x -> Maybe x -> Maybe x
eitherOf (Just x) _ = Just x
eitherOf Nothing mx = mx
parseAnswer :: String -> Maybe Answer
-- prefer positive answers!
parseAnswer s = eitherOf (parseYes s) (parseNo s)parseAnswer "yes" ==> Just Yes
parseAnswer "y" ==> Just Yes
parseAnswer "n" ==> Just No
parseAnswer "maybe" ==> Just Yes
parseAnswer "x" ==> NothingКак обобщить eitherOf? Нельзя просто дать ей тип Applicative f => f x -> f x -> f x: тогда реализация должна была бы выглядеть примерно как eitherOf a b = liftA2 something a b, а выражение eitherOf Nothing (Just x) вернуло бы Nothing — именно так работает экземпляр Applicative.
Оказывается, нужен ещё один класс типов — Alternative. Он добавляет к Applicative две операции: empty обозначает отсутствие результатов, а <|> объединяет результаты.
class Applicative f => Alternative f where
empty :: f a
(<|>) :: f a -> f a -> f a
-- some other operations omittedТеперь мы можем переписать наш код с помощью общих операций:
data Answer = Yes | No
deriving (Show, Eq)
parseYes :: Alternative f => String -> f Answer
parseYes "y" = pure Yes
parseYes "yes" = pure Yes
parseYes "maybe" = pure Yes
parseYes _ = empty
parseNo :: Alternative f => String -> f Answer
parseNo "n" = pure No
parseNo "no" = pure No
parseNo "maybe" = pure No
parseNo _ = empty
parseAnswer :: Alternative f => String -> f Answer
parseAnswer s = parseYes s <|> parseNo sМы также можем выбрать, какой Alternative мы запускаем наш парсер, чтобы получить различное поведение. Maybe дает нам только один результат, а [] дает нам все возможные результаты.
> parseAnswer "yes" :: Maybe Answer
Just Yes
> parseAnswer "maybe" :: Maybe Answer
Just Yes
> parseAnswer "yes" :: [Answer]
[Yes]
> parseAnswer "maybe" :: [Answer]
[Yes,No]Примеры Alternative для [] и Maybe неудивительны:
instance Alternative [] where
empty = []
(<|>) = (++)
instance Alternative Maybe where
empty = Nothing
Just x <|> _ = Just x
Nothing <|> mx = mxТип Validation также является Alternative. Этот экземпляр собирает вместе все сообщения об ошибках, как и экземпляр Applicative.
instance Alternative Validation where
empty = Errors []
Ok x <|> _ = Ok x
Errors e1 <|> Ok y = Ok y
Errors e1 <|> Errors e2 = Errors (e1++e2)Вот последний пример: проверка контактной информации, которая является либо номером телефона, либо адресом электронной почты.
data ContactInfo = Email String | Phone String
deriving Show
validateEmail :: String -> Validation ContactInfo
validateEmail s = check (elem '@' s) "Not an email: should contain a @" (Email s)
checkLength :: String -> Validation ContactInfo
checkLength s = check (length s <= 10) "Not a phone number: should be at most 10 digits" (Phone s)
checkDigits :: String -> Validation ContactInfo
checkDigits s = check (all isDigit s) "Not a phone number: should be all numbers" (Phone s)
validatePhone :: String -> Validation ContactInfo
validatePhone s = checkDigits s *> checkLength s
validateContactInfo :: String -> Validation ContactInfo
validateContactInfo s = validateEmail s <|> validatePhone svalidateContactInfo "[email protected]"
==> Ok (Email "[email protected]")
validateContactInfo "01234"
==> Ok (Phone "01234")
validateContactInfo "01234567890"
==> Errors ["Not an email: should contain a @","Not a phone number: should be at most 10 digits"]
validateContactInfo "01234567890x"
==> Errors ["Not an email: should contain a @",
"Not a phone number: should be all numbers",
"Not a phone number: should be at most 10 digits"]
validateContactInfo "x"
==> Errors ["Not an email: should contain a @",
"Not a phone number: should be all numbers"]Обратите внимание, что здесь, как и в предыдущих примерах, ошибки собираются слева направо: ошибки от validateEmail приходят раньше ошибок от validatePhone. Ошибка от checkDigits предшествует ошибке от checkLength.
Аппликативные функторы стоит изучить, хотя по выразительной силе они не превосходят монады. Во-первых, как обсуждалось в лекции 13, стандартная библиотека GHC требует, чтобы у каждой монады был экземпляр Applicative. Поэтому программист на Haskell неизбежно будет часто встречать аппликативный код.
Во-вторых, вы столкнетесь с операторами приложений даже в коде Monadic. Такие выражения, как f <$> x <*> y <*> z, могут быть полезны во многих монадических контекстах. Кроме того, поскольку класс типа Traversable построен с точки зрения Applicative, вы часто будете использовать аппликативные операции с ним.
В-третьих, приложения являются отличным упражнением в понимании функциональных шаблонов дизайна. Они сочетают модель Функтора с моделью Моноида, и Альтернатива привносит еще одно измерение, подобное Моноиду. Умение эффективно работать с приложениями облегчит работу с дополнительными абстракциями, такими как трансформаторы monad или lenses.
Наконец, существуют типы, являющиеся аппликативными функторами, но не монадами. Практический пример — Validation. Без понимания аппликативов мы не смогли бы распознавать и обобщать операции над такими типами. Другой пример — ZipList.
Хотя мы рассмотрели только некоторые очень простые и конкретные приложения в этой лекции, есть много библиотек Haskell, которые используют приложения для больших вещей. Вот несколько примеров.
Такая же идея, как и у нашего приложения Validation, реализована в библиотеках validation и either.
Существует несколько парсерных библиотек, которые используют приложения. Например, regex-applicative, optparse-applicative, yamlparse-applicative, json-stream и так далее.
Какова связь между монадой и аппликацией? Если заявитель также является монадой, действуют следующие законы:
pure === return
fmap === liftM
fmap f op === do x <- op
return (f x)
liftA2 === liftM2
liftA2 f op1 op2 === do x <- op1
y <- op2
return (f x y)
op1 *> op2 === op1 >> op2
op1 <*> op2 === do f <- op1
x <- op2
return (f x)При работе в Monad можно свободно сочетать операции Applicative и Functor с операциями Monad. Для примера перепишем mapM, оставив только аппликативные операции, и тем самым получим реализацию traverse. Начнём с такого определения:
myMapM op [] = return []
myMapM op (x:xs) = do y <- op x
ys <- myMapM op xs
return (y:ys)GHCi говорит нам, что это работает только для монад:
Prelude> :t myMapM
myMapM :: Monad m => (a -> m b) -> [a] -> m [b]Применим приведённые выше равенства pure === return и liftA2:
myMapM op [] = pure []
myMapM op (x:xs) = liftA2 (:) (op x) (myMapM op xs)Готово! Теперь myMapM работает для любого Applicative:
Prelude> :t myMapM
myMapM :: Applicative f => (a -> f b) -> [a] -> f [b]Какой тип x в liftA2 (&&) Nothing x?
Applicative f => f BoolApplicative BoolMaybe BoolСколько элементов у liftA2 f xs ys, когда xs и ys являются списками?
length xs + length yslength xs * length ysmin (length xs) (length ys)Какое из этих выражений эквивалентно liftA2 f x y? Может быть несколько правильных ответов.
f <$> x <*> yf <*> x <*> yf <*> x <$> yfmap f x <*> ypure f <*> x <*> yЕсли f :: a -> Maybe b и xs :: [a], то какое из этих выражений имеет тип, отличный от других?
fmap f xstraverse f xsmap f xsДля чего Applicative выдают разные результаты выражения pure x <* pure y и pure x <|> pure y?
Maybe[]ValidationЭта заключительная лекция будет посвящена некоторым второстепенным темам, которые больше нигде не вписываются. Вы закончили все сложные части курса. Теперь пришло время расслабиться и насладиться классным Хаскелом!
Одно из преимуществ чистоты состоит в том, что чистые функции легко тестировать: не нужно настраивать глобальное состояние — достаточно передать аргументы и проверить результат. В этом разделе мы кратко познакомимся с библиотекой для тестирования на основе свойств (property-based testing) QuickCheck. Она же используется для проверки решений упражнений этого курса.
Рассмотрим тестирование ошибочной реализации reverse. Этот и следующие примеры находятся в файле exercises/Examples/QuickCheck.hs.
rev :: [a] -> [a]
rev (x:y:xs) = y:x:xs
rev xs = xsМы можем написать индивидуальный тест-кейс с помощью оператора === от QuickCheck:
(===) :: (Eq a, Show a) => a -> a -> PropertypropRevSmall :: Property
propRevSmall = rev [1,2] === [2,1]Мы можем попросить QuickCheck запустить их в GHCi:
*Examples.QuickCheck> quickCheck propRevSmall
+++ OK, passed 1 test.Пока всё неплохо, но QuickCheck создавался не только для этого. Он предназначен для тестирования на основе свойств: вы задаёте свойство программы, а QuickCheck многократно запускает код со случайными входными данными и проверяет это свойство. Какое простое свойство есть у корректной реализации reverse? Если перевернуть список дважды, получится исходный список. Запишем это:
propRevTwice :: [Int] -> Property
propRevTwice xs = rev (rev xs) === xsНаш Property имеет аргумент, который означает, что QuickCheck будет генерировать случайные значения и запускать тест. Мы можем использовать функцию verboseCheck, чтобы увидеть, какие значения выполняются. Мы также можем сами дать параметр тесту, если хотим проверить конкретное значение.
*Examples.QuickCheck> quickCheck propRevTwice
+++ OK, passed 100 tests.
*Examples.QuickCheck> verboseCheck propRevTwice
Passed:
[]
[] == []
Passed:
[1]
[1] == [1]
Passed:
[-2,1,-1]
[-2,1,-1] == [-2,1,-1]
-- lots of output
+++ OK, passed 100 tests.
*Examples.QuickCheck> quickCheck (propRevTwice [1,2,3])
+++ OK, passed 1 test.Даже это свойство не уловило ошибку в нашей реализации. Давайте попробуем еще один. Вот пример того, как ведет себя rev (xs ++ ys). Возможно, вам понадобится минутка, чтобы убедить себя, что он должен поддерживать правильную функцию rev.
propRevTwo :: [Int] -> [Int] -> Property
propRevTwo xs ys = rev (xs ++ ys) === rev ys ++ rev xsПосмотрим, будет ли это способствовать нашей реализации:
*Examples.QuickCheck> quickCheck propRevTwo
*** Failed! Falsified (after 5 tests and 3 shrinks):
[0,0]
[1]
[0,1,0] /= [1,0,0]Наконец, провал! Здесь есть что распаковать. Во-первых, QuickCheck сообщает нам аргументы, с которыми имущество не удалось: это [0,0] и [1]. Мы можем проверить это сами:
*Examples.QuickCheck> quickCheck (propRevTwo [0,0] [1])
*** Failed! Falsified (after 1 test):
[0,1,0] /= [1,0,0]Что означает «после 5 тестов и 3 сокращений»? Одна из интересных вещей в QuickCheck заключается в том, что, когда он находит неудачу, он пробует некоторые связанные значения, чтобы найти более приятный, меньший провал. Мы можем увидеть это в действии с verboseShrinking, который распечатывает все сбои, через которые проходит QuickCheck:
*Examples.QuickCheck> quickCheck (verboseShrinking propRevTwo)
Failed:
[4,-1,-4]
[1,4]
[-1,-4,1,4,4] /= [4,1,-1,-4,4]
Failed:
[-1,-4]
[1,4]
[-4,1,4,-1] /= [4,1,-4,-1]
Failed:
[-4]
[1,4]
[1,4,-4] /= [4,1,-4]
Failed:
[4]
[1,4]
[1,4,4] /= [4,1,4]
Failed:
[0]
[1,4]
[1,4,0] /= [4,1,0]
Failed:
[0]
[0,4]
[0,4,0] /= [4,0,0]
Failed:
[0]
[0,2]
[0,2,0] /= [2,0,0]
Failed:
[0]
[0,1]
[0,1,0] /= [1,0,0]QuickCheck прошел путь от контрпримера [4,1,-1,4,4] до [1,0,0]. Очень мило!
Иногда вам нужно ограничить значения, генерируемые QuickCheck. Ваша функция может работать не на всех входах, например? Давайте попробуем написать тест для last.
propLast :: [Int] -> Property
propLast xs = last xs === head (reverse xs)*Examples.QuickCheck> quickCheck propLast
*** Failed! Exception: 'Prelude.last: empty list' (after 1 test):
[]В этом случае мы можем исправить тест, просто перейдя на другой тип ввода. QuickCheck определяет тип NonEmptyList (не путать с Data.List.NonEmpty!), который является просто оберткой для обычного списка. Однако при генерации значений NonEmptyList QuickCheck не будет генерировать пустые списки.
newtype NonEmptyList a = NonEmpty [a]propLastFixed :: NonEmptyList Int -> Property
propLastFixed (NonEmpty xs) = last xs === head (reverse xs)*Examples.QuickCheck> quickCheck propLastFixed
+++ OK, passed 100 tests.Существуют другие модификаторы: например, Positive для положительных чисел, NonNegative для неотрицательных или SortedList для отсортированных списков. Ниже приведён более сложный тест: он проверяет корректность n-го элемента cycle xs. Оба модификатора необходимы, поскольку !! не принимает отрицательные индексы, а cycle [] завершается ошибкой.
propCycle :: NonEmptyList Int -> NonNegative Int -> Property
propCycle (NonEmpty xs) (NonNegative n) =
cycle xs !! n === xs !! (mod n (length xs))forAllИногда нам нужно еще больше ограничить диапазон входных данных для тестирования. В качестве простого примера, вот тест, который Data.Char.toUpper меняет переданный ему персонаж:
propToUpperChanges :: Char -> Property
propToUpperChanges c = toUpper c =/= cquickCheck propToUpperChanges
*** Failed! Falsified (after 1 test and 1 shrink):
'A'
'A' == 'A'Конечно, он меняет только буквы lowercase. Как мы можем написать тест для этого? Не существует модификатора Lowercase, который работал бы как Positive или NonEmptyList. Мы должны вернуться к явной генерации значений с использованием forAll:
propToUpperChangesLetter :: Property
propToUpperChangesLetter = forAll (elements ['a'..'z']) propToUpperChanges*Examples.QuickCheck> verboseCheck propToUpperChangesLetter
Passed:
's'
'S' /= 's'
Passed:
'z'
'Z' /= 'z'
-- lots of output omitted
+++ OK, passed 100 tests.Отлично! Давайте посмотрим на типы, чтобы увидеть, что здесь происходит.
elements :: [a] -> Gen a
elements ['a'..'z'] :: Gen Char
forAll :: (Show a, Testable prop) => Gen a -> (a -> prop) -> Property
forAll (elements ['a'..'z']) :: Testable prop => (Char -> prop) -> PropertyЗдесь есть несколько новых типов. Значение типа Gen a является генератором для значений типа a. В следующем разделе мы поговорим немного больше об Gen, но в этом разделе вы увидите несколько функций, которые возвращают Gen, чтобы мы могли использовать их с forAll. Функция elements является генератором, который возвращает один из элементов данного списка наугад, как вы могли догадаться.
Класс типа Testable такой же, как и функция quickCheck. Существует так, что quickCheck может тестировать такие типы, как [Int] -> Bool -> Property, в дополнение к простым значениям Property.
quickCheck :: Testable prop => prop -> IO ()Кроме того, некоторые простые типы, такие как Bool, имеют экземпляр Testable, так что вы можете писать тесты с использованием обычных предикатов Haskell вместо ===:
listHasZero :: [Int] -> Bool
listHasZero xs = elem 0 xs*Examples.QuickCheck> quickCheck (listHasZero [1,0,2])
+++ OK, passed 1 test.
*Examples.QuickCheck> quickCheck listHasZero
*** Failed! Falsified (after 1 test):
[]Возвращаясь к forAll, мы можем использовать forAll для написания более сложных тестов. Вот тест, который проверяет, что sort xs имеет те же элементы, что и xs. Обратите внимание, как мы используем NonEmptyList, чтобы гарантировать, что forAll имеет некоторые элементы для выбора.
propSort :: NonEmptyList Int -> Property
propSort (NonEmpty xs) =
forAll (elements xs) (\x -> elem x (sort xs))Мы только что поцарапали поверхность QuickCheck. Вот некоторые указания на некоторые вещи, которые вы найдете полезными, когда начнете писать более крупные тесты QuickCheck.
Иногда результаты QuickCheck недостаточно многословны. Вы можете добавить свои собственные линии на выход, используя комбинатор counterexample:
counterexample :: Testable prop => String -> prop -> PropertyВ качестве примера добавим запись входа в rev в propRevTwo:
propRevTwo' :: [Int] -> [Int] -> Property
propRevTwo' xs ys =
let input = xs ++ ys
in counterexample ("Input: " ++ show input) $
rev input === rev ys ++ rev xs*Examples.QuickCheck> quickCheck propRevTwo'
*** Failed! Falsified (after 4 tests and 5 shrinks):
[0]
[0,1]
Input: [0,0,1]
[0,1,0] /= [1,0,0]Как вы уже догадались, Gen является Monad. Вы можете написать свои собственные генераторы, объединив генераторы, определенные QuickCheck. Вы можете проверить выход генераторов с помощью sample.
someLetters :: Gen String
someLetters = do
c <- elements "xyzw"
n <- choose (1,10)
return (replicate n c)*Examples.QuickCheck> sample someLetters
"yyyyyyyy"
"zzzzzzzzz"
"xxxxxxxxx"
"yyyyyyy"
"yyy"
"ww"
"xxxxxx"
"yyy"
"yyyyyyy"
"xxxxxxxxxx"
"y"Близко к генераторам относится класс типа Arbitrary. Arbitrary - это то, как QuickCheck автоматически генерирует все эти входные данные.
class Arbitrary a where
arbitrary :: Gen a
shrink :: a -> [a]Если вы пишете тесты для пользовательских типов, вам нужно либо использовать forAll, либо реализовать экземпляр Arbitrary. Вот что происходит, если вы упускаете один случай:
data Switch = On | Off
deriving (Show, Eq)
toggle :: Switch -> Switch
toggle On = Off
toggle Off = On
propToggleTwice :: Switch -> Property
propToggleTwice s = s === toggle (toggle s)*Examples.QuickCheck> quickCheck propToggleTwice
error:
• No instance for (Arbitrary Switch)
arising from a use of ‘quickCheck’
• In the expression: quickCheck propToggleTwiceВот два способа исправить это:
*Examples.QuickCheck> quickCheck (forAll (elements [On,Off]) propToggleTwice)
+++ OK, passed 100 tests.instance Arbitrary Switch where
arbitrary = elements [On,Off]Бу! В системе типов есть призрак! Давайте посмотрим, что phantom Types может сделать для вас.
Фантомные типы — это типы, которые не принимают никаких значений. Они связаны с новыми типами (см. лекцию 10), поскольку оба являются способом добавления дополнительной проверки типа, не влияя на оценку программы вообще.
Давайте используем фантомные типы, чтобы отслеживать, в какой валюте находится сумма денег. Мы определяем фантомные типы EUR и USD (обратите внимание, что у них нет конструкторов!) и параметризованный тип Money a, который не использует параметр типа a ни для чего. Затем можно определить две константы, одна в евро, а другая в долларах. Вы можете найти весь код из этого раздела в файле exercises/Examples/Phantom.hs.
data EUR
data USD
data Money currency = Money Double
deriving Show
dollar :: Money USD
dollar = Money 1
twoEuros :: Money EUR
twoEuros = Money 2Обратите внимание, что все работы по типу подписей для dollar и twoEuros выполняются здесь. Выражение Money 1 имеет полиморфный тип Money currency, если не ограничивать его более конкретным типом. Мы явно даем dollar и twoEuros более ограниченные типы. Это аналогично определению чего-то вроде one :: Int; one = 1, поскольку константа 1 имеет полиморфный тип Num p => p, но мы даем ему более ограниченный тип.
*Examples.Phantom> :t Money
Money :: Double -> Money currency
*Examples.Phantom> :t Money 1
Money 1 :: Money currencyТеперь, когда у нас есть некоторые константы, мы можем писать функции, которые работают на них. Начнем с функции scaleMoney, которая умножает сумму денег на число. Валюта остается постоянной. Здесь тоже работает подпись типа: без подписи типа Хаскелл вывел бы тип Double -> Money a -> Money b.
scaleMoney :: Double -> Money currency -> Money currency
scaleMoney factor (Money a) = Money (factor * a)*Examples.Phantom> :t scaleMoney 3 twoEuros
scaleMoney 3 twoEuros :: Money EUR
*Examples.Phantom> :t scaleMoney 3 dollar
scaleMoney 3 dollar :: Money USDДалее: добавление двух сумм в одной валюте. Мы получаем хорошую ошибку типа, если мы пытаемся добавить значения в двух разных валютах.
addMoney :: Money currency -> Money currency -> Money currency
addMoney (Money a) (Money b) = Money (a+b)*Examples.Phantom> :t addMoney dollar dollar
addMoney dollar dollar :: Money USD
*Examples.Phantom> :t addMoney twoEuros twoEuros
addMoney twoEuros twoEuros :: Money EUR
*Examples.Phantom> :t addMoney twoEuros dollar
error:
• Couldn't match type ‘USD’ with ‘EUR’
Expected type: Money EUR
Actual type: Money USD
• In the second argument of ‘addMoney’, namely ‘dollar’
In the expression: addMoney twoEuros dollarКак и прежде, ключевое значение имеет тип подписи. Вот та же реализация с неограниченным типом. Теперь мы можем добавить что угодно к чему угодно!
addMoneyUnsafe :: Money x -> Money y -> Money z
addMoneyUnsafe (Money a) (Money b) = Money (a+b)*Examples.Phantom> addMoneyUnsafe twoEuros dollar
Money 3.0Мы можем продолжать придерживаться этого подхода и определять конверсию валюты. Мы определяем тип Rate, который использует фантомные типы для отслеживания валют, которые он переводит. Типы convert и invert ограничены, чтобы иметь свойства, которые мы хотим. Существует также неограниченная версия функции преобразования, позволяющая сравнивать типы.
data Rate from to = Rate Double
deriving Show
eurToUsd :: Rate EUR USD
eurToUsd = Rate 1.22
convert :: Rate from to -> Money from -> Money to
convert (Rate r) (Money a) = Money (r*a)
invert :: Rate from to -> Rate to from
invert (Rate r) = Rate (1/r)
convertUnsafe :: Rate from to -> Money x -> Money y
convertUnsafe (Rate r) (Money a) = Money (r*a)*Examples.Phantom> convert eurToUsd twoEuros
Money 2.44
*Examples.Phantom> convert eurToUsd dollar
error:
• Couldn't match type ‘USD’ with ‘EUR’
Expected type: Money EUR
Actual type: Money USD
• In the second argument of ‘convert’, namely ‘dollar’
In the expression: convert eurToUsd dollar
In an equation for ‘it’: it = convert eurToUsd dollar
*Examples.Phantom> convert (invert eurToUsd) dollar
Money 0.819672131147541
*Examples.Phantom> convertUnsafe eurToUsd dollar
Money 1.22Записка! Слова currency, from,
to И так далее в предыдущих примерах Только переменные типа Ничего особенного с ними не происходит. Мы могли бы также дать invert Тип как Rate a b -> Rate b a без каких-либо изменений в типе безопасности.
Этот подход, использующий фантомные типы, имеет явные преимущества: дает нам ошибки типов для недействительного кода. Кроме того, по сравнению с определением множества конкретных типов, таких как data MoneyEur = MoneyEur Double, с фантомными типами нам нужно реализовать такие функции, как scaleMoney и addMoney только один раз. Кроме того, мы можем определить полиморфные и многоразовые концепции, такие как Rate. Вы можете сравнить этот подход с разделом 7.
Тем не менее, фантомные типы также имеют недостатки. Без продвинутых трюков мы не сможем справиться с валютами, которые определяются во время выполнения (например, чтение суммы от пользователя). Также легко оказаться в месте, где вам понадобятся языковые расширения, такие как Generalized Algebraic Datatypes, type family и другие type-level programming сборки. В конце концов, вы находитесь в мире dependent typing.
Итак, каковы хорошие приложения для фантомных типов? Когда вам нужно отследить некоторую простую, но важную информацию, это известно во время компиляции. Примером, который несколько лучше, чем валюты, является отслеживание того, были ли введены данные от пользователя для предотвращения атак, таких как SQL injection или cross-site scripting.
Мы можем использовать типы Input Safe и Input Unsafe, чтобы отслеживать, безопасны ли строки для прохождения в базу данных или нет. Если наш модуль экспортирует только функцию makeInput, а не конструктор Input, система типа гарантирует, что любые входы должны пройти через функцию escapeInput в какой-то момент, прежде чем войти в функцию базы данных, такую как addForumComment.
data Safe
data Unsafe
data Input a = Input String
-- Public constructor function for Input, only allows constructing
-- Unsafe Inputs from Strings.
makeInput :: String -> Input Unsafe
makeInput xs = Input xs
-- Adds comment to the database.
addForumComment :: Input Safe -> IO Result
addForumComment = ...
-- We can combine inputs, but that won't change their safety
concatInputs :: Input a -> Input a -> Input a
concatInputs (Input xs) (Input ys) = Input (xs++ys)
-- Strip bad characters to turn an unsafe input safe
escapeInput :: Input Unsafe -> Input Safe
escapeInput (Input xs) = Input (filter (\c -> isAlpha c || isSpace c) xs)Одно из замечательных следствий чистоты — она сильно упрощает параллелизм (parallelism), то есть одновременное выполнение нескольких вычислений. Посмотрим, как это делается в Haskell. Сначала запустим новый сеанс GHCi с поддержкой параллельного выполнения:
$ stack ghci --ghci-options "+RTS -N"
Далее, давайте определим очень наивную версию функции Фибоначчи (помните лекцию 1?), включим статистику производительности с помощью :set +s и посмотрим, сколько времени требуется для вычисления пяти значений функции:
Prelude> fib 0 = 1; fib 1 = 1; fib n = fib (n-1) + fib (n-2)
Prelude> :set +s
Prelude> map fib [29,29,29,29,29]
[832040,832040,832040,832040,832040]
(7.54 secs, 2,440,860,632 bytes)Теперь введем модуль Control.Parallel.Strategies определяет стратегии параллельного вычисления значений. Мы воспользуемся стратегией parList rseq, чтобы параллельно вычислить все элементы списка до WHNF.
Prelude> import Control.Parallel.Strategies
Prelude Control.Parallel.Strategies> withStrategy (parList rseq) (map fib [29,29,29,29,29])
[832040,832040,832040,832040,832040]
(4.80 secs, 488,531,384 bytes)На двухъядерной машине из примера это почти вдвое быстрее. Особенно важно, что само вычисление (computation) (map fib ...) удалось определить независимо от стратегии вычисления (evaluation strategy) (parList rseq): мы отделили что вычислять от того, как вычислять.
В информатике различают параллельные и конкурентные (concurrent) вычисления. При параллельных вычислениях независимые задачи просто выполняются одновременно; иначе говоря, параллелизм может оставаться чистым (pure). В конкурентных вычислениях взаимодействуют несколько потоков выполнения. Конкурентность обычно связана с потоками, блокировками, сообщениями и взаимными блокировками.
Помимо удобных средств параллелизма, Haskell хорошо поддерживает конкурентность с помощью потоков (threads). Поскольку конкурентность связана с побочными эффектами, конкурентные вычисления выполняются в монаде IO. Классический пример — два потока: один печатает последовательность букв A, другой — букв B. Вот как это выглядит в Haskell:
printA :: IO ()
printA = putStrLn (replicate 40 'A')
printB :: IO ()
printB = putStrLn (replicate 40 'B')
concurrency :: IO ()
concurrency = do
forkIO printA
forkIO printB
return ()Prelude Control.Concurrent> concurrency
AABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABABB
Операция forkIO :: IO () -> IO ThreadId берет операцию ввода-вывода и начинает выполнять ее в фоновом режиме. Он производит ThreadId, который может использоваться, например, для завершения потока.
Если мы хотим добавить фактическую связь между потоками, мы можем использовать абстракции, такие как MVar (изменяемая безвредная переменная) или Chan (очередь).
Вот простой пример, когда один поток записывает значение в MVar, а другой ждет их и печатает. MVar работает как почтовый ящик: он либо пустой, либо полный. Вызов takeMVar на пустой коробке ждет, когда коробка будет заполнена (с putMVar). Симметрично, пытаясь putMVar в полный ящик, ждет, пока ящик не опустеет.
takeMVar :: MVar a -> IO a
putMVar :: MVar a -> a -> IO ()
newEmptyMVar :: IO (MVar a)send :: [String] -> MVar String -> IO ()
send values var = mapM_ (putMVar var) values
receive :: MVar String -> IO ()
receive var = do val <- takeMVar var
print val
-- loop unless at last value
when (val/="end") (receive var)
concurrency2 :: IO ()
concurrency2 = do
var <- newEmptyMVar
forkIO (send ["hello","world","and","goodbye","end"] var)
forkIO (receive var)
return ()Prelude Control.Concurrent Control.Monad> concurrency2
"hello"
"world"
"and"
"goodbye"
"end"Поздравляю! Вы закончили этот курс по функциональному программированию в Haskell. Что дальше? Вы определенно знаете достаточно Хаскела, чтобы продолжать учиться самостоятельно. Онлайн-сообщество Haskell очень дружелюбно, и есть много сообщений в блогах и другого контента, объясняющего передовые методы и функции. Вы можете найти много интересных вещей, например:
#haskell на libera.chatПросто продолжайте писать Haskell, изучая вещи (например, библиотеки и инструменты), когда вы сталкиваетесь с ними. Многие вещи, которые вы изучаете с Haskell, могут быть переведены на другие языки, такие как TypeScript, Elm, Rust или F#.
Наконец, вот неполный список вещей, которые остались в стороне от этого курса, но стоит изучить:
~) и @. См., например, A Нежное введение в Haskell.MultiParamTypeClasses, ViewPatterns и т. Д. Это один хороший гидfixЭтот курс стал возможен благодаря Нитор Он пожертвовал на этот проект часы и часы рабочего времени Джоэла. Спасибо!
Спасибо всей команде Haskell MOOC, особенно
Спасибо всем студентам, которые терпеливо ждали часть 2 и сообщали об ошибках в материале.&Упражнения!