Декларативный и композиционный стиль

В 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-выражения

В декларативном стиле для этого предусмотрены 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-выражений. Но лучше избегать сильно вложенных выражений.

let-выражения

В композиционном стиле функция вычисления площади треугольника будет выглядеть так:

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-выражения

Оказывается декомпозицию можно проводить в любом выражении, для этого существуют 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-выражения

В композиционном стиле в качестве условных выражений используются уже знакомые нам 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 на оставшейся части списка.

Краткое содержание

В этой главе мы узнали очень много новых синтаксических конструкций для определения функций. Они появлялись парами. Сведём их в таблицу:

Элемент Декларативный стиль Композиционный

Локальные переменные

where-выражения

let-выражения

Декомпозиция

Сопоставление с образцом

case-выражения

Условные выражения

Охранные выражения

if-выражения

Определение функций

Уравнения

лямбда-функции

Особенности синтаксиса

Нам встретилась новая конструкция в сопоставлении с образцом:

beside :: Nat -> (Nat, Nat)
beside  Zero       = error "undefined"
beside  x@(Succ y) = (y, Succ x)

Она позволяет проводить декомпозицию и давать имя всему значению одновременно. Такие выражения x@(...) в англоязычной литературе принято называть as-patterns.

Упражнения

Зарегистрировано под лицензией Creative commons Attribution-NonCommercial-NoDerivs 3.0 Generic (CC BY-NC-ND 3.0)