ТОП 10:

Сопоставление со списками и генераторы списков



 
 

В генераторах списков тоже можно использовать сопоставление с образцом, например:

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 "Нельзя вызывать head на пустом списке, тупица!" head' (x:_) = x

Проверим, работает ли это...

ghci> head' [4,5,6]

 
 

ghci> head' "Привет" H'

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


 

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

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

 
 

Давайте напишем простую функцию, которая сообщает нам о нескольких первых элементах списка – в довольно неудобной, че- ресчур многословной форме.

tell :: (Show a) => [a] –> String tell [] = "Список пуст"

tell (x:[]) = "В списке один элемент: " ++ show x

tell (x:y:[]) = "В списке два элемента: " ++ show x ++ " и " ++ show y tell (x:y:_) = "Список длинный. Первые два элемента: " ++ show x

 
 

++ " и " ++ show y

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

 
 

Вот несколько примеров использования этой функции:

ghci> tell [1]

"В списке один элемент: 1" ghci> tell [True, False]

"В списке два элемента: True и False" ghci> tell [1, 2, 3, 4]

"Список длинный. Первые два элемента: 1 и 2" ghci> tell []

 
 

"Список пуст"

Функцию tell можно вызывать совершенно безопасно, потому что её параметр можно сопоставлять пустому списку, одноэлемент- ному списку, списку с двумя и более элементами. Она умеет работать со списками любой длины и всегда знает, что нужно возвратить.

 
 

А что если определить функцию, которая умеет обрабатывать только списки с тремя элементами? Вот один такой пример:

badAdd :: (Num a) => [a] -> a

 
 

badAdd (x:y:z:[]) = x + y + z

А вот что случится, если подать ей не то, что она ждёт:


 

ghci> badAdd [100, 20]

 
 

*** Exception: Non-exhaustive patterns in function badAdd

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

И последнее замечание относительно сопоставления с образ- цами для списков: в образцах нельзя использовать операцию ++ (напомню, что это объединение двух списков). К примеру, если вы попытаетесь написать в образце (xs++ys), то Haskell не сможет определить, что должно попасть в xs, а что в ys. Хотя и могут по- казаться логичными сопоставления типа (xs++[x,y,z]) или даже (xs ++ [x]), работать это не будет – такова природа списков1.

 

Именованные образцы

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

 
 

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

firstLetter :: String –> String firstLetter "" = "Упс, пустая строка!"

 
 

firstLetter all@(x:xs) = "Первая буква строки " ++ all ++ " это " ++ [x]

Загрузим эту функцию и посмотрим, как она работает:

ghci> firstLetter "Дракула" "Первая буква строки Дракула это Д"

1 На деле в образцах нельзя использовать операторы, представляющие собой двухместные функции (например, +, / и ++), поскольку при сопоставлении с об- разцами производится, по сути, обратная операция. Как сопоставить заданное число 5 с образцом (x + y)? Это можно сделать несколькими способами, то есть ситуация неопределённа. Между тем оператор : является конструктором данных (все бинарные операторы, начинающиеся с символа :, могут использоваться как конструкторы данных), поэтому для него можно произвести однозначное со- поставление. — Прим. ред.


ЭЙ, СТРАЖА! 67

Эй, стража!

В то время как образцы – это способ убедить- ся, что значение соответствует некоторой форме, и разобрать его на части, сторожевые условия (охрана, охранные выражения) – это способ проверить истинность некоторого свойства значения или нескольких значений, переданных функции. Тут можно провести аналогию с условным выражением if: оно работает схожим образом. Однако охранные выражения гораздо легче читать, если у вас имеется несколько условий; к тому же они от- лично работают с образцами.

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

(индекса массы тела). Ваш ИМТ равен вашему весу, разделенному на квадрат вашего роста.

 
 

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

bmiTell :: Double -> String bmiTell bmi

| bmi <= 18.5 = "Слышь, эмо, ты дистрофик!"

| bmi <= 25.0 = "По части веса ты в норме. Зато, небось, уродец!"

| bmi <= 30.0 = "Ты толстый! Сбрось хоть немного веса!"

 
 

| otherwise = "Мои поздравления, ты жирный боров!"

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


 

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

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

Во многих случаях последним охранным выражением являет- ся otherwise («иначе»). Значение otherwise определяется просто: otherwise = True; такое условие всегда истинно. Работа условий очень похожа на то, как работают образцы, но образцы проверяют входные данные, а охранные выражения могут производить любые проверки. Если все охранные выражения ложны (и при этом мы не записа-

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

 
 

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

bmiTell :: Double -> Double -> String bmiTell weight height

| weight / height ^ 2 <= 18.5 = "Слышь, эмо, ты дистрофик!"

| weight / height ^ 2 <= 25.0 = "По части веса ты в норме.

Зато, небось, уродец!"

| weight / height ^ 2 <= 30.0 = "Ты толстый!

Сбрось хоть немного веса!"

 
 

| otherwise = "Мои поздравления, ты жирный боров!"

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


 

ghci> bmiTell 85 1.90

 
 

"По части веса ты в норме. Зато, небось, уродец!"

Ура! По крайней мере, я не толстый! Правда, Haskell обозвал меня уродцем. Ну, это не в счёт.

ПРИМЕЧАНИЕ.Обратите внимание, что после имени функции и ее параметров нет знака равенства до первого охранного выра- жения. Многие новички ставят этот знак, что приводит к ошибке.

 
 

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

max' :: (Ord a) => a –> a –> a max' a b

| a <= b = b

 
 

| otherwise = a

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

myCompare :: (Ord a) => a –> a –> Ordering a `myCompare` b

| a == b = EQ

| a <= b = LT

 
 

| otherwise = GT

 

ghci> 3 `myCompare` 2 GT

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

Где же ты, where?!

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


 

промежуточных вычислений примерно с той же функциональ- ностью.

 
 

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

bmiTell :: Double -> Double -> String bmiTell weight height

| weight / height ^ 2 <= 18.5 = "Слышь, эмо, ты дистрофик!"

| weight / height ^ 2 <= 25.0 = "По части веса ты в норме.

Зато, небось, уродец!"

| weight / height ^ 2 <= 30.0 = "Ты толстый!

Сбрось хоть немного веса!"

 
 

| otherwise = "Мои поздравления, ты жирный боров!"

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

bmiTell :: Double -> Double -> String bmiTell weight height

| bmi <= 18.5 = "Слышь, эмо, ты дистрофик!"

| bmi <= 25.0 = "По части веса ты в норме.

Зато, небось, уродец!"

| bmi <= 30.0 = "Ты толстый!

Сбрось хоть немного веса!"

 
 

| otherwise = "Мои поздравления, ты жирный боров!" where bmi = weight / height ^ 2

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

Использование ключевого слова where улучшает читаемость, так как даёт имена понятиям и может сделать программы быстрее за счёт того, что переменные вроде bmi вычисляются лишь однаж-


 

 
 

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

bmiTell :: Double -> Double -> String bmiTell weight height

| bmi <= skinny = "Слышь, эмо, ты дистрофик!"

| bmi <= normal = "По части веса ты в норме.

Зато, небось, уродец!"

| bmi <= fat = "Ты толстый!

Сбрось хоть немного веса!"

| otherwise = "Мои поздравления, ты жирный боров!" where bmi = weight / height ^ 2

skinny = 18.5

normal = 25.0

 
 

fat = 30.0

 

ПРИМЕЧАНИЕ.Заметьте, что все идентификаторы расположены в одном столбце. Если не отформатировать исходный код подоб- ным образом, язык Haskell не поймёт, что все они – часть одного блока определений.







Последнее изменение этой страницы: 2017-02-17; Нарушение авторского права страницы

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 34.204.202.44 (0.016 с.)