Миран Липовача - Изучай Haskell во имя добра!
ghci> :k Barry
Barry :: (* –> *) –> * –> * –> *
Ага, мы были правы. Как приятно! Чтобы сделать для типа Barry экземпляр класса Functor, мы должны частично применить первые два параметра, после чего у нас останется сорт * –> *. Следовательно, начало декларации экземпляра будет таким:
instance Functor (Barry a b) where
Если бы функция fmap была написана специально для типа Barry, она бы имела тип
fmap :: (a –> b) –> Barry c d a –> Barry c d b
Здесь тип-параметр f просто заменён частично применённым типом Barry c d. Третий параметр типа Barry должен измениться, и мы видим, что это удобно сделать таким образом:
instance Functor (Barry a b) where
fmap f (Barry {yabba = x, dabba = y}) = Barry {yabba = f x, dabba = y}
Готово! Мы просто отобразили тип f по первому полю.
В данной главе мы хорошенько изучили, как работают параметры типов, и как они формализуются с помощью сортов по аналогии с тем, как формализуются параметры функций с помощью декларации типов. Мы провели любопытные параллели между функциями и конструкторами типов, хотя на первый взгляд они и не имеют ничего общего. При реальной работе с языком Haskell обычно не приходится возиться с сортами и делать вывод сортов вручную, как мы делали в этой главе. Обычно вы просто частично применяете свой тип к сорту * –> * или * при создании экземпляра от одного из стандартных классов типов, но полезно знать, как это работает на самом деле. Также интересно, что у типов есть свои собственные маленькие типы.
Ещё раз повторю: вы не должны понимать всё, что мы сделали, в деталях, но если вы по крайней мере понимаете, как работают сор та, есть надежда на то, что вы постигли суть системы типов языка Haskell.
8
Ввод-вывод
Разделение «чистого» и «нечистого»
В этой главе вы узнаете, как вводить данные с клавиатуры и печатать их на экран. Начнём мы с основ ввода-вывода:
• Что такое действия?
• Как действия позволяют выполнять ввод-вывод?
• Когда фактически исполняются действия?
Вводу-выводу приходится иметь дело с некоторыми ограничениями функций языка Haskell, поэтому первым делом мы обсудим, что с этим можно сделать.
Мы уже упоминали, что Haskell – чисто функциональный язык. В то время как в императивных языках вы указываете компьютеру серию шагов для достижения некой цели, в функциональном программировании мы описываем, чем является то или иное понятие. В языке Haskell функция не может изменить некоторое состояние, например поменять значение переменной (если функция изменяет состояние, мы говорим, что она имеет побочные эффекты). Единственное, что могут сделать функции в языке Haskell, – это вернуть нам некоторый результат, основываясь на переданных им параметрах. Если вызвать функцию дважды с одинаковыми параметрами, она всегда вернёт одинаковый результат. Если вы знакомы с императивными языками, может показаться, что это ограничивает свободу наших действий, но мы видели, что на самом деле это даёт весьма мощные возможности. В императивном языке у вас нет гарантии, что простая функция, которая всего-то навсего должна обсчитать пару чисел, не сожжёт ваш дом, не похитит собаку и не поцарапает машину во время вычислений! Например, когда мы создавали бинарное поисковое дерево, то вставляли элемент в дерево не путём модификации дерева в точке вставки. Наша функция добавления нового элемента в дерево возвращала новое дерево, так как не могла изменить старое.
Конечно, это хорошо, что функции не могут изменять состояние: это помогает нам строить умозаключения о наших программах. Но есть одна проблема. Если функция не может ничего изменить, как она сообщит нам о результатах вычислений? Для того чтобы вывести результат, она должна изменить состояние устройства вывода – обычно это экран, который излучает фотоны; они путешествуют к нашему мозгу и изменяют состояние нашего сознания… вот так-то, чувак!
Но не надо отчаиваться, не всё ещё потеряно. Оказывается, в языке Haskell есть весьма умная система для работы с функциями с побочными эффектами, которая чётко разделяет чисто функциональную и «грязную» части нашей программы. «Грязная» часть выполняет всю грязную работу, например отвечает за взаимодействие с клавиатурой и экраном. Разделив «чистую» и «грязную»части, мы можем так же свободно рассуждать о чисто функциональной части нашей программы, получать все преимущества функциональной чистоты, а именно – ленивость, гибкость, модульность, и при этом эффективно взаимодействовать с внешним миром.
Привет, мир!
До сих пор для того, чтобы протестировать наши функции, мы загружали их в интерпретатор GHCi. Там же мы изучали функции из стандартной библиотеки. Но теперь, спустя семь глав, мы наконец-то собираемся написать первую программу на языке Haskell! Ура! И, конечно же, это будет старый добрый шедевр «Привет, мир».
Итак, для начинающих: наберите в вашем любимом текстовом редакторе строку
main = putStrLn "Привет, мир"
Мы только что определили имя main; в нём мы вызываем функцию putStrLn с параметром "Привет, мир". На первый взгляд, ничего необычного, но это не так: мы убедимся в этом через несколько минут. Сохраните файл как helloworld.hs.
Сейчас мы собираемся сделать то, чего ещё не пробовали делать. Мы собираемся скомпилировать нашу программу! Я даже разволновался!.. Откройте ваш терминал, перейдите в папку с сохранённым файлом helloworld.hs и выполните следующую команду:
$ ghc helloworld
[1 of 1] Compiling Main ( helloworld.hs, helloworld.o )
Linking helloworld …
О’кей! При некотором везении вы получите нечто похожее и теперь можете запустить свою программу, вызвав ./helloworld.
$ ./helloworld
Привет, мир
ПРИМЕЧАНИЕ. Если вы используете Windows, то вместо выполнения команды ./helloworld просто запустите файл helloworld.exe.
Ну вот и наша первая программа, которая печатает что-то на терминале! Банально до невероятности!
Давайте изучим более подробно, что же мы написали. Сначала посмотрим на тип функции putStrLn:
ghci> :t putStrLn
putStrLn :: String -> IO ()
ghci> :t putStrLn "Привет, мир"
putStrLn "Привет, мир" :: IO ()
Тип putStrLn можно прочесть таким образом: putStrLn принимает строку и возвращает действие ввода-вывода (I/O action) с результирующим типом () (это пустой кортеж). Действие ввода-вывода – это нечто вызывающее побочные эффекты при выполнении (обычно чтение входных данных или печать на экране); также действие может возвращать некоторые значения. Печать строки на экране не имеет какого-либо значимого результата, поэтому возвращается значение ().
ПРИМЕЧАНИЕ. Пустой кортеж имеет значение (), его тип – также ().
Когда будет выполнено действие ввода-вывода? Вот для чего нужна функция main. Операции ввода-вывода выполняются, если мы поместим их в функцию main и запустим нашу программу.
Объединение действий ввода-вывода
Возможность поместить в программу всего один оператор ввода-вывода не очень-то вдохновляет. Но мы можем использовать ключевое слово do для того, чтобы «склеить» несколько операторов ввода-вывода в один. Рассмотрим пример:
main = do
putStrLn "Привет, как тебя зовут?"
name <– getLine
putStrLn ("Привет, " ++ name ++ ", ну ты и хипстота!")
О, новый синтаксис!.. И он похож на синтаксис императивных языков. Если откомпилировать и запустить эту программу, она будет работать так, как вы и предполагаете. Обратите внимание: мы записали ключевое слово do и затем последовательность шагов, как сделали бы в императивном языке. Каждый из этих шагов – действие ввода-вывода. Расположив их рядом с помощью ключевого слова do, мы свели их в одно действие ввода-вывода. Получившееся действие имеет тип IO(); это тип последнего оператора в цепочке.
По этой причине функция main всегда имеет тип main :: IO <нечто>, где <нечто> – некоторый конкретный тип. По общепринятому соглашению обычно не пишут декларацию типа для функции main.
В третьей строке можно видеть ещё один не встречавшийся нам ранее элемент синтаксиса, name <– getLine. Создаётся впечатление, будто считанная со стандартного входа строка сохраняется в переменной с именем name. Так ли это на самом деле? Давайте посмотрим на тип getLine.
ghci> :t getLine
getLine :: IO String
Ага!.. Функция getLine – действие ввода-вывода, которое содержит результирующий тип – строку. Это понятно: действие ждёт, пока пользователь не введёт что-нибудь с терминала, и затем это нечто будет представлено как строка. Что тогда делает выражение name <– getLine? Можно прочитать его так: «выполнить действие getLine и затем связать результат выполнения с именем name». Функция getLine имеет тип IO String, поэтому образец name будет иметь тип String. Можно представить действие ввода-вывода в виде ящика с ножками, который ходит в реальный мир, что-то в нём делает (рисует граффити на стене, например) и иногда приносит обратно какие-либо данные. Если ящик что-либо принёс, единственный способ открыть его и извлечь данные – использовать конструкцию с символом <–. Получить данные из действия ввода-вывода можно только внутри другого действия ввода-вывода. Таким образом, язык Haskell чётко разделяет чистую и «грязную» части кода. Функция getLine – не чистая функция, потому что её результат может быть неодинаковым при последовательных вызовах. Вот почему она как бы «запачкана» конструктором типов IO, и мы можем получить данные только внутри действий ввода-вывода, имеющих в сигнатуре типа маркёр IO. Так как код для ввода-вывода также «испачкан», любое вычисление, зависящее от «испачканных» IO-данных, также будет давать «грязный»результат.
Откройте для себя мир чтения на siteknig.com - месте, где каждая книга оживает прямо в браузере. Здесь вас уже ждёт произведение Миран Липовача - Изучай Haskell во имя добра!, относящееся к жанру Программирование. Никаких регистраций, никаких преград - только вы и история, доступная в полном формате. Наш литературный портал создан для тех, кто любит комфорт: хотите читать с телефона - пожалуйста; предпочитаете ноутбук - идеально! Все книги открываются моментально и представлены полностью, без сокращений и скрытых страниц. Каталог жанров поможет вам быстро найти что-то по настроению: увлекательный роман, динамичное фэнтези, глубокую классику или лёгкое чтение перед сном. Мы ежедневно расширяем библиотеку, добавляя новые произведения, чтобы вам всегда было что открыть "на потом". Сегодня на siteknig.com доступно более 200000 книг - и каждая готова стать вашей новой любимой. Просто выбирайте, открывайте и наслаждайтесь чтением там, где вам удобно.

