Изучай Haskell ради Добра! Синтаксис функций перевод

Осенью прошлого года хабралюди начали переводить туториал «Learn You a Haskell for Great Good!». Уверен, те, кто следили за публикациями помнят холивары, которые разворачивались вокруг перевода названия. Также уверен, те, кому это было интересно продолжают участвовать в переводах. Для всех же остальных публикую этот топик в надежде оживить недели Хаскелля на Хабре.

image

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



В этой главе будет рассказано о некоторых крутых синтаксических конструкциях языка Haskell, а начнем мы с сопоставлений с образцом. Идея заключается в указании определенных шаблонов (образцов), которым должны соответствовать некоторые данные. Во время выполнения программы данные будут проверяться на соответствие шаблону. Если шаблон подходит, тогда данные будут разобраны в соответствии с ним.

Когда вы определяете функцию, ее определение можно разбить на несколько частей, по одной части для каждого шаблона. Это приводит к очень стройному коду, который прост и легко читается. Вы можете задавать шаблоны для любого типа данных — для чисел, символов, списков, кортежей и т.д. Давайте создадим простую функцию, которая проверяет, является ли ее параметр числом семь или нет.


lucky :: (Integral a) => a -> String
lucky 7 = "LUCKY NUMBER SEVEN!"
lucky x = "Sorry, you're out of luck, pal!"


Когда вы вызываете функцию lucky, происходит проверка параметра на совпадение с заданными шаблонами, в том порядке как они были заданы. Когда проверка даст положительный результат — используется соответствующее тело функции. Единственный случай, когда число переданное функции удовлетворяет первому шаблону — когда оно равно семи. Если нет, то происходит проверка на совпадение со следующим шаблоном. Следующий шаблон может быть успешно сопоставлен с любым числом, также он привязывает переданное число к переменной x.

Эта функция может быть реализована с использованием оператора if. Но что если нам потребуется написать функцию, которая называет цифры от 1 до 5, и выводит "Not between 1 and 5" для чисел? Без сопоставления с образцом нам бы пришлось создать очень запутанное дерево выражений if then else. А вот что получится, если использовать сопоставление:

sayMe :: (Integral a) => a -> String
sayMe 1 = "One!"
sayMe 2 = "Two!"
sayMe 3 = "Three!"
sayMe 4 = "Four!"
sayMe 5 = "Five!"
sayMe x = "Not between 1 and 5"


Заметьте, что если бы мы переместили последний шаблон (который соответствует любому вводу) вверх, то функция всегда бы выводила "Not between 1 and 5", потому что этот шаблон подходит для любого числа, и невозможно было бы пройти дальше и сделать проверку на совпадение с другими шаблонами.

Помните реализованную нами функцию факториала? Мы определили факториал числа n как произведение чисел [1..n]. Мы можем определить данную функцию рекурсивно, точно так же, как факториал определяется в математике. Начнем с того, что объявим факториал нуля равным единице.

Затем определим факториал любого положительного числа как это число умноженное на факториал предыдущего числа. Вот как это транслируется в термины языка Haskell.

factorial :: (Integral a) => a -> a
factorial 0 = 1
factorial n = n * factorial (n - 1)


Это первый раз, когда мы задали функцию рекурсивно. Рекурсия очень важна в языке Haskell, и мы подробнее рассмотрим её позже. Но по сути, вот что происходит, когда мы пытаемся вычислить факториал числа 3: функция пробует вычислить 3 * factorial 2. factorial 2 — это 2 * factorial 1, таким образом, у нас получается 3 * (2 * factorial 1). factorial 1 — это 1 * factorial 0, и в итоге у нас 3 * (2 * (1 * (factorial 0))).

И вот в чем здесь фокус: мы указали, что факториал нуля просто равен единице. Так как шаблон, проверяющий эту часть функции, определен перед универсальным шаблоном, который проверяет все остальные числа, функция возвращает 1.

Финальный результат — 3 * (2 * (1 * 1)). Если бы мы написали второй шаблон выше первого, то он совпадал бы с любым числом, включая 0, и наше вычисление никогда бы не закончилось. Вот почему так важен порядок в котором вы задаете образцы, и всегда лучше задать сначала более частные случаи, а потом — более общие.

Сопоставление с образцом может завершиться неудачей, если мы зададим функцию следующим образом:

charName :: Char -> String
charName 'a' = "Albert"
charName 'b' = "Broseph"
charName 'c' = "Cecil"


а затем попытаемся вызвать её с параметром, который не ожидали, и вот что произойдет:

ghci> charName 'a'
"Albert"
ghci> charName 'b'
"Broseph"
ghci> charName 'h'
"*** Exception: tut.hs:(53,0)-(55,21): Non-exhaustive patterns in function charName


Это жалоба на то, что наши шаблоны не покрывают всех возможных случаев (недоопределены), и, воистину, так оно и есть. Когда мы задаем шаблоны, мы должны всегда включать универсальный образец, который можно сопоставить с любым вводом, для того, чтобы наша программа не падала, если функция получит какие-то непредвиденные входные данные.

Сопоставление с образцом может быть использовано для кортежей. Что если мы хотим создать функцию, которая принимает два двумерных вектора (представленных в форме пары) и складывает их? Чтобы сложить два вектора, нужно сложить их соответствующие координаты. Вот как мы бы написали такую функцию, не знай мы о сопоставлении по шаблону:

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors a b = (fst a + fst b, snd a + snd b)


Это, конечно, работает, но есть способ лучше. Давайте исправим функцию, чтобы она использовала сопоставление с образцом.

addVectors :: (Num a) => (a, a) -> (a, a) -> (a, a)
addVectors (x1, y1) (x2, y2) = (x1 + x2, y1 + y2)


Вот так то! Намного лучше. Заметьте, что у нас имеется общий образец. Тип функции addVectors (в обоих случаях) — (Num a) => (a, a) -> (a, a) -> (a, a), так что мы гарантировано получим две пары на входе.

Функции fst и snd извлекают компоненты пары. Но как насчет троек? Ну, стандартных функций для этой цели не существует, но мы можем создать свои.

first :: (a, b, c) -> a
first (x, _, _) = x

second :: (a, b, c) -> b
second (_, y, _) = y

third :: (a, b, c) -> c
third (_, _, z) = z


Символ _ имеет то же значение, что и в выражениях списков. Этот символ означает, что нам не интересно значение на этом месте, так что мы просто пишем _.

Это напомнило мне, что вы можете использовать сопоставление с образцом в списковых выражениях. Смотрите:

ghci> let xs = [(1,3), (4,3), (2,4), (5,3), (5,6), (3,1)]
ghci> [a+b | (a,b) <- xs]
[4,7,6,8,11,4]


Если сопоставление с образцом закончится неудачей для одного элемента списка, просто произойдет переход к следующему элементу.

Списки сами по себе (т.е. заданные прямо в тексте шаблона, списковые литералы) могут быть использованы при сопоставлении с образцом. Вы можете сравнивать с пустым списком или с любым шаблоном который включает : и пустой список. Так как [1,2,3] это просто синтактический сахар (упрощенная запись) для 1:2:3:[], вы можете использовать [1,2,3] как шаблон.

Образец вида x:xs связывает голову списка с x, а оставшуюся часть с xs, даже если в списке всего один элемент; в этом случае xs — пустой список.

Замечание: Образец x:xs используется очень часто, особенно с рекурсивными функциями. Образцы, у которых присутствует ":" в определении, могут быть использованы только для списков, длина которых равна единице или больше.


Если вы, скажем, хотите связать первые три элемента с переменными, а оставшиеся элементы списка с другой переменной, вы можете использовать что-то наподобие x:y:z:zs. Образец сработает только для списков, в которых три или более элементов.

Теперь, когда мы знаем, как использовать сопоставление с образцом для списков, давайте создадим собственную реализацию функции head.

head' :: [a] -> a
head' [] = error "Can't call head on an empty list, dummy!"
head' (x:_) = x


Проверяем, работает ли:

ghci> head' [4,5,6]
4
ghci> head' "Hello"
'H'


Отлично! Заметьте, что если вы хотите выполнить привязку к нескольким переменным (даже если одна из них всего лишь _ и на самом деле ни с чем не связывается), вам необходимо заключить их в круглые скобки. Также обратите внимание на использование функции error. Она принимает строковый параметр и генерирует ошибку времени исполнения, используя этот параметр для сообщения о причине ошибки.

Вызов функции error приводит к аварийному завершению программы, так что не стоит использовать её слишком часто. Но вызов head на пустом списке не имеет смысла.

Давайте создадим тривиальную функцию, которая сообщает нам о нескольких элементах с начала списка, в (не)удобной текстовой форме.

tell :: (Show a) => [a] -> String
tell [] = "The list is empty"
tell (x:[]) = "The list has one element: " ++ show x
tell (x:y:[]) = "The list has two elements: " ++ show x ++ " and " ++ show y
tell (x:y:_) = "This list is long. The first two elements are: " ++ show x ++ " and " ++ show y


Эта функция безопасна, потому что она обрабатывает случаи, когда входной список пуст, содержит один, два и более элементов. Обратите внимание, что (x:[]) и (x:y:[]) могут быть записаны как [x] и [x,y] (так как это облегченная запись, нам не нужны круглые скобки). Мы не можем записать (x:y:_) с помощью квадратных скобок, потому что такая запись соответствует любому списку длиной два или более.

Мы уже создали нашу собственную функцию length, используя выражения списков. Теперь мы попробуем создать её с помощью сопоставления с образцом и небольшой рекурсии:

length' :: (Num b) => [a] -> b
length' [] = 0
length' (_:xs) = 1 + length' xs


Это похоже на функцию факториала, которую мы писали ранее. Сначала определяется результат известного входного значения — пустого списка. Это также называется краевым условием. Затем, во втором образце, список разделяется на голову и на хвост.

Пусть длина строки равна 1 плюс длине хвоста. В качестве шаблона для головы списка используется _, поскольку нам не нужно знать, какое именно значение содержалось в головном элементе. Также стоит отметить, что шаблоны записаны для всех возможных случаев, которые могут возникнуть при работе со списками. Первый шаблон обрабатывает случай, когда список пуст, второй шаблон обрабатывает все непустые списки.

Посмотрим что произойдет если мы вызовем length' на "ham". Во-первых, произойдет проверка, пустой список или нет. Так как список не пустой, будет проверяться второй шаблон. Список соответствует второму шаблону, следовательно, он разбивается на голову и хвост, длина получается равна 1 + length' "am". Океюшки.

Длина "am", таким же образом, это 1 + length' "m". Длина "m" — это 1 + length' "" (также может быть записано как 1 + length' []). Мы определили length' [] как равную нулю. Таким образом, в конце получаем 1 + (1 + (1 + 0)).

Давайте реализуем sum. Мы знаем, что сумма элементов пустого списка равна нулю. Запишем это в виде шаблона. Также ясно, что сумма элементов списка — это значение из головы списка плюс сумма всех остальных элементов. Если все это записать, у нас получится так:

sum' :: (Num a) => [a] -> a
sum' [] = 0
sum' (x:xs) = x + sum' xs


Есть еще одна конструкция, которая называется as-образцом. Это удобный способ разбить что-нибудь в соответствии с шаблоном и связать результат разбиения с переменными, но в то же время сохранить ссылку на исходные данные. Это можно сделать если поместить имя и @ перед образцом. Например, шаблон xs@(x:y:ys).

Такой шаблон работает так же как x:y:ys, но вы легко можете получить исходный список по имени xs, вместо того, чтобы раз за разом печатать x:y:ys в теле функции. Вот пример на скорую руку:

capital :: String -> String
capital "" = "Empty string, whoops!"
capital all@(x:xs) = "The first letter of " ++ all ++ " is " ++ [x]

ghci> capital "Dracula"
"The first letter of Dracula is D"


Обычно мы используем as-образец чтобы не повторяться, когда идет сравнение с большим составным шаблоном и нам нужно использовать неразбитое целое в теле функции.

Вот еще что: вы не можете использовать ++ в шаблонах. Если вы попытаетесь записать шаблон который сравнивает с (xs ++ ys), что будет соответствовать первому и второму списку? В этом немного смысла. Возможно было бы полезно уметь сопоставлять с (xs ++ [x,y,z]) или (xs ++ [x]), но по самой сути списков вы не можете этого делать.

Эй, стража!



imageВ то время как шаблоны — это способ убедиться, что значение соответствует некоторой форме, и разобрать его на части, сторожевые условия — это способ проверить истинность некоторого свойства у значения, переданного функции (или нескольких значений). Это похоже на оператор if и работает схожим образом. На самом деле, сторожевые условия гораздо легче читать, если у вас несколько условий и они отлично работают с шаблонами.

Вместо того, чтобы объяснять их синтаксис, давайте просто напишем функцию с использованием сторожевых условий. Мы собираемся написать простую функцию, которая оценит вас на основе ИМТ (индекс массы тела). Ваш ИМТ равен вашему весу разделенному на квадрат вашего роста.

Если ваш ИМТ меньше чем 18.5, можно считать вас тощим. Если ИМТ от 18.5 до 25, то вы находитесь в пределах нормы. От 25 до 30 — вы полненький, и более 30 — вы тучный. Запишем эту функцию (мы не будем расчитывать ИМТ, функция принимает его как параметр и ругнет вас соответственно).

bmiTell :: (RealFloat a) => a -> String
bmiTell bmi
    | bmi <= 18.5 = "You're underweight, you emo, you!" --Эй ты, дистрофик!
    | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"  --Ладно, нормальный. Зато небось уродец!
    | bmi <= 30.0 = "You're fat! Lose some weight, fatty!" --Ты толстый! Хоть немного веса сбрось!
    | otherwise = "You're a whale, congratulations!" --Мои поздравления, ты - жирный боров!


Сторожевые условия обозначаются вертикальными черточками после имени и параметров функции. Обычно они печатаются с отступом вправо и начинаются с одной позиции. Сторожевое условие должно быть булевым. Если после вычисления условие имеет значение True, используется соответствующее тело функции. Если вычисленное условие ложно, проверка продолжается со следующего условия, и так далее.

Если мы вызовем эту функцию с параметром 24.3, она вначале проверит, не является ли это значение меньшим или равным 18.5. Так как сторожевое условие не выполняется, функция перейдет к следующему. Проверяется следующее условие, и так как 24.3 меньше чем 25.0, будет возвращена вторая строка.

Это очень напоминает большие деревья условий if-else в императивных языках программирования, только такой способ записи значительно лучше и легче для чтения. Несмотря на то, что большие деревья if-else обычно не рекомендуется использовать, иногда задача представлена в настолько разрозненном виде, что просто невозможно обойтись без них. Сторожевые условия очень хорошая альтернатива для таких задач.

Во многих случаях последнее сторожевое условие — это otherwise (иначе). Otherwise определяется просто как otherwise = True, такое условие всегда истинно. Работа условий очень похоже на то, как работают шаблоны, но шаблоны проверяют входные данные, а сторожевые условия могут делать любые проверки.

Если все сторожевые условия ложны (и мы не записали otherwise как последнее условие), вычисление продолжается со следующего шаблона. Вот почему шаблоны и условия так хорошо работают вместе. Если нет ни подходящих условий, ни шаблонов, будет сгенерирована ошибка.

Конечно же мы можем использовать сторожевые условия с функциями, которые имеют столько входных параметров, сколько нам нужно. Вместо того, чтобы заставлять пользователя вычислять его ИМТ перед вызовом функции, давайте модифицируем эту функцию так, чтобы она принимала рост и вес и вычисляла ИМТ.

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!" --Эй ты, дистрофик!
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!" --Ладно, нормальный. Зато небось уродец!
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!" --Ты толстый! Хоть немного веса сбрось!
    | otherwise = "You're a whale, congratulations!"--Мои поздравления, ты жирный боров!


Ну-ка проверим, не толстый ли я…

ghci> bmiTell 85 1.90
"You're supposedly normal. Pffft, I bet you're ugly!"
--Ладно, нормальный. Зато небось уродец!


О! Я не толстый! Но Хаскель назвал меня уродцем. Ну, хоть что-то.

Обратите внимание, что после имени функции и ее параметров нет знака равенства до первого сторожевого условия. Многие новички ставят этот знак и получают ошибку.

Еще один очень простой пример: давайте напишем нашу собственную функцию max. Если вы помните, она принимает два значения, которые можно сравнить, и возвращает большее из них.

max' :: (Ord a) => a -> a -> a
max' a b
    | a > b = a
    | otherwise = b


Сторожевые условия можно писать в той же строке, что и остальной код, но я не советую так делать, потому что это труднее читается даже для очень коротких функций. Но чтобы показать, что это можно сделать, мы запишем max' таким образом:

max' :: (Ord a) => a -> a -> a
max' a b | a > b = a | otherwise = b


Хе! Совсем не читабельно! Продолжим: давайте напишем нашу собственную функцию сравнения используя сторожевые условия.

myCompare :: (Ord a) => a -> a -> Ordering
a `myCompare` b
    | a > b = GT
    | a == b = EQ
    | otherwise = LT


ghci> 3 `myCompare` 2
GT


Примечание: мы можем не только вызывать функции с помощью обратных кавычек, мы можем их так же определять. Иногда такую запись легче читать.


А где же where!?



В прошлом разделе мы определили вычислитель ИМТ и ругатель таким образом:

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | weight / height ^ 2 <= 18.5 = "You're underweight, you emo, you!"
    | weight / height ^ 2 <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
    | weight / height ^ 2 <= 30.0 = "You're fat! Lose some weight, fatty!"
    | otherwise = "You're a whale, congratulations!"


Заметили, мы повторили вычисление три раза. Копи-пастить при программировании, да еще три раза подряд, приблизительно так же приятно как получать по голове. Раз уж у нас вычисление повторяется три раза, было бы очень удобно, если бы мы могли вычислить его один раз, присвоить результату имя, и использовать его, вместо того, чтобы повторять вычисление. Мы можем переписать нашу функцию так:

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | bmi <= 18.5 = "You're underweight, you emo, you!"
    | bmi <= 25.0 = "You're supposedly normal. Pffft, I bet you're ugly!"
    | bmi <= 30.0 = "You're fat! Lose some weight, fatty!"
    | otherwise = "You're a whale, congratulations!"
    where bmi = weight / height ^ 2


Мы помещаем ключевое слово where после сторожевых условий (обычно его печатают с тем же отступом как и сторожевые условия), и затем определяем несколько имен или функций. Эти имена видимы внутри объявления функции и позволяют нам не повторять код. Если вдруг нам вздумается вычислять ИМТ другим образом, мы должны исправить способ его вычисления только однажды.

Использование where улучшает читаемость, так как дает имена понятиям и может сделать наши программы быстрее за счет того, что переменные вроде нашей bmi вычисляются только однажды. Мы можем зайти еще дальше и представить нашу функцию так:

bmiTell :: (RealFloat a) => a -> a -> String
bmiTell weight height
    | bmi <= skinny = "You're underweight, you emo, you!"
    | bmi <= normal = "You're supposedly normal. Pffft, I bet you're ugly!"
    | bmi <= fat = "You're fat! Lose some weight, fatty!"
    | otherwise = "You're a whale, congratulations!"
        where bmi = weight / height ^ 2
        skinny = 18.5
        normal = 25.0
        fat = 30.0


Имена, которые мы определили в секции where этой функции, видимы только для нее самой, так что нам можно не беспокоиться о том, что мы засоряем пространство имен других функций. Заметьте, что все имена выровнены в одну колонку. Если не выровнять имена подобным образом, Haskell будет сконфужен, потому что он не будет знать, являются ли они частью одного блока или нет.

Привязки в where не являются общими для различных шаблонов заданной функции. Если вы хотите, чтобы несколько шаблонов имели доступ к каким то именам, вам необходимо определить их глобально.

Вы также можете использовать привязки в where для сопоставления по образцу. Мы можем переписать секцию where в нашей предыдущей функции следующим образом:

...
where bmi = weight / height ^ 2
    (skinny, normal, fat) = (18.5, 25.0, 30.0)


Давайте создадим еще одну простую функцию, которая принимает два аргумента: имя и фамилию, и возвращает инициалы.

initials :: String -> String -> String
initials firstname lastname = [f] ++ ". " ++ [l] ++ "."
    where (f:_) = firstname
    (l:_) = lastname


Можно было бы выполнять сопоставление с образцом прямо в параметрах функции (это проще и понятнее), но мы хотим показать, что это возможно делать и в определениях where.

Точно так же, как мы определяли константы в where, можно определять и функции. Придерживаясь нашей темы «здорового» программирования, создадим функцию, которая принимает список из пар вес-рост и возвращает список из ИМТ.

calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi w h | (w, h) <- xs]
    where bmi weight height = weight / height ^ 2


Видите что происходит? Причина, по которой нам пришлось представить bmi в виде функции в данном примере, заключается в том, что мы не можем просто вычислить один ИМТ для параметров, переданных в функцию. Нам необходимо пройтись по всему списку и для каждой пары вычислить ИМТ.

Определения в where так же могут быть вложенными. Это популярный подход: создавать функцию и определять для нее функции-помощники в where, и затем задавать функциям-помощникам свои вспомогательные функции.

Пусть будет let



Определения заданные с помощью оператора let очень похожи на определения where. Where — это синтаксическая конструкция, которая позволяет вам связывать выражения с переменными в конце функции, объявленные переменные видны во всем теле функции, включая сторожевые условия. Let позволяет вам связывать выражения с именами в любом месте функции, конструкции let сами по себе являются выражениями, но их область видимости ограничена локальным контекстом. Таким образом определение let сделанное в сторожевом условии видно только в нем самом.

Как и любые другие конструкции языка Хаскель, которые используются для привязывания имен к значениям, определения let могут быть использованы в сопоставлении с образцом. Посмотрим на них в действии! Вот как мы могли бы определить функцию, которая вычисляет площадь поверхности цилиндра по высоте и радиусу:

cylinder :: (RealFloat a) => a -> a -> a
cylinder r h =
    let sideArea = 2 * pi * r * h
        topArea = pi * r ^2
    in sideArea + 2 * topArea


imageОбщее выражение выглядит как
let <определения> in <выражение>
. Имена, которые вы определили в части let, видимы в выражении после in. Как видите, мы могли бы воспользоваться where для той же цели. Обратите внимание, что имена также выровнены в одну позицию. Ну и какая разница между where и let? Выглядит так, будто в let вначале следуют определения, а затем выражение, а в where наоборот.

Различие в том, что определения let сами по себе являются выражениями. Определения в where — просто синтаксические конструкции. Помните как мы создавали оператор if и говорили, что так как оператор if является выражением, то вы можете использовать его практически где угодно?

ghci> [if 5 > 3 then "Woo" else "Boo", if 'a' > 'b' then "Foo" else "Bar"]
["Woo", "Bar"]
ghci> 4 * (if 10 > 5 then 10 else 0) + 2
42


Так же вы можете поступать с let.

ghci> 4 * (let a = 9 in a + 1) + 2
42


Let можно использовать для определения локальных функций:

ghci> [let square x = x * x in (square 5, square 3, square 2)]
[(25,9,4)]


Если нам надо привязать значения к нескольким переменным в одной строке, мы не можем записать их в столбик. Поэтому мы разделяем их с помощью точки с запятой.

ghci> (let a = 100; b = 200; c = 300 in a*b*c, let foo="Hey "; bar = "there!" in foo ++ bar)
(6000000,"Hey there!")


Точка с запятой после последнего определения не нужна, но ее можно поставить, если есть желание. Как мы уже говорили ранее, let могут использоваться при сопоставлении с образцом. Они очень полезны для того, чтобы быстро разобрать кортеж на элементы и привязать значения элементов к переменным, а также в других подобных случаях.

ghci> (let (a,b,c) = (1,2,3) in a+b+c) * 100
600


Вы также можете поместить let в списковые выражения. Давайте перепишем наш прошлый пример, который обрабатывал списки пар вес-рост, чтобы он использовал let в выражении вместо того, чтобы определять вспомогательную функцию в where.

calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2]


Мы поместили let в списковое выражение так, словно это предикат, но он не фильтрует список, а просто определяет имя. Имена, определенные в let внутри спискового выражения, видны в функции вывода (часть до |) и для всех предикатов и секций которые следуют за let. Так что мы можем сделать функцию, которая выводит только толстяков.

calcBmis :: (RealFloat a) => [(a, a)] -> [a]
calcBmis xs = [bmi | (w, h) <- xs, let bmi = w / h ^ 2, bmi >= 25.0]


Мы не можем использовать имя bmi в части (w, h) <- xs, потому что оно определено до let.

Мы опускаем часть in в операторах let когда мы используем их в списковых выражениях, потому что видимость имен в этом случае предопределена. Тем не менее, мы можем использовать let в предикатах, и определенные таким образом имена будут видны только в предикате. Часть in также может быть пропущена при определении функций и констант напрямую в GHCI. В этом случае имена будут видимы во время интерактивной сессии.

ghci> let zoot x y z = x * y + z
ghci> zoot 3 9 2
29
ghci> let boot x y z = x * y + z in boot 3 4 2
14
ghci> boot
<interactive>:1:0: Not in scope: `boot'


Если let настолько крут, почему бы не использовать его вместо where? Ну, так как let это все-таки выражения, которые довольно-таки локальны, они не могут быть видимы во всех сторожевых условиях. Некоторые предпочитают where, потому что в этом случае имена следуют за функцией. Таким образом тело функции ближе к ее имени и объявлению типа, считается, что так легче читать.

Выражения для выбора из вариантов



imageМногие императивные языки (C, C++, Java, и т.д.) имеют оператор case, и если вам доводилось программировать на них, вы знаете, что это такое. Вы берете переменную и выполняете некую часть кода для каждого значения этой переменной, ну и, возможно, используете финальное условие, которое срабатывает если не отработали другие.

Хаскель взял эту концепцию и усовершенствовал ее. Как нам намекает имя, выражения для выбора являются… эээ, выражениями, так же как if else и let. Мы не только можем вычислять выражения основываясь на возможных значениях переменной, мы также можем делать сопоставление с образцом.

Таак, берем переменную, выполняем сопоставление с образцом, выполняем участок кода в зависимости от значения, где мы это слышали ранее? Ах да, сопоставление с образцом по параметрам при объявлении функции! На самом деле это всего-навсего облегченная запись для выражений выбора. Эти два куска кода делают одно и то же, они взаимозаменимы:

head' :: [a] -> a
head' [] = error "No head for empty lists!"
head' (x:_) = x

head' :: [a] -> a
head' xs = case xs of [] -> error "No head for empty lists!"
                                (x:_) -> x


Как вы видите, синтаксис для выражений отбора довольно прост:

case expression of pattern -> result
                            pattern -> result
                            pattern -> result
                            ...


Выражения проверяются на соответствие шаблонам. Сопоставление с образцом работает как обычно: используется первый шаблон который подошел. Если все шаблоны были опробованы и ни один не подошел, генерируется ошибка времени выполнения.

Сопоставление с образцом по параметрам функции может быть сделано только при объявлении функции, выражения отбора могут использоваться практически везде. Например:

describeList :: [a] -> String
describeList xs = "The list is " ++ case xs of [] -> "empty."
                                                                  [x] -> "a singleton list."
                                                                  xs -> "a longer list."


Они удобны для сопоставления с каким-нибудь образцом в середине выражения. Так как сопоставление с образцом при объявлении функции — это всего лишь синтаксический сахар для выражений отбора, мы могли бы определить функцию таким образом:

describeList :: [a] -> String
describeList xs = "The list is " ++ what xs
where what [] = "empty."
what [x] = "a singleton list."
what xs = "a longer list."



Предыдущие переводы данного туториала на хабре:

Перевод книги по главам:
  1. Введение
  2. Начало
  3. Типы и классы типов
  4. Синтаксис функций
  5. Рекурсия
  6. Функции высшего порядка
  7. Модули
  8. Создание своих собственных типов и классов типов
  9. Ввод-вывод
  10. Функциональное решение задач
  11. Функторы, Аппликативные функторы и Моноиды

Переведено толпой, переводы координирует хабраюзер plague.
+47
2 июля 2010, 14:44
38
folone 57,4

комментарии (10)

0
Ice_venom #
Благодарю!
0
Zubchick #
Такая огромная работа и всего один комментарий :)
Надеюсь статья найдет своего читателя.
0
maovrn #
Это во-первых перевод, во-вторых книги. Флудить не интересно. Кому надо — прочитал.
0
laughedelic #
замечательно! спасибо за переводы (:

p.s. было бы совсем замечательно, если бы вы немного поправили форматирование кода, а то табы как-то съехали.
0
XPilot #
Интересно, выложит ли Miran Lipovača последние 3 главы, поскольку:
a) Они помечены как «Coming soon»
b) Его книга выйдет в январе 2011 (400 страниц?!)
0
Halt #
Мы не можем использовать имя bmi в части (w, h) < — xs, потому что оно определено до let.

Не пропущено :)
0
StrangeAttractor #
Пока не вижу для себя зачем всё это нужно когда есть C# и Java с ясным и понятным с первого взгляда синтаксисом и кучами библиотек, но надеюсь что это именно от непонимания. Был бы очень признателен, если бы кто-нибудь «подсадил» меня на какой-нибудь Хаскель или другой актуальный функциональный язык, парочкой жизненных (желательно даже не математических) примеров показав в чём вкусность этого дела.
0
Colwin #
Некоторые задачи функциональным способом решаются гораздо практичнее, чем принятым в Java или C# способом. В этих языках, конечно, можно создать аналоги функциоанльного стиля, но при этом будет присутствовать куча синтаксического мусора.
Думаю, будущее будет за языком, который будет поддерживать обе этих парадигмы при минимизации синтаксических конструкций.
0
StrangeAttractor #
> Думаю, будущее будет за языком, который будет поддерживать обе этих парадигмы при минимизации синтаксических конструкций.

Scala? Python 3? F#?
+1
folone #
Класс объектно-ориентированно-функциональных гибридов в целом, имхо. Scala, Clojure, F#, OCaml, Nemerle, O'Haskell, Common Lisp (как ни странно, да), и так далее.

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.