Главная / Курсы / О Haskell по-человечески / Выбираем и возвращаемся
# Глава 5. Выбираем и возвращаемся В этой главе мы встретимся с условными конструкциями, выглянем в терминал, а также узнаем, почему из Haskell-функций не возвращаются (впрочем, последнее — не более чем игра слов). ## Выглянем во внешний мир Мы начинаем писать настоящий код. А для этого нам понадобится окно во внешний мир. Еще раз взглянем на модуль `app/Main.hs` с функцией `main`: ```haskell {.example_for_playground} main :: IO () main = putStrLn "Hi, real world!" ``` Вы уже знаете, что стандартная функция `putStrLn` выводит строку на консоль. А если говорить строже, функция `putStrLn` применяется к значению типа `String` и делает так, чтобы мы увидели это значение в нашем терминале. Да, я уже слышу вопрос внимательного читателя. Как же так, спросите вы, разве мы не говорили о чистых функциях в прошлой главе, неспособных взаимодействовать с внешним миром? Придётся признаться: функция `putStrLn` относится к особым функциям, которые могут-таки вылезти во внешний мир. Но об этом в следующих главах. Это прелюбопытнейшая тема, поверьте мне! ## Комментарии И ещё нам следует познакомиться с Haskell-комментариями, они нам понадобятся: ```haskell {.example_for_playground} {- Я - сложный многострочный комментарий, содержащий нечто очень важное! -} main :: IO () main = -- А я - скромный однострочный комментарий. putStrLn "Hi, real world!" ``` Символы `{-` и `-}` скрывают многострочный комментарий, а символ `--` начинает комментарий однострочный. ## Выбор и выход Выбирать внутри функции приходится очень часто. Существует несколько способов задания условной конструкции. Вот базовый вариант: ```haskell if CONDITION then EXPR1 else EXPR2 ``` где `CONDITION` — логическое выражение, дающее ложь или истину, `EXPR1` — выражение, используемое в случае `True`, `EXPR2` — выражение, используемое в случае `False`. Пример: {#block-check-localhost} ```haskell {.example_for_playground .example_for_playground_001} checkLocalhost :: String -> String checkLocalhost ip = -- True или False? if ip == "127.0.0.1" || ip == "0.0.0.0" -- Если True - идёт туда... then "It's a localhost" -- А если False - сюда... else "No, it's not a localhost" ``` Функция `checkLocalhost` применяется к единственному аргументу типа `String` и возвращает другое значение типа `String`. В качестве аргумента выступает строка, содержащая IP-адрес, а функция проверяет, не лежит ли в ней localhost. Оператор `||` — стандартный оператор логического «ИЛИ», а оператор `==` — стандартный оператор проверки на равенство. Кстати, для проверки на неравенство используется оператор `\=`. Итак, если строка `ip` равна `127.0.0.1` или `0.0.0.0`, значит в ней localhost, и мы возвращаем первое выражение, то есть строку `It's a localhost`, в противном случае возвращаем второе выражение, строку `No, it's not a localhost`. А кстати, что значит «возвращаем»? Ведь, как мы узнали, функции в Haskell не вызывают (англ. call), а значит, из них и не возвращаются (англ. return). И это действительно так. Если напишем: ```haskell main :: IO () main = putStrLn (checkLocalhost "127.0.0.1") ``` при запуске увидим это: ``` It's a localhost ``` а если так: ```haskell main :: IO () main = putStrLn (checkLocalhost "173.194.22.100") ``` тогда увидим это: ``` No, it's not a localhost ``` Круглые скобки включают выражение типа `String` по схеме: ```haskell main :: IO () main = putStrLn (checkLocalhost "173.194.22.100") ``` Здесь `checkLocalhost "173.194.22.100"` — выражение типа `String`. То есть функция `putStrLn` видит не применение функции `checkLocalhost` к строке, а просто выражение типа `String`. Если бы мы опустили скобки и написали так: ```haskell main :: IO () main = putStrLn checkLocalhost "173.194.22.100" ``` произошла бы ошибка компиляции, и это вполне ожидаемо: функция `putStrLn` применяется к одному аргументу, а тут их получается два: ```haskell main = putStrLn checkLocalhost "173.194.22.100" ``` Не знаю как вы, а я не очень люблю круглые скобки, при всём уважении к Lisp-программистам. К счастью, в Haskell существует способ уменьшить число скобок. Об этом способе — в одной из последующих глав. Так что же с возвращением из функции? Вспомним о равенстве в определении: ```haskell checkLocalhost ip = if ip == "127.0.0.1" || ip == "0.0.0.0" then "It's a localhost" else "No, it's not a localhost" ``` То, что слева от знака равенства, равно тому, что справа. А раз так, эти два кода эквивалентны: ```haskell main :: IO () main = putStrLn (checkLocalhost "173.194.22.100") ``` ```haskell main :: IO () main = putStrLn (if "173.194.22.100" == "127.0.0.1" || "173.194.22.100" == "0.0.0.0" then "It's a localhost" else "No, it's not a localhost") ``` Мы просто заменили применение функции `checkLocalhost` её внутренним выражением, подставив вместо аргумента `ip` конкретную строку `173.194.22.100`. В итоге, в зависимости от истинности или ложности проверок на равенство, эта условная конструкция будет также заменена одним из двух выражений. В этом и заключается идея: возвращаемое функцией значение — это её последнее, итоговое выражение. То есть если выражение: ```haskell "173.194.22.100" == "127.0.0.1" || "173.194.22.100" == "0.0.0.0" ``` даст нам результат `True`, то мы переходим к выражению из логической ветви `then`. Если же оно даст нам `False` — мы переходим к выражению из логической ветви `else`. Это даёт нам право утверждать, что условная конструкция вида: ```haskell if True then "It's a localhost" else "No, it's not a localhost" ``` может быть заменена на первое нередуцируемое выражение, строку `It's a localhost`, а условную конструкцию вида: ```haskell if False then "It's a localhost" else "No, it's not a localhost" ``` можно спокойно заменить вторым нередуцируемым выражением, строкой `No, it's not a localhost`. Поэтому код: ```haskell main :: IO () main = putStrLn (checkLocalhost "0.0.0.0") ``` эквивалентен коду: ```haskell main :: IO () main = putStrLn "It's a localhost" ``` Аналогично, код: ```haskell main :: IO () main = putStrLn (checkLocalhost "173.194.22.100") ``` есть ни что иное, как: ```haskell main :: IO () main = putStrLn "No, it's not a localhost" ``` Каким бы сложным ни было логическое ветвление внутри функции `checkLocalhost`, в конечном итоге оно вернёт/вычислит какое-то одно итоговое выражение. Именно поэтому из функции в Haskell нельзя выйти в произвольном месте, как это принято в императивных языках, ведь она не является набором инструкций, она — выражение, состоящее из других выражений. Вот почему функции в Haskell так просто компоновать друг с другом, и позже мы встретим множество таких примеров. Помните функцию `clamp` из прошлой главы? Перепишите ее так, чтобы вместо `min` и `max` функция использовала вложенный `if-then-else`. {.task_text} ```haskell {.task_source #haskell_chapter_0050_task_0010} module Main where clamp :: Int -> Int -> Int -> Int clamp a val b = min (max a val) b main :: IO () main = do print (clamp 0 128 100) print (clamp 0 (-3) 100) print (clamp 0 41 100) ``` Тело функции должно состоять из 2-х вложенных выражений `if-then-else`. {.task_hint} ```haskell {.task_answer} module Main where clamp :: Int -> Int -> Int -> Int clamp a val b = if val < a then a else if val < b then val else b main :: IO () main = do print (clamp 0 128 100) print (clamp 0 (-3) 100) print (clamp 0 41 100) ``` Напишите функцию `nextLetter`, которая принимает аргумент `с` типа `Char` — букву английского алфавита. И возвращает следующую за `c` букву по принципу циклического буфера: для `'a'` — `'b'`, для `'n'` — `'m'`, а для `'z'` — снова `'a'`. {.task_text} Чтобы получить следующее значение символа, примените к нему встроенную функцию `succ`. Эта функция умеет принимать любое значение, для которого может быть вычислено следующее значение. {.task_text} ```haskell {.task_source #haskell_chapter_0050_task_0020} module Main where -- Your code here main :: IO () main = do print (nextLetter 'g') print (nextLetter 'z') print (nextLetter 'r') print (nextLetter 'j') print (nextLetter 'd') print (nextLetter 'k') print (nextLetter 'k') ``` В теле функции воспользуйтесь проверкой: если буква равна `z`, то необходимо вернуть `a`. Иначе — вернуть применение `succ` к букве. {.task_hint} ```haskell {.task_answer} module Main where nextLetter :: Char -> Char nextLetter c = if c == 'z' then 'a' else succ c main :: IO () main = do print (nextLetter 'g') print (nextLetter 'z') print (nextLetter 'r') print (nextLetter 'j') print (nextLetter 'd') print (nextLetter 'k') print (nextLetter 'k') ``` ## Для любопытных Внимательный читатель несомненно заметил необычное объявление главной функции нашего проекта, функции `main`: {#block-io} ```haskell main :: IO () -- Объявление? main = putStrLn ... ``` Если `IO` — это тип, то что такое `()`? И почему указан лишь один тип? Что такое `IO ()`: аргумент функции `main`, или же то, что она вычисляет? Сожалею, но пока я вынужден сохранить это в секрете. Когда мы поближе познакомимся со Вторым Китом Haskell, я непременно [расскажу](/courses/haskell/chapters/haskell_chapter_0230#block-io) про этот странный `IO ()`. Что выведет этот код? В случае ошибки напишите `error`. Встроенная функция `mod` возвращает остаток от деления первого аргумента на второй. {.task_text} ```haskell {.example_for_playground} module Main where f :: Int -> Int f x = (if mod x 2 == 0 then x*2 else x*3) + 1 main :: IO () main = print (f 5) ``` ```consoleoutput {.task_source #haskell_chapter_0050_task_0030} ``` Функция `f` принимает целое `x`. Если аргумент `x` является четным числом, то он умножается на 2. Иначе — на 3. К полученному значению прибавляется единица. {.task_hint} ```haskell {.task_answer} 16 ```
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!