В этой главе мы встретимся с условными конструкциями, выглянем в терминал, а также узнаем, почему из Haskell-функций не возвращаются (впрочем, последнее — не более чем игра слов).
Мы начинаем писать настоящий код. А для этого нам понадобится окно во внешний мир. Откроем модуль app/Main.hs
, найдём функцию main
и напишем в ней следующее:
main :: IO ()
main = putStrLn "Hi, real world!"
Стандартная функция putStrLn
выводит строку на консоль. А если говорить строже, функция putStrLn
применяется к значению типа String
и делает так, чтобы мы увидели это значение в нашем терминале.
Да, я уже слышу вопрос внимательного читателя. Как же так, спросите вы, разве мы не говорили о чистых функциях в прошлой главе, неспособных взаимодействовать с внешним миром? Придётся признаться: функция putStrLn
относится к особым функциям, которые могут-таки вылезти во внешний мир. Но об этом в следующих главах. Это прелюбопытнейшая тема, поверьте мне!
И ещё нам следует познакомиться с Haskell-комментариями, они нам понадобятся:
{-
Я - сложный многострочный
комментарий, содержащий
нечто
очень важное!
-}
main :: IO ()
main =
-- А я - скромный однострочный комментарий.
putStrLn "Hi, real world!"
Символы {-
и -}
скрывают многострочный комментарий, а символ --
начинает комментарий однострочный.
На всякий случай напоминаю команду сборки, запускаемую из корня проекта:
$ stack build
После сборки запускаем:
$ stack exec real-exe
Hi, real world!
Выбирать внутри функции приходится очень часто. Существует несколько способов задания условной конструкции. Вот базовый вариант:
if CONDITION then EXPR1 else EXPR2
где CONDITION
— логическое выражение, дающее ложь или истину, EXPR1
— выражение, используемое в случае True
, EXPR2
— выражение, используемое в случае False
. Пример:
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). И это действительно так. Если напишем:
main :: IO ()
main = putStrLn (checkLocalhost "127.0.0.1")
при запуске увидим это:
It's a localhost!
а если так:
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100")
тогда увидим это:
No, it's not a localhost.
Круглые скобки включают выражение типа String
по схеме:
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100")
└─── выражение типа String ───┘
То есть функция putStrLn
видит не применение функции checkLocalhost
к строке, а просто выражение типа String
. Если бы мы опустили скобки и написали так:
main :: IO ()
main = putStrLn checkLocalhost "173.194.22.100"
произошла бы ошибка компиляции, и это вполне ожидаемо: функция putStrLn
применяется к одному аргументу, а тут их получается два:
main = putStrLn checkLocalhost "173.194.22.100"
функция к этому
применяется аргументу...
и к этому??
Не знаю как вы, а я не очень люблю круглые скобки, при всём уважении к Lisp-программистам. К счастью, в 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."
То, что слева от знака равенства, равно тому, что справа. А раз так, эти два кода эквивалентны:
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100")
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
. В итоге, в зависимости от истинности или ложности проверок на равенство, эта условная конструкция будет также заменена одним из двух выражений. В этом и заключается идея: возвращаемое функцией значение — это её последнее, итоговое выражение. То есть если выражение:
"173.194.22.100" == "127.0.0.1" ||
"173.194.22.100" == "0.0.0.0"
даст нам результат True
, то мы переходим к выражению из логической ветви then
. Если же оно даст нам False
— мы переходим к выражению из логической ветви else
. Это даёт нам право утверждать, что условная конструкция вида:
if True
then "It's a localhost!"
else "No, it's not a localhost."
может быть заменена на первое нередуцируемое выражение, строку It's a localhost!
, а условную конструкцию вида:
if False
then "It's a localhost!"
else "No, it's not a localhost."
можно спокойно заменить вторым нередуцируемым выражением, строкой No, it's not a localhost.
. Поэтому код:
main :: IO ()
main = putStrLn (checkLocalhost "0.0.0.0")
эквивалентен коду:
main :: IO ()
main = putStrLn "It's a localhost!"
Аналогично, код:
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100")
есть ни что иное, как:
main :: IO ()
main = putStrLn "No, it's not a localhost."
Каким бы сложным ни было логическое ветвление внутри функции checkLocalhost
, в конечном итоге оно вернёт/вычислит какое-то одно итоговое выражение. Именно поэтому из функции в Haskell нельзя выйти в произвольном месте, как это принято в императивных языках, ведь она не является набором инструкций, она — выражение, состоящее из других выражений. Вот почему функции в Haskell так просто компоновать друг с другом, и позже мы встретим множество таких примеров.
Внимательный читатель несомненно заметил необычное объявление главной функции нашего проекта, функции main
:
main :: IO () -- Объявление?
main = putStrLn ...
Если IO
— это тип, то что такое ()
? И почему указан лишь один тип? Что такое IO ()
: аргумент функции main
, или же то, что она вычисляет? Сожалею, но пока я вынужден сохранить это в секрете. Когда мы поближе познакомимся со Вторым Китом Haskell, я непременно расскажу про этот странный IO ()
.