В Haskell существует несколько встроенных выражений, которые облегчают построение функций и делают код более наглядным. Их можно разделить на два вида: выражения, которые поддерживают декларативный стиль (declarative style) определения функций, и выражения которые поддерживают композиционный стиль (expression style).
Что это за стили? В декларативном стиле определения функций больше похожи на математическую нотацию, словно это предложения языка. В композиционном стиле мы строим из маленьких выражений более сложные, применяем к этим выражениям другие выражения и строим ещё большие.
В Haskell есть полноценная поддержка и того и другого стиля, поэтому конструкции которые мы рассмотрим в этой главе будут по смыслу дублировать друг друга. Выбор стиля скорее дело вкуса, существуют приверженцы и того и другого стиля, поэтому разработчики Haskell не хотели никого ограничивать.
Вспомним формулу вычисления площади треугольника по трём сторонам:
$$S = \sqrt{p \cdot (p - a) \cdot (p - b) \cdot (p - c)}$$
Где $a$, $b$ и $c$ – длины сторон треугольника, а $p$ это полупериметр.
Как бы мы определили эту функцию теми средствами, что у нас есть? Наверное, мы бы написали так:
square a b c = sqrt (p a b c * (p a b c - a) * (p a b c - b) * (p a b c - c)) p a b c = (a + b + c) / 2
Согласитесь это не многим лучше чем решение в лоб:
square a b c = sqrt ((a+b+c)/2 * ((a+b+c)/2 - a) * ((a+b+c)/2 - b) * ((a+b+c)/2 - c))
И в том и в другом случае нам приходится дублировать выражения, нам бы хотелось чтобы определение выглядело так же, как и обычное математическое определение:
square a b c = sqrt (p * (p - a) * (p - b) * (p - c)) p = (a + b + c) / 2
Нам нужно, чтобы p
знало, что a
, b
и c
берутся из аргументов функции square
. В этом нам помогут локальные переменные.
В декларативном стиле для этого предусмотрены where
-выражения. Они пишутся так:
square a b c = sqrt (p * (p - a) * (p - b) * (p - c)) where p = (a + b + c) / 2
Или так:
square a b c = sqrt (p * (p - a) * (p - b) * (p - c)) where p = (a + b + c) / 2
За определением функции следует специальное слово where
, которое вводит локальные имена-синонимы. При этом аргументы функции включены в область видимости имён. Синонимов может быть несколько:
square a b c = sqrt (p * pa * pb * pc) where p = (a + b + c) / 2 pa = p - a pb = p - b pc = p - c
Отметим, что отступы обязательны. Haskell по отступам понимает, что эти выражения относятся к where
.
Как и в случае объявления функций порядок следования локальных переменных в where
-выражении не важен. Главное чтобы в выражениях справа от знака равно мы пользовались именами из списка аргументов исходной функции или другими определёнными именами. Локальные переменные видны только в пределах той функции, в которой они вводятся.
Что интересно, слева от знака равно в where
-выражениях можно проводить декомпозицию значений, также как и в аргументах функции:
pred :: Nat -> Nat pred x = y where (Succ y) = x
Эта функция делает тоже самое что и функция
pred :: Nat -> Nat pred (Succ y) = y
В where
-выражениях можно определять новые функции а также выписывать их типы:
add2 x = succ (succ x) where succ :: Int -> Int succ x = x + 1
А можно и не выписывать, компилятор догадается:
add2 x = succ (succ x) where succ x = x + 1
Но иногда это бывает полезно, при использовании классов типов, для избежания неопределённости применения.
Приведём ещё один пример. Посмотрим на функцию фильтрации списков, она определена в Prelude
:
filter :: (a -> Bool) -> [a] -> [a] filter p [] = [] filter p (x:xs) = if p x then x : rest else rest where rest = filter p xs
Мы определили локальную переменную rest
, которая указывает на рекурсивный вызов функции на оставшейся части списка.
where
-выражения определяются для каждого уравнения в определении функции:
even :: Nat -> Bool even Zero = res where res = True even (Succ Zero) = res where res = False even x = even res where (Succ (Succ res)) = x
Конечно в этом примере where
не нужны, но здесь они приведены для иллюстрации привязки where
-выражения к данному уравнению. Мы определили три локальных переменных с одним и тем же именем.
where
-выражения могут быть и у значений, которые определяются внутри where
-выражений. Но лучше избегать сильно вложенных выражений.
В композиционном стиле функция вычисления площади треугольника будет выглядеть так:
square a b c = let p = (a + b + c) / 2 in sqrt (p * (p - a) * (p - b) * (p - c))
Слова let
и in
– ключевые. Выгодным отличием let
-выражений является то, что они являются обычными выражениями и не привязаны к определённому месту как where
-выражения. Они могут участвовать в любой части обычного выражения:
square a b c = let p = (a + b + c) / 2 in sqrt ((let pa = p - a in p * pa) * (let pb = p - b pc = p - c in pb * pc))
В этом проявляется их принадлежность композиционному стилю. let
-выражения могут участвовать в любом подвыражении, они также группируются скобками. А where
-выражения привязаны к уравнениям в определении функции.
Также как и в where
-выражениях, в let
-выражениях слева от знака равно можно проводить декомпозицию значений.
pred :: Nat -> Nat pred x = let (Succ y) = x in y
Определим функцию фильтрации списков через let
:
filter :: (a -> Bool) -> [a] -> [a] filter p [] = [] filter p (x:xs) = let rest = filter p xs in if p x then x : rest else rest
Декомпозиция или сопоставление с образцом позволяет выделять из составных значений, простейшие значения с помощью которых они были построены
pred (Succ x) = x
и организовывать условные вычисления которые зависят от вида поступающих на вход функции значений
not True = False not False = True
Декомпозицию в декларативном стиле мы уже изучили, это обычный случай разбора значений в аргументах функции. Рассмотрим одну полезную возможность при декомпозиции. Иногда нам хочется провести декомпозицию и дать псевдоним всему значению. Это можно сделать с помощью специального символа @
.
Например определим функцию, которая возвращает соседние числа для данного числа Пеано:
beside :: Nat -> (Nat, Nat) beside Zero = error "undefined" beside x@(Succ y) = (y, Succ x)
В выражении x@(Succ y)
мы одновременно проводим разбор и даём имя всему значению.
Оказывается декомпозицию можно проводить в любом выражении, для этого существуют case
-выражения:
data AnotherNat = None | One | Two | Many deriving (Show, Eq) toAnother :: Nat -> AnotherNat toAnother x = case x of Zero -> None Succ Zero -> One Succ (Succ Zero) -> Two _ -> Many fromAnother :: AnotherNat -> Nat fromAnother None = Zero fromAnother One = Succ Zero fromAnother Two = Succ (Succ Zero) fromAnother Many = error "undefined"
Слова case
и of
– ключевые. Выгодным отличием case
-выражений является то, что нам не приходится каждый раз выписывать имя функции. Обратите внимание на то, что в case
-выражениях также можно пользоваться обычными переменными и безымянными переменными.
Для проведения декомпозиции по нескольким переменным можно воспользоваться кортежами. Например определим знакомую функцию равенства для Nat
:
instance Eq Nat where (==) a b = case (a, b) of (Zero, Zero) -> True (Succ a', Succ b') -> a' == b' _ -> False
Мы проводим сопоставление с образцом по кортежу (a, b)
, соответственно слева от знака ->
мы проверяем значения в кортежах, для этого мы также заключаем значения в скобки и пишем их через запятую.
Давайте определим функцию filter
в ещё более композиционном стиле. Для этого мы заменим в исходном определении where
на let
и декомпозицию в аргументах на case
-выражение:
filter :: (a -> Bool) -> [a] -> [a] filter p a = case a of [] -> [] x:xs -> let rest = filter p xs in if (p x) then (x:rest) else rest
С условными выражениями мы уже сталкивались в сопоставлении с образцом. Например в определении функции not
:
not True = False not False = True
В зависимости от поступающего значения мы выбираем одну из двух альтернатив. Условные выражении в сопоставлении с образцом позволяют реагировать лишь на частичное (с учётом переменных) совпадение дерева значения в аргументах функции.
Часто нам хочется определить более сложные условия для альтернатив. Например, если значение на входе функции больше 2, но меньше 10, верни A
, а если больше 10, верни B
, а во всех остальных случаях верни C
. Или если на вход поступила строка состоящая только из букв латинского алфавита, верни A
, а в противном случае верни B
. Нам бы хотелось реагировать лишь в том случае, если значение некоторого типа a
удовлетворяет некоторому предикату. Предикатами обычно называют функции типа a -> Bool
. Мы говорим, что значение удовлетворяет предикату, если предикат для этого значения возвращает True
.
В декларативном стиле условные выражения представлены охранными выражениями (guards). Предположим у нас есть тип:
data HowMany = Little | Enough | Many
И мы хотим написать функцию, которая принимает число людей, которые хотят посетить выставку, а возвращает значение типа HowMany
. Эта функция оценивает вместительность выставочного зала. С помощью охранных выражений мы можем написать её так:
hallCapacity :: Int -> HowMany hallCapacity n | n < 10 = Little | n < 30 = Enough | True = Many
Специальный символ |
уже встречался нам в определении типов. Там он играл роль разделителя альтернатив в сумме типов. Здесь же он разделяет альтернативы в условных выражениях. Сначала мы пишем |
затем выражение-предикат, которое возвращает значение типа Bool
, затем равно и после равно – возвращаемое значение. Альтернативы так же как и в случае декомпозиции аргументов функции обходятся сверху вниз, до тех пор пока в одной из альтернатив предикат не вернёт значение True
. Обратите внимание на то, что нам не нужно писать во второй альтернативе:
| 10 <= n && n < 30 = Enough
Если вычислитель дошёл до этой альтернативы, значит значение точно больше либо равно 10
. Поскольку в предыдущей альтернативе предикат вернул False
.
Предикат в последней альтернативе является константой True
, он пройдёт сопоставление с любым значением n
. В данном случае, если учесть предыдущие альтернативы мы знаем, что если вычислитель дошёл до последней альтернативы , значение n
больше либо равно 30
. Для повышения наглядности кода в Prelude
определена специальная константа-синоним значению True
под именем otherwise
.
Определим функцию filter
для списков в более декларативном стиле, для этого заменим if
-выражение в исходной версии на охранные выражения:
filter :: (a -> Bool) -> [a] -> [a] filter p [] = [] filter p (x:xs) | p x = x : rest | otherwise = rest where rest = filter p xs
Или мы можем разместить охранные выражения по-другому:
filter :: (a -> Bool) -> [a] -> [a] filter p [] = [] filter p (x:xs) | p x = x : rest | otherwise = rest where rest = filter p xs
Отметим то, что локальная переменная rest
видна и в той и в другой альтернативе. Вы спокойно можете пользоваться локальными переменными в любой части уравнения, в котором они определены.
Определим с помощью охранных выражений функцию all
, она принимает предикат и список, и проверяет удовлетворяют ли все элементы списка данному предикату.
all :: (a -> Bool) -> [a] -> Bool all p [] = True all p (x:xs) | p x = all p xs | otherwise = False
С помощью охранных выражений можно очень наглядно описывать условные выражения. Но иногда можно обойтись и простыми логическими операциями. Например функцию all
можно было бы определить так:
all :: (a -> Bool) -> [a] -> Bool all p [] = True all p (x:xs) = p x && all p xs
Или так:
all :: (a -> Bool) -> [a] -> Bool all p xs = null (filter notP xs) where notP x = not (p x)
Или даже так:
import Prelude(all)
Функция null
определена в Prelude
она возвращает True
только если список пуст.
В композиционном стиле в качестве условных выражений используются уже знакомые нам if
-выражения. Вспомним как они выглядят:
a = if bool then x1 else x2
Слова if
, then
и else
– ключевые. Тип a
, x1
и x2
совпадают.
Любое охранное выражение, в котором больше одной альтернативы, можно представить в виде if
-выражения и наоборот. Перепишем все функции их предыдущего подраздела с помощью if
-выражений:
hallCapacity :: Int -> HowMany hallCapacity n = if (n < 10) then Little else (if n < 30 then Enough else Many) all :: (a -> Bool) -> [a] -> Bool all p [] = True all p (x:xs) = if (p x) then all p xs else False
Под функцией мы понимаем составной синоним, который принимает аргументы, возможно разбирает их на части и составляет из этих частей новые выражения. Теперь посмотрим как такие синонимы определяются в каждом из стилей.
В декларативном стиле функции определяются с помощью уравнений. Пока мы видели лишь этот способ определения функций, примерами могут служить все предыдущие примеры. Вкратце напомним, что функция определяется набором уравнений вида:
name декомпозиция1 = композиция1 name декомпозиция2 = композиция2 ... name декомпозицияN = композицияN
Где name
– имя функции. В декомпозиции
происходит разбор поступающих на вход значений, а в композиции
происходит составление значения результата. Уравнения обходятся вычислителем сверху вниз до тех пор пока он не найдёт такое уравнение, для которого переданные в функции значения не подойдут в указанный в декомпозиции шаблон значений (если сопоставление с образцом аргументов пройдёт успешно). Как только такое уравнение найдено, составляется выражение справа от знака равно (композиция
). Это значение будет результатом функции. Если такое уравнение не будет найдено программа остановится с ошибкой.
К примеру попробуйте вычислить в интерпретаторе выражение notT False
, для такой функции:
notT :: Bool -> Bool notT True = False
Что мы увидим?
Prelude> notT False *** Exception: <interactive>:1:4-20: Non-exhaustive patterns in function notT
Интерпретатор сообщил нам о том, что он не нашёл уравнения для переданного в функцию значения.
В композиционном стиле функции определяются по-другому. Это необычный метод, он пришёл в Haskell из лямбда-исчисления. Функции строятся с помощью специальных конструкций, которые называются лямбда-функциями. По сути лямбда-функции являются безымянными функциями. Давайте посмотрим на лямбда функцию, которая прибавляет к аргументу единицу:
\x -> x + 1
Для того, чтобы превратить лямбда-функцию в обычную функцию мысленно замените знак \
на имя noName
, а стрелку на знак равно:
noName x = x + 1
Мы получили обычную функцию Haskell, с такими мы уже много раз встречались. Зачем специальный синтаксис для определения безымянных функций? Ведь можно определить её в виде уравнений. К тому же кому могут понадобиться безымянные функции? Ведь смысл функции в том, чтобы выделить определённый шаблон поведения и затем ссылаться на него по имени функции.
Смысл безымянной функции в том, что ею, также как и любым другим элементом композиционного стиля, можно пользоваться в любой части обычных выражений. С её помощью мы можем создавать функции “на лету”. Предположим, что мы хотим профильтровать список чисел, мы хотим выбрать из них лишь те, что меньше 10, но больше 2, и к тому же они должны быть чётными. Мы можем написать:
f :: [Int] -> [Int] f = filter p where p x = x > 2 && x < 10 && even x
При этом нам приходится давать какое-нибудь имя предикату, например p
. С помощью безымянной функции мы могли бы написать так:
f :: [Int] -> [Int] f = filter (\x -> x > 2 && x < 10 && even x)
Смотрите мы составили предикат сразу в аргументе функции filter
. Выражение (\x -> x > 2 && x < 10 && even x)
является обычным значением.
Возможно у вас появился вопрос, где аргумент функции? Где тот список по которому мы проводим фильтрацию. Ответ на этот вопрос кроется в частичном применении. Давайте вычислим по правилу применения тип функции filter
:
f :: (a -> Bool) -> [a] -> [a], x :: (Int -> Bool) ------------------------------------------------------ (f x) :: [Int] -> [Int]
После применения параметр a
связывается с типом Int
, поскольку при применении происходит сопоставление более общего предиката a -> Bool
из функции filter
с тем, который мы передали первым аргументом Int -> Bool
. После этого мы получаем тип (f x) :: [Int] -> [Int]
это как раз тип функции, которая принимает список целых чисел и возвращает список целых чисел. Частичное применение позволяет нам не писать в таких выражениях:
f xs = filter p xs where p x = ...
последний аргумент xs
.
К примеру вместо
add a b = (+) a b
мы можем просто написать:
add = (+)
Такой стиль определения функций называют бесточечным (point-free).
Давайте выразим функцию filter
с помощью лямбда-функций:
filter :: (a -> Bool) -> ([a] -> [a]) filter = \p -> \xs -> case xs of [] -> [] (x:xs) -> let rest = filter p xs in if p x then x : rest else rest
Мы определили функцию filter
пользуясь только элементами композиционного стиля. Обратите внимание на скобки в объявлении типа функции. Я хотел напомнить вам о том, что все функции в Haskell являются функциями одного аргумента. Это определение функции filter
как нельзя лучше подчёркивает этот факт. Мы говорим, что функция filter
является функцией одного аргумента p
в выражении \p ->
, которая возвращает также функцию одного аргумента. Мы выписываем это в явном виде в выражении \xs ->
. Далее идёт выражение, которое содержит определение функции.
Отметим, что лямбда функции могут принимать несколько аргументов, в предыдущем определении мы могли бы написать:
filter :: (a -> Bool) -> ([a] -> [a]) filter = \p xs -> case xs of ...
но это лишь синтаксический сахар, который разворачивается в предыдущую запись.
Для тренировки определим несколько стандартных функций для работы с кортежами с помощью лямбда-функций (все они определены в Prelude
):
fst :: (a, b) -> a fst = \(a, _) -> a snd :: (a, b) -> b snd = \(_, b) -> b swap :: (a, b) -> (b, a) swap = \(a, b) -> (b, a)
Обратите внимание на то, что все функции словно являются константами. Они не содержат аргументов. Аргументы мы “пристраиваем” с помощью безымянных функций.
Определим функции преобразования первого и второго элемента кортежа (эти функции определены в модуле Control.Arrow
)
first :: (a -> a') -> (a, b) -> (a', b) first = \f (a, b) -> (f a, b) second :: (b -> b') -> (a, b) -> (a, b') second = \f (a, b) -> (a, f b)
Также в Prelude
есть полезные функции, которые превращают функции с частичным применением в обычны функции и наоборот:
curry :: ((a, b) -> c) -> a -> b -> c curry = \f -> \a -> \b -> f (a, b) uncurry :: (a -> b -> c) -> ((a, b) -> c) uncurry = \f -> \(a, b) -> f a b
Функция curry
принимает функцию двух аргументов для которой частичное применение невозможно. Это имитируется с помощью кортежей. Функция принимает кортеж из двух элементов. Функция curry
(от слова каррирование, частичное применение) превращает такую функцию в обычную функцию Haskell. А функция uncurry
выполняет обратное преобразование.
С помощью лямбда-функций можно имитировать локальные переменные. Так например можно переписать формулу для вычисления площади треугольника:
square a b c = (\p -> sqrt (p * (p - a) * (p - b) * (p - c))) ((a + b + c) / 2)
Смотрите мы определили функцию, которая принимает параметром полупериметр p
и передали в неё значение ((a + b + c) / 2)
. Если в нашей функции несколько локальных переменных, то мы можем составить лямбда-функцию от нескольких переменных и подставить в неё нужные значения.
Основной критерий выбора заключается в том, сделает ли этот элемент код более ясным. Наглядность кода станет залогом успешной поддержки. Его будет легче понять и улучшить при необходимости.
Далее мы рассмотрим несколько примеров определений из Prelude
и подумаем, почему был выбран тот или иной стиль. Начнём с класса Ord
и посмотрим на определения по умолчанию:
-- Тип упорядочивания data Ordering = LT | EQ | GT deriving (Eq, Ord, Enum, Read, Show, Bounded) class (Eq a) => Ord a where compare :: a -> a -> Ordering (<), (<=), (>=), (>) :: a -> a -> Bool max, min :: a -> a -> a -- Минимальное полное определение: -- (<=) или compare -- Использование compare может оказаться более -- эффективным для сложных типов. compare x y | x == y = EQ | x <= y = LT | otherwise = GT x <= y = compare x y /= GT x < y = compare x y == LT x >= y = compare x y /= LT x > y = compare x y == GT max x y | x <= y = y | otherwise = x min x y | x <= y = x | otherwise = y
Все функции определены в декларативном стиле. Тип Ordering
кодирует результат операции сравнения. Два числа могут быть либо равны (значение EQ
), либо первое меньше второго (значение LT
), либо первое больше второго (значение GT
).
Обратите внимание на функцию compare
. Мы не пишем дословное определение значений типа Ordering
:
compare x y | x == y = EQ | x < y = LT | x > y = GT
В этом случае функция compare
была бы определена через две других функции класса Ord
, а именно больше >
и меньше <
. Мы же хотим минимизировать число функций в этом определении. Поэтому вместо этого определения мы полагаемся на очерёдность обхода альтернатив в охранном выражении.
Если первый случай не прошёл, то во втором случае нет разницы между функциями <
и <=
. А если не прошёл и этот случай, то остаётся только вернуть значение GT
. Так мы определили функцию compare
через одну функцию класса Ord
.
Теперь посмотрим на несколько полезных функций для списков. Посмотрим на три основные функции для списков, одна из них возможно вам уже порядком поднадоела:
-- Преобразование списка map :: (a -> b) -> [a] -> [b] map f [] = [] map f (x:xs) = f x : map f xs -- Фильтрация списка filter :: (a -> Bool) -> [a] -> [a] filter p [] = [] filter p (x:xs) | p x = x : filter p xs | otherwise = filter p xs -- Свёртка списка foldr :: (a -> b -> b) -> b -> [a] -> b foldr f z [] = z foldr f z (x:xs) = f x (foldr f z xs)
Приведём несколько примеров для функции foldr
:
and, or :: [Bool] -> Bool and = foldr (&&) True or = foldr (||) False (++) :: [a] -> [a] -> [a] [] ++ ys = ys (x:xs) ++ ys = x : (xs ++ ys) concat :: [[a]] -> [a] concat = foldr (++) []
Функции and
и or
выполняют логические операции на списках. Так каждый конструктор (:)
заменяется на соответствующую логическую операцию, а пустой список заменяется на значение, которое не влияет на результат выполнения данной логической операции. Имеется ввиду, что функции (&& True)
и (|| False)
дают тот же результат, что и функция id x = x
. Функция (++)
объединяет два списка, а функция concat
выполняет ту же операцию, но на списке списков.
Функция zip
принимает два списка и смешивает их в список пар. Как только один из списков оборвётся оборвётся и список-результат. Эта функция является частным случаем более общей функции zipWith
, которая принимает функцию двух аргументов и два списка и составляет новый список попарных применений.
-- zip-ы zip :: [a] -> [b] -> [(a, b)] zip = zipWith (,) zipWith :: (a -> b -> c) -> [a] -> [b] -> [c] zipWith z (a:as) (b:bs) = z a b : zipWith z as bs zipWith _ _ _ = []
Посмотрим как работают эти функции в интерпретаторе:
Prelude> zip [1,2,3] "hello" [(1,'h'),(2,'e'),(3,'l')] Prelude> zipWith (+) [1,2,3] [3,2,1] [4,4,4] Prelude> zipWith (*) [1,2,3] [5,4,3,2,1] [5,8,9]
Отметим, что в Prelude
также определена обратная функция unzip
:
unzip :: [(a,b)] -> ([a], [b])
Она берёт список пар и разбивает его на два списка.
Пока по этим определениям кажется, что композиционный стиль совсем нигде не применяется. Он встретился нам лишь в функции break
. Но давайте посмотрим и на функции с композиционным стилем:
lines :: String -> [String] lines "" = [] lines s = let (l, s') = break (== '\n') s in l : case s' of [] -> [] (_:s'') -> lines s''
Функция lines
разбивает строку на список строк. Эти строки были разделены в исходной строке символом переноса '\n'
.
Функция break
принимает предикат и список и возвращает два списка. В первом все элементы от начала списка, которые не удовлетворяют предикату, а во втором все остальные. Наш предикат (== '\n')
выделяет все символы кроме переноса каретки. В строке
let (l, s') = break (== '\n') s
Мы сохраняем все символы до '\n'
от начала строки в переменной l
. Затем мы рекурсивно вызываем функцию lines
на оставшейся части списка:
in l : case s' of [] -> [] (_:s'') -> lines s''
При этом мы пропускаем в s'
первый элемент, поскольку он содержит символ переноса каретки.
Посмотрим на ещё одну функцию для работы со строками.
words :: String -> [String] words s = case dropWhile Char.isSpace s of "" -> [] s' -> w : words s'' where (w, s'') = break Char.isSpace s'
Функция words
делает тоже самое, что и lines
, только теперь в качестве разделителя выступает пробел. Функция dropWhile
отбрасывает от начала списка все элементы, которые удовлетворяют предикату. В строке
case dropWhile Char.isSpace s of
Мы одновременно отбрасываем все первые пробелы и готовим значение для декомпозиции. Дальше мы рассматриваем два возможных случая для строк.
"" -> [] s' -> w : words s'' where (w, s'') = break Char.isSpace s'
Если строка пуста, то делать больше нечего. Если – нет, мы также как и в предыдущей функции применяем функцию break
для того, чтобы выделить все элементы кроме пробела, а затем рекурсивно вызываем функцию words
на оставшейся части списка.
В этой главе мы узнали очень много новых синтаксических конструкций для определения функций. Они появлялись парами. Сведём их в таблицу:
Элемент | Декларативный стиль | Композиционный |
---|---|---|
Локальные переменные |
|
|
Декомпозиция |
Сопоставление с образцом |
|
Условные выражения |
Охранные выражения |
|
Определение функций |
Уравнения |
лямбда-функции |
Нам встретилась новая конструкция в сопоставлении с образцом:
beside :: Nat -> (Nat, Nat) beside Zero = error "undefined" beside x@(Succ y) = (y, Succ x)
Она позволяет проводить декомпозицию и давать имя всему значению одновременно. Такие выражения x@(...)
в англоязычной литературе принято называть as-patterns.
В этой главе нам встретилось много полезных стандартных функций, потренируйтесь с ними в интерпретаторе. Вызывайте их с различными значениями, экспериментируйте.
Попробуйте определить функции из предыдущих глав в чисто композиционном стиле.
Посмотрите на те функции, которые мы прошли и попробуйте переписать их определения шиворот на выворот. Если вы видите, что элемент написан композиционном стиле перепишите его в декларативном и наоборот. Получившиеся функции могут показаться монстрами, но это упражнение может помочь вам в закреплении новых конструкций и почувствовать сильные и слабые стороны того или иного стиля.
Определите модуль, который будет вычислять площади простых фигур, треугольника, окружности, прямоугольника, трапеции. Помните, что фигуры могут задаваться различными способами.
Поток это бесконечный список, или список, у которого нет конструктора пустого списка:
data Stream a = a :& Stream a
Так например мы можем составить поток из всех чисел Пеано:
nats :: Nat -> Stream Nat nats a = a :& nats (Succ a)
Или поток, который содержит один и тот же элемент:
constStream :: a -> Stream a constStream a = a :& constStream a
Напишите модуль для потоков. В первую очередь нам понадобятся функции выделения частей потока, поскольку мы не сможем распечатать поток целиком (ведь он бесконечный):
-- Первый элемент потока head :: Stream a -> a -- Хвост потока, всё кроме первого элемента tail :: Stream a -> Stream a -- n-тый элемент потока (!!) :: Stream a -> Int -> a -- Берёт из потока несколько первых элементов: take :: Int -> Stream a -> [a]
Имена этих функций будут совпадать с именами функций для списков чтобы избежать коллизий имён мы воспользуемся квалифицированным импортом функций. Делается это так:
import qualified Prelude as P( определения )
Слова qualified
и as
– ключевые. Теперь для использования функций из модуля Prelude
мы будем писать P.имяФункции
. Такие имена называются квалифицированными. Для того чтобы пользоваться квалифицированными именами только для тех функций, для которых возможна коллизия имён можно поступить так:
import qualified Prelude as P import Prelude
Компилятор разберётся, какую функцию мы имеем в виду.
Для удобства тестирования можно определить такую функцию печати потоков:
instance Show a => Show (Stream a) where show xs = showInfinity (show (take 5 xs)) where showInfinity x = P.init x P.++ "..."
Функция P.init
выделяет все элементы списка кроме последнего. В данном случае она откусит от строки закрывающуюся скобку. После этого мы добавляем троеточие, как символ бесконечности списка.
Функции преобразования потоков:
-- Преобразование потока map :: (a -> b) -> Stream a -> Stream b -- Фильтрация потока filter :: (a -> Bool) -> Stream a -> Stream a -- zip-ы для потоков: zip :: Stream a -> Stream b -> Stream (a, b) zipWith :: (a -> b -> c) -> Stream a -> Stream b -> Stream c
Функция генерации потока:
iterate :: (a -> a) -> a -> Stream a
Эта функция принимает два аргумента: функцию следующего элемента потока и значение первого элемента потока и возвращает поток:
iterate f a = a :& f a :& f (f a) :& f (f (f a)) :& ...
Так с помощью этой функции можно создать поток всех чисел Пеано от нуля или постоянный поток:
nats = iterate Succ Zero constStream a = iterate (\x -> x) a
Возможно вас удивляет тот факт, что в этом упражнении мы оперируем бесконечными значениями, но пока мы не будем вдаваться в детали того как это работает, просто попробуйте определить этот модуль и посмотрите в интерпретаторе, что получится.