denisshevchenko/ohaskell.guide

View on GitHub
chapters/07-if-n-return.md

Summary

Maintainability
Test Coverage
# Выбираем и возвращаемся

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

## Выглянем во внешний мир

Мы начинаем писать настоящий код. А для этого нам понадобится окно во внешний мир. Откроем модуль `app/Main.hs`, найдём функцию `main` и напишем в ней следующее:

```haskell
main :: IO ()
main = putStrLn "Hi, real world!"
```

Стандартная функция `putStrLn` выводит строку на консоль. А если говорить строже, функция `putStrLn` применяется к значению типа `String` и делает так, чтобы мы увидели это значение в нашем терминале.

Да, я уже слышу вопрос внимательного читателя. Как же так, спросите вы, разве мы не говорили о чистых функциях в прошлой главе, неспособных взаимодействовать с внешним миром? Придётся признаться: функция `putStrLn` относится к особым функциям, которые могут-таки вылезти во внешний мир. Но об этом в следующих главах. Это прелюбопытнейшая тема, поверьте мне!

И ещё нам следует познакомиться с Haskell-комментариями, они нам понадобятся:

```haskell
{-
    Я - сложный многострочный
     комментарий, содержащий
  нечто
        очень важное!
-}
main :: IO ()
main =
  -- А я - скромный однострочный комментарий.
  putStrLn "Hi, real world!"
```

Символы `{-` и `-}` скрывают многострочный комментарий, а символ `--` начинает комментарий однострочный.

На всякий случай напоминаю команду сборки, запускаемую из корня проекта:

```bash
$ stack build
```

После сборки запускаем:

```bash
$ stack exec real-exe
Hi, real world!
```

## Выбор и выход

Выбирать внутри функции приходится очень часто. Существует несколько способов задания условной конструкции. Вот базовый вариант:

```haskell
if CONDITION then EXPR1 else EXPR2
```

где `CONDITION` — логическое выражение, дающее ложь или истину, `EXPR1` — выражение, используемое в случае `True`, `EXPR2` — выражение, используемое в случае `False`. Пример:

```haskell
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")
```

при запуске увидим это:

```bash
It's a localhost!
```

а если так:

```haskell
main :: IO ()
main = putStrLn (checkLocalhost "173.194.22.100")
```

тогда увидим это:

```bash
No, it's not a localhost.
```

Круглые скобки включают выражение типа `String` по схеме:

```haskell
main :: IO ()
main = putStrLn (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 так просто компоновать друг с другом, и позже мы встретим множество таких примеров.

## Для любопытных

Внимательный читатель несомненно заметил необычное объявление главной функции нашего проекта, функции `main`:

```haskell
main :: IO ()   -- Объявление?
main = putStrLn ...
```

Если `IO` — это тип, то что такое `()`? И почему указан лишь один тип? Что такое `IO ()`: аргумент функции `main`, или же то, что она вычисляет? Сожалею, но пока я вынужден сохранить это в секрете. Когда мы поближе познакомимся со Вторым Китом Haskell, я непременно расскажу про этот странный `IO ()`.