Эта глава откроет нам другие способы выбора, а также познакомит нас с образцами. Уверяю, вы влюбитесь в них!
Часто мы хотим выбирать не только из двух возможных вариантов. Вот как это можно сделать:
analyzeGold :: Int -> String
analyzeGold standard =
if standard == 999
then "Wow! 999 standard!"
else if standard == 750
then "Great! 750 standard."
else if standard == 585
then "Not bad! 585 standard."
else "I don't know such a standard..."
main :: IO ()
main = putStrLn (analyzeGold 999)
Уверен, вы уже стираете плевок с экрана. Вложенная if-then-else
конструкция не может понравиться никому, ведь она крайне неудобна в обращении. А уж если бы анализируемых проб золота было штук пять или семь, эта лестница стала бы поистине ужасной. К счастью, в Haskell можно написать по-другому:
analyzeGold :: Int -> String
analyzeGold standard =
if | standard == 999 -> "Wow! 999 standard!"
| standard == 750 -> "Great! 750 standard."
| standard == 585 -> "Not bad! 585 standard."
| otherwise -> "I don't know such a standard..."
Не правда ли, так красивее? Это — множественный if
. Работает он по схеме:
if | COND1 -> EXPR1
| COND2 -> EXPR2
| ...
| CONDn -> EXPRn
| otherwise -> COMMON_EXPR
где COND1..n
— выражения, дающие ложь или истину, а EXPR1..n
— соответствующие им результирующие выражения. Особая функция otherwise
соответствует общему случаю, когда ни одно из логических выражений не дало True
, и в этой ситуации результатом условной конструкции послужит выражение COMMON_EXPR
.
Не пренебрегайте otherwise
! Если вы его не укажете и при этом примените функцию analyzeGold
к значению, отличному от проверяемых:
analyzeGold :: Int -> String
analyzeGold standard =
if | standard == 999 -> "Wow! 999 standard!"
| standard == 750 -> "Great! 750 standard."
| standard == 585 -> "Not bad! 585 standard."
main :: IO ()
main = putStrLn (analyzeGold 583) -- Ой...
компиляция завершится успешно, однако в момент запуска программы вас ожидает неприятный сюрприз в виде ошибки:
Non-exhaustive guards in multi-way if
Проверка получилась неполной, вот и получите ошибку.
Кстати, видите слово guards
в сообщении об ошибке? Вертикальные черты перед логическими выражениями — это и есть охранники (англ. guard), неусыпно охраняющие наши условия. Потешное название выбрали. Чтобы читать их было легче, воспринимайте их как аналог слова «ИЛИ».
А сейчас стоп. Вы ведь попробовали скомпилировать этот код, не так ли? А почему вы не ругаетесь? Ведь такой код не скомпилируется, так как не хватает одной маленькой, но важной детали. Вот как должен выглядеть модуль Main
:
{-# LANGUAGE MultiWayIf #-} -- Что это??
module Main where
analyzeGold :: Int -> String
analyzeGold standard =
if | standard == 999 -> "Wow! 999 standard!"
| standard == 750 -> "Great! 750 standard."
| standard == 585 -> "Not bad! 585 standard."
| otherwise -> "I don't know such a standard..."
main :: IO ()
main = putStrLn (analyzeGold 999)
Вот теперь всё в порядке. Но что это за странный комментарий в первой строке модуля? Вроде бы оформлен как многострочный комментарий, но выглядит необычно. Перед нами — указание расширения языка Haskell.
Стандарт Haskell 2010 — это официальный стержень языка. Однако компилятор GHC, давно уж ставший компилятором по умолчанию при разработке на Haskell, обладает рядом особых возможностей. По умолчанию многие из этих возможностей выключены, а прагма LANGUAGE
как раз для того и предназначена, чтобы их включать/активизировать. В данном случае мы включили расширение MultiWayIf
. Именно это расширение позволяет нам использовать множественный if
. Такого рода расширений существует очень много, и мы будем часто их использовать.
Помните: расширение, включённое с помощью прагмы LANGUAGE
, действует лишь в рамках текущего модуля. И если я прописал его только в модуле app/Main.hs
, то на модуль src/Lib.hs
механизм MultiWayIf
не распространяется.
Множественный if
весьма удобен, но есть способ более красивый. Взгляните:
analyzeGold :: Int -> String
analyzeGold standard
| standard == 999 = "Wow! 999 standard!"
| standard == 750 = "Great! 750 standard."
| standard == 585 = "Not bad! 585 standard."
| otherwise = "I don't know such a standard..."
Ключевое слово if
исчезло. Схема здесь такая:
function arg -- Нет знака равенства?
| COND1 = EXPR1
| COND2 = EXPR2
| ...
| CONDn = EXPRn
| otherwise = COMMON_EXPR
Устройство почти такое же, но, помимо исчезновения ключевого слова if
, мы теперь используем знаки равенства вместо стрелок. Именно поэтому исчез знакомый нам знак равенства после имени аргумента arg
. В действительности он, конечно, никуда не исчез, он лишь перешёл в выражения. А чтобы это легче прочесть, напишем выражения в строчку:
function arg | COND1 = EXPR1 | ...
эта или равна
функция
этому
выражению
в случае
истинности
этого
выражения
или и т.д.
То есть перед нами уже не одно определение функции, а цепочка определений, потому нам и не нужно ключевое слово if
. Но и эту цепочку определений можно упростить.
Убрав слово if
, мы и с нашими виртуальными «ИЛИ» можем расстаться. В этом случае останется лишь это:
analyzeGold :: Int -> String -- Одно объявление.
-- И множество определений...
analyzeGold 999 = "Wow! 999 standard!"
analyzeGold 750 = "Great! 750 standard."
analyzeGold 585 = "Not bad! 585 standard."
analyzeGold _ = "I don't know such a standard..."
Мы просто перечислили определения функции analyzeGold
одно за другим. На первый взгляд, возможность множества определений одной и той же функции удивляет, но если вспомнить, что применение функции суть выражение, тогда ничего удивительного. Вот как это читается:
analyzeGold 999 = "Wow! 999 standard!"
если эта функция применяется тогда этому выражению
к этому она
аргументу равна
analyzeGold 750 = "Wow! 999 standard!"
если эта функция применяется тогда другому выражению
к другому она
аргументу равна
...
analyzeGold _ = "I don't know such a standard..."
иначе эта функция равна общему выражению
Когда функция analyzeGold
применяется к конкретному аргументу, этот аргумент последовательно сравнивается с образцом (англ. pattern matching). Образца здесь три: 999
, 750
и 585
. И если раньше мы сравнивали аргумент с этими числовыми значениями явно, посредством функции ==
, теперь это происходит скрыто. Идея сравнения с образцом очень проста: что-то (в данном случае реальный аргумент) сопоставляется с образцом (или образцами) на предмет «подходит/не подходит». Если подходит — то есть сравнение с образцом даёт результат True
— готово, используем соответствующее выражение. Если же не подходит — переходим к следующему образцу.
Сравнение с образцом, называемое ещё «сопоставлением с образцом» используется в Haskell чрезвычайно широко. В русскоязычной литературе перевод словосочетания «pattern matching» не особо закрепился, вместо этого так и говорят «паттерн матчинг». Я поступлю так же.
Но что это за символ подчёркивания такой, в последнем варианте определения? Вот этот:
analyzeGold _ = "I don't know such a standard..."
^
С формальной точки зрения, это — универсальный образец, сравнение с которым всегда истинно (ещё говорят, что с ним матчится (англ. match) всё что угодно). А с неформальной — это символ, который можно прочесть как «мне всё равно». Мы как бы говорим: «В данном случае нас не интересует конкретное содержимое аргумента, нам всё равно, мы просто возвращаем строку I don't know such a standard...
».
Важно отметить, что сравнение аргумента с образцами происходит последовательно, сверху вниз. Поэтому если мы напишем так:
analyzeGold :: Int -> String
analyzeGold _ = "I don't know such a standard..."
analyzeGold 999 = "Wow! 999 standard!"
analyzeGold 750 = "Great! 750 standard."
analyzeGold 585 = "Not bad! 585 standard."
наша функция будет всегда возвращать первое выражение, строку I don't know such a standard...
, и это вполне ожидаемо: первая же проверка гарантированно даст нам True
, ведь с образцом _
совпадает всё что угодно. Таким образом, общий образец следует располагать в самом конце, чтобы мы попали на него лишь после того, как не сработали все остальные образцы.
Существует ещё один вид паттерн матчинга, с помощью конструкции case-of
:
analyzeGold standard =
case standard of
999 -> "Wow! 999 standard!"
750 -> "Great! 750 standard."
585 -> "Not bad! 585 standard."
_ -> "I don't know such a standard..."
Запомните конструкцию case-of
, мы встретимся с нею не раз. Работает она по модели:
case EXPRESSION of
PATTERN1 -> EXPR1
PATTERN2 -> EXPR2
...
PATTERNn -> EXPRn
_ -> COMMON_EXPR
где EXPRESSION
— анализируемое выражение, последовательно сравниваемое с образцами PATTERN1..n
. Если ни одно не сработало — как обычно, упираемся в универсальный образец _
и выдаём COMMON_EXPR
.
В последующих главах мы встретимся и с другими видами паттерн матчинга, ведь он используется не только для выбора.