В данной главе мы хорошенько изучили, как работают параметры типов, и как они формализуются с помощью сортов по аналогии с тем, как формализуются параметры функций с помощью декларации типов. Мы провели любопытные параллели между функциями и конструкторами типов, хотя на первый взгляд они и не имеют ничего общего. При реальной работе с языком Haskell обычно не приходится возиться с сортами и делать вывод сортов вручную, как мы делали в этой главе. Обычно вы просто частично применяете свой тип к сорту * –> *
или *
при создании экземпляра от одного из стандартных классов типов, но полезно знать, как это работает на самом деле. Также интересно, что у типов есть свои собственные маленькие типы.
Ещё раз повторю: вы не должны понимать всё, что мы сделали, в деталях, но если вы по крайней мере понимаете, как работают сор та, есть надежда на то, что вы постигли суть системы типов языка Haskell.
8
Ввод-вывод
Разделение «чистого» и «нечистого»
В этой главе вы узнаете, как вводить данные с клавиатуры и печатать их на экран. Начнём мы с основ ввода-вывода:
• Что такое действия?
• Как действия позволяют выполнять ввод-вывод?
• Когда фактически исполняются действия?
Вводу-выводу приходится иметь дело с некоторыми ограничениями функций языка Haskell, поэтому первым делом мы обсудим, что с этим можно сделать.
Мы уже упоминали, что Haskell – чисто функциональный язык. В то время как в императивных языках вы указываете компьютеру серию шагов для достижения некой цели, в функциональном программировании мы описываем, чем является то или иное понятие. В языке Haskell функция не может изменить некоторое состояние, например поменять значение переменной (если функция изменяет состояние, мы говорим, что она имеет побочные эффекты). Единственное, что могут сделать функции в языке Haskell, – это вернуть нам некоторый результат, основываясь на переданных им параметрах. Если вызвать функцию дважды с одинаковыми параметрами, она всегда вернёт одинаковый результат. Если вы знакомы с императивными языками, может показаться, что это ограничивает свободу наших действий, но мы видели, что на самом деле это даёт весьма мощные возможности. В императивном языке у вас нет гарантии, что простая функция, которая всего-то навсего должна обсчитать пару чисел, не сожжёт ваш дом, не похитит собаку и не поцарапает машину во время вычислений! Например, когда мы создавали бинарное поисковое дерево, то вставляли элемент в дерево не путём модификации дерева в точке вставки. Наша функция добавления нового элемента в дерево возвращала новое дерево, так как не могла изменить старое.
Конечно, это хорошо, что функции не могут изменять состояние: это помогает нам строить умозаключения о наших программах. Но есть одна проблема. Если функция не может ничего изменить, как она сообщит нам о результатах вычислений? Для того чтобы вывести результат, она должна изменить состояние устройства вывода – обычно это экран, который излучает фотоны; они путешествуют к нашему мозгу и изменяют состояние нашего сознания… вот так-то, чувак!
Но не надо отчаиваться, не всё ещё потеряно. Оказывается, в языке Haskell есть весьма умная система для работы с функциями с побочными эффектами, которая чётко разделяет чисто функциональную и «грязную» части нашей программы. «Грязная» часть выполняет всю грязную работу, например отвечает за взаимодействие с клавиатурой и экраном. Разделив «чистую» и «грязную»части, мы можем так же свободно рассуждать о чисто функциональной части нашей программы, получать все преимущества функциональной чистоты, а именно – ленивость, гибкость, модульность, и при этом эффективно взаимодействовать с внешним миром.
Привет, мир!
До сих пор для того, чтобы протестировать наши функции, мы загружали их в интерпретатор GHCi. Там же мы изучали функции из стандартной библиотеки. Но теперь, спустя семь глав, мы наконец-то собираемся написать первую программу на языке Haskell! Ура! И, конечно же, это будет старый добрый шедевр «Привет, мир».
Итак, для начинающих: наберите в вашем любимом текстовом редакторе строку
main = putStrLn "Привет, мир"
Мы только что определили имя main
; в нём мы вызываем функцию putStrLn
с параметром "Привет, мир"
. На первый взгляд, ничего необычного, но это не так: мы убедимся в этом через несколько минут. Сохраните файл как
Сейчас мы собираемся сделать то, чего ещё не пробовали делать. Мы собираемся скомпилировать нашу программу! Я даже разволновался!.. Откройте ваш терминал, перейдите в папку с сохранённым файлом
$ ghc helloworld
[1 of 1] Compiling Main ( helloworld.hs, helloworld.o )
Linking helloworld …
О’кей! При некотором везении вы получите нечто похожее и теперь можете запустить свою программу, вызвав ./helloworld
.
$ ./helloworld
Привет, мир