# Глава 6. Выбор и образцы Эта глава откроет нам другие способы выбора, также вы узнаете про сопоставление с образцом. Уверяю, вы не останетесь равнодушными! ## Не только из двух Часто мы хотим выбирать не только из двух возможных вариантов. Вот как это можно сделать: ```haskell {.example_for_playground} 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 можно написать по-другому: ```haskell {.example_for_playground .example_for_playground_001} 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`. Работает он по схеме: ```haskell if | COND1 -> EXPR1 | COND2 -> EXPR2 | ... | CONDn -> EXPRn | otherwise -> COMMON_EXPR ``` где `COND1..n` — выражения, дающие ложь или истину, а `EXPR1..n` — соответствующие им результирующие выражения. Особая функция `otherwise` соответствует общему случаю, когда ни одно из логических выражений не дало `True`, и в этой ситуации результатом условной конструкции послужит выражение `COMMON_EXPR`. Проведите рефакторинг функции `mercuryState`, которая принимает температуру ртути в градусах Цельсия и возвращает ее агрегатное состояние. {.task_text} В теле функции замените вложенный `if-then-else` на множественный `if`. {.task_text} ```haskell {.task_source #haskell_chapter_0060_task_0010} {-# LANGUAGE MultiWayIf #-} module Main where eqAbs :: Double -> Double -> Double -> Bool eqAbs a b eps = abs (a - b) <= eps mercuryState :: Double -> String mercuryState t = if eqAbs t (-38.83) 1e-2 then "Solid -> liquid" else if eqAbs t 356.73 1e-2 then "Liquid -> gas" else if t > (-38.83) && t < 356.73 then "Liquid" else if t < (-38.83) then "Solid" else "Gas" main :: IO () main = do print (mercuryState (-39)) print (mercuryState (-38.83)) print (mercuryState 0) print (mercuryState 356.73) print (mercuryState 400) ``` Синтаксис множественного `if`: `if | COND1 -> EXPR1 | ... | CONDn -> EXPRn | otherwise -> COMMON_EXPR`. {.task_hint} ```haskell {.task_answer} {-# LANGUAGE MultiWayIf #-} module Main where eqAbs :: Double -> Double -> Double -> Bool eqAbs a b eps = abs (a - b) <= eps mercuryState :: Double -> String mercuryState t = if | eqAbs t (-38.83) 1e-2 -> "Solid -> liquid" | eqAbs t 356.73 1e-2 -> "Liquid -> gas" | t > (-38.83) && t < 356.73 -> "Liquid" | t < (-38.83) -> "Solid" | otherwise -> "Gas" main :: IO () main = do print (mercuryState (-39)) print (mercuryState (-38.83)) print (mercuryState 0) print (mercuryState 356.73) print (mercuryState 400) ``` Не пренебрегайте `otherwise`! Возвращаясь к функции `analyzeGold`: если вы не укажете `otherwise` и при этом примените функцию к значению, отличному от проверяемых: ```haskell {.example_for_playground} 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`: ```haskell {.example_for_playground} {-# 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](https://www.haskell.org/onlinereport/haskell2010/) — это официальный стержень языка. Однако компилятор GHC, давно уж ставший компилятором по умолчанию при разработке на Haskell, обладает рядом особых возможностей. По умолчанию многие из этих возможностей выключены, а прагма `LANGUAGE` как раз для того и предназначена, чтобы их включать/активизировать. В данном случае мы включили расширение `MultiWayIf`. Именно это расширение позволяет нам использовать множественный `if`. Такого рода расширений существует очень много, и мы будем часто их использовать. Помните: расширение, включённое с помощью прагмы `LANGUAGE`, действует лишь в рамках текущего модуля. И если я прописал его только в модуле `app/Main.hs`, то на модуль `src/Lib.hs` механизм `MultiWayIf` не распространяется. ## Без Если Множественный `if` весьма удобен, но есть способ более красивый. Взгляните: ```haskell {.example_for_playground .example_for_playground_002} 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` исчезло. Схема здесь такая: ```haskell function arg -- Нет знака равенства? | COND1 = EXPR1 | COND2 = EXPR2 | ... | CONDn = EXPRn | otherwise = COMMON_EXPR ``` Устройство почти такое же, но, помимо исчезновения ключевого слова `if`, мы теперь используем знаки равенства вместо стрелок. Именно поэтому исчез знакомый нам знак равенства после имени аргумента `arg`. В действительности он, конечно, никуда не исчез, он лишь перешёл в выражения. А чтобы это легче прочесть, напишем выражения в строчку: ```haskell function arg | COND1 = EXPR1 | ... ``` То есть перед нами уже не одно определение функции, а цепочка определений, потому нам и не нужно ключевое слово `if`. Объявите модуль `Main`. Заведите функцию `requestStatus`. Она должна принимать целочисленный аргумент — HTTP-код ответа от сервера. И в зависимости от кода возвращать строку: {.task_text} - 1xx — "Informational". - 2xx — "Success". - 3xx — "Redirection". - 4xx — "Client error". - 5xx — "Server error". - В любом другом случае — "Invalid HTTP code". {.task_text} ```haskell {.task_source #haskell_chapter_0060_task_0020} -- Your code here main :: IO () main = do print (requestStatus 0) print (requestStatus 102) print (requestStatus 201) print (requestStatus 304) print (requestStatus 403) print (requestStatus 500) print (requestStatus 600) ``` Пример условия из цепочки определений: `| code >= 300 && code < 400 = "Redirection"`. {.task_hint} ```haskell {.task_answer} module Main where requestStatus :: Int -> String requestStatus code | code >= 100 && code < 200 = "Informational" | code >= 200 && code < 300 = "Success" | code >= 300 && code < 400 = "Redirection" | code >= 400 && code < 500 = "Client error" | code >= 500 && code < 600 = "Server error" | otherwise = "Invalid HTTP code" main :: IO () main = do print (requestStatus 0) print (requestStatus 102) print (requestStatus 201) print (requestStatus 304) print (requestStatus 403) print (requestStatus 500) print (requestStatus 600) ``` Что выведет этот код? В случае ошибки напишите `error`. {.task_text} ```haskell {.example_for_playground} module Main where f :: Int -> Int -> Char f x y | div x 2 == 0 && div y 2 /= 0 = 'A' | otherwise = 'B' main :: IO () main = print (f 17 0) ``` ```consoleoutput {.task_source #haskell_chapter_0060_task_0030} ``` Если аргумент `x` функции `f` четный, а `y` — нечетный, то функция возвращает `'A'` Иначе — `'B'`. {.task_hint} ```haskell {.task_answer} B ``` Но и эту цепочку определений `function arg | COND1 = EXPR1 | ...` можно упростить. ## Сравнение с образцом {#block-pattern-matching} Убрав слово `if`, мы и с нашими виртуальными «ИЛИ» можем расстаться. В этом случае останется лишь это: ```haskell {.example_for_playground .example_for_playground_003} 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` применяется к конкретному аргументу, этот аргумент последовательно сравнивается с образцом (англ. pattern matching). Образца здесь три: `999`, `750` и `585`. И если раньше мы сравнивали аргумент с этими числовыми значениями явно, посредством функции `==`, теперь это происходит скрыто. Идея сравнения с образцом очень проста: что-то (в данном случае реальный аргумент) сопоставляется с образцом (или образцами) на предмет «подходит/не подходит». Если подходит — то есть сравнение с образцом даёт результат `True` — готово, используем соответствующее выражение. Если же не подходит — переходим к следующему образцу. Сравнение с образцом, называемое ещё «сопоставлением с образцом», используется в Haskell чрезвычайно широко. В русскоязычной литературе перевод словосочетания «pattern matching» не особо закрепился, вместо этого так и говорят «паттерн матчинг». Я поступлю так же. Но что это за символ подчёркивания такой, в последнем варианте определения? Вот этот `_`: {#block-wildcard-match} ```haskell analyzeGold _ = "I don't know such a standard..." ``` С формальной точки зрения, это — универсальный образец, сравнение с которым всегда истинно. Ещё говорят, что с ним матчится (англ. match) всё что угодно. А с неформальной — это символ, который можно прочесть как «мне всё равно». Мы как бы говорим: «В данном случае нас не интересует конкретное содержимое аргумента, нам всё равно, мы просто возвращаем строку `I don't know such a standard...`». Важно отметить, что сравнение аргумента с образцами происходит последовательно, сверху вниз. Поэтому если мы напишем так: ```haskell {.example_for_playground .example_for_playground_004} 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`, ведь с образцом `_` совпадает всё что угодно. Таким образом, общий образец следует располагать в самом конце, чтобы мы попали на него лишь после того, как не сработали все остальные образцы. Напишите функцию `baseToInt`, которая принимает строку и возвращает число по принципу: {.task_text} - "Bin" — 2. - "Oct" — 8. - "Hex" — 16. - -1 в любом другом случае. {.task_text} Для реализации функции используйте паттерн матчинг. {.task_text} ```haskell {.task_source #haskell_chapter_0060_task_0040} module Main where -- Your code here main :: IO () main = do print (baseToInt "Bin") print (baseToInt "Oct") print (baseToInt "Hex") print (baseToInt "Random") ``` Не забудьте обработать общий случай: `baseToInt _ = -1`. {.task_hint} ```haskell {.task_answer} module Main where baseToInt :: String -> Int baseToInt "Bin" = 2 baseToInt "Oct" = 8 baseToInt "Hex" = 16 baseToInt _ = -1 main :: IO () main = do print (baseToInt "Bin") print (baseToInt "Oct") print (baseToInt "Hex") print (baseToInt "Random") ``` ## Конструкция case-of Существует ещё один вид паттерн матчинга, с помощью конструкции `case-of`: ```haskell {.example_for_playground .example_for_playground_005} 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`, мы встретимся с нею не раз. Работает она по модели: ```haskell case EXPRESSION of PATTERN1 -> EXPR1 PATTERN2 -> EXPR2 ... PATTERNn -> EXPRn _ -> COMMON_EXPR ``` где `EXPRESSION` — анализируемое выражение, последовательно сравниваемое с образцами `PATTERN1..n`. Если ни одно не сработало — как обычно, упираемся в универсальный образец `_` и выдаём `COMMON_EXPR`. Перепишите функцию `baseToInt` с использованием `case-of`. {.task_text} ```haskell {.task_source #haskell_chapter_0060_task_0050} module Main where baseToInt :: String -> Int baseToInt "Bin" = 2 baseToInt "Oct" = 8 baseToInt "Hex" = 16 baseToInt _ = -1 main :: IO () main = do print (baseToInt "Bin") print (baseToInt "Oct") print (baseToInt "Hex") print (baseToInt "Random") ``` Функция принимает единственный аргумент, допустим `base`. В теле функции в выражении `case base of` обрабатываются 3 варианта строк и универсальный образец `_`. {.task_hint} ```haskell {.task_answer} module Main where baseToInt :: String -> Int baseToInt base = case base of "Bin" -> 2 "Oct" -> 8 "Hex" -> 16 _ -> -1 main :: IO () main = do print (baseToInt "Bin") print (baseToInt "Oct") print (baseToInt "Hex") print (baseToInt "Random") ``` В последующих главах мы встретимся и с другими видами паттерн матчинга, ведь он используется не только для выбора.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!