Write Yourself a Scheme in 48 Hours/Первые шаги
Для начала, вам нужно установить GHC. Под Linux он скорее всего уже установлен, если это не так, то его можно установить с помощью системы пакетов вашего дистрибутива. Скорее всего, проще будет установить бинарный пакет, кроме тех случаев, когда вы действительно знаете, что делаете. Его можно скачать и установить также, как и любой другой пакет. Этот учебник был написан на Linux, но всё должно работать и под Windows, при условии, что вы умеете пользоваться командной строкой.
Для пользователей UNIX (или Windows Emacs) существует отличный режим для Emacs, включающий в себя подсветку синтаксиса и автоматическое выравнивание. Пользователи Windows могут использовать Блокнот (Notepad) или любой другой текстовый редактор, но вам нужно быть осторожнее с отступами. Пользователи Eclipse, возможно, захотят попробовать Function Programming плагин. Наконец, существует и Haskell-плагин для Visual Studio, который использует компилятор GHC.
Теперь настало время написать вашу первую программу на Haskell. Эта программа читает имя с командной строки и выводит на экран приветствие. Создайте файл, который оканчивается на '.hs', и наберите в нём следующий текст. Убедитесь, что вы правильно расставили отступы, иначе программа может не компилироваться.
module Main where
import System.Environment
main :: IO ()
main = do
args <- getArgs
putStrLn ("Hello, " ++ args !! 0)
Теперь давайте рассмотрим, что мы написали. Первые две строки означают, что мы создаём модуль с именем Main, который импортирует модуль System. Каждая программа на Haskell начинается с действия, которое называется main
, определенного в модуле Main. Этот модуль может импортировать другие модули, но он обязательно должен присутствовать, чтобы компилятор смог создать исполяемый файл. Haskell чувствителен к регистру символов: название модуля всегда начинается с большой буквы, определения всегда с маленькой.
Строка main :: IO ()
содержит объявление типа, в ней говорится, что действие main
имеет тип IO ()
. В Haskell тип указывать не обязательно: компилятор выведет его автоматически, и выдаст ошибку, только если его тип будет отличаться от заданного вами. Для ясности я буду всегда объявлять тип.
Тип IO в некотором роде пример того, что называется монада, за страшным названием которой скрывается не такое уж страшное понятие. По сути, монада - это способ сказать "мы будем определенным образом "тянуть" за собой и комбинировать некую дополнительную информацию, которая "прикреплена" к необходимым нам данным, и которая не нужна большинству функций". Каким именно образом мы "тянем" за собой эту дополнительную информацию и комбинируем наши значения, определяется каждым конкретным монадическим типом; "основные" же данные могут изменяться или конвертироваться из одного типа в другой при помощи обычных функций, которые мы вызываем из монады, не затрагивая при этом дополнительную информацию, "прикрепленную" к ним. При этом принцип монадического "конвейера", передающего данные с предыдущего шага на последующий шаг, во всех монадах одинаков.
В нашем примере, "дополнительная информация" это действия ввода/вывода, которые должны быть выполнены с использованием переданных значений, а также пустое значение, которое в коде представлено как ()
. И IO [String]
, и IO ()
относятся к одному типу монады IO
с разными базовыми типами, что означает, что они представляют собой действия ввода/вывода, оперирующие значениями разных типов: [String]
и ()
. Подобные монадные значения, созданные из базовых типов, упакованных внутрь монады, часто называют "действиями", потому что простейший способ представить себе монаду IO - думать о ней, как о последовательности действий, каждое из которых может оперировать с переданными значениями базовых типов, взаимодействуя с внешним миром.
Haskell - это декларативный язык: вместо указания компьютеру последовательности инструкций для выполнения, вы даете ему набор определений, как нужно выполнять каждую из функций, которые могут ему понадобиться. Эти определения состоят из различных комбинации действий и функций. Компилятор вычисляет, в каком порядке следует выполнять функции, чтобы получить конечный результат.
Чтобы написать одно из таких определений, вы просто задаете его в виде уравнений. Левая часть определяет имя и, возможно, один или несколько образцов (объяснено далее), которые связываются с переменными. Правая часть определяет некоторую комбинацию из других определений, которые говорят компьютеру, что делать, когда он встречает их имя в определении. Эти уравнения внешне очень похожи на обычные уравнения из алгебры: вы всегда можете подставить правую часть вместо левой в тексте программы и результат выполнения будет точно таким же. Называемое "ссылочной прозрачностью", это свойство делает чтение и понимание программ на Haskell значительно легче, чем на других языках.
Как мы определяем действие main
? Мы знаем, что оно должно быть действием IO ()
, ведь мы хотим читать аргументы из командной строки и выводить на печать результаты, используя ()
, или пустое значение, в конце концов.
Есть два способа создать действие ввода/вывода (либо напрямую, либо вызывая функцию, которая сделает это):
- Передать обычное значение в монаду
IO
, используя функциюreturn
. - Объединить два существующих действия ввода/вывода.
Так как мы хотим сделать две вещи, мы выберем второй подход. Предопределенное действие getArgs читает аргументы командной строки и передает их далее, как список строк. Предопределенная функция putStrLn принимает строку и создает действие, которое выводит эту строку на консоль.
Чтобы объединить эти действия, мы используем конструкцию do-block
. do-block
состоит из нескольких строк, выровненных по первому значащему символу, после ключевого слова do. Каждая строка может иметь одну из двух форм:
- имя <- действие1
- действие2
Первая форма связывает результат действие1 с имя, чтобы он стал доступен в следующих действиях. Например, если действие1 имеет тип IO [String]
(действие ввода/вывода, возвращающее список строк, как getArgs
), то имя будет связано во всех последующих действиях со списком строк, который будет передан через использование "связующего" оператора >>=
. Вторая форма просто выполняет действие2, переходя к следующей строке (она обязательно должна существовать) через оператор >>
operator. Оператор связывания имеет разную семантику в разных монадах: в случае монады IO
, он выполняет действие последовательно, производя разного рода побочные эффекты, как результаты действий. Поскольку семантика композиции действий зависит от текущей используемой монады, вы не можете смешивать действия из монад разных типов в одном do-block
- может быть использована только монада IO
(Это очень похоже на "pipe").
Внимание! Перевод выражения (it's all in the same "pipe") требует уточнения |
Конечно, эти действия могут могут сами вызывать функции или сложные выражения, передавая результаты их вычисления(либо через вызов функции return
, либо некоторой функции, которая в последствии сделает то же самое). В наше примере, мы сначала берем первый элемент из списка аргументов (с индексом 0, args !! 0
), прилепляем его в конец строки "Hello, " ("Hello, " ++
), и, наконец, передаем результат putStrLn
, которая создает новое действие ввода/вывода, следующей в do-block.
Новое только что созданное действие, представляющее собой комбинацию последовательных действий, как описано выше, сохранено под именем main
с типом IO ()
. Система Haskell находит это определение и выполняет действие в нем.
Строки представлены в Haskell списком символов, так что вы можете применять к ним любые функции для работы со списками. Полная таблица стандартных операторов и их порядка:
Оператор(ы) | Порядок | Ассоциативность | Описание |
---|---|---|---|
. | 9 | Правая | Композиция функций |
!! | 9 | Левая | Взятие индекса в списке |
^, ^^, ** | 8 | Правая | Возведение в степень (целое, дробное, и действительное число) |
*, / | 7 | Левая | Умножение, Деление |
+, - | 6 | Левая | Сложение, Вычитание |
: | 5 | Правая | Cons (конструктор списков) |
++ | 5 | Правая | Склеивание списков |
`elem`, `notElem` | 4 | Левая | List Membership |
==, /=, <, <=, >=,> | 4 | Левая | Проверки на равенство, не равенство, и другие операции сравнения |
&& | 3 | Правая | Логическое И |
|| | 2 | Правая | Логическое ИЛИ |
>>, >>= | 1 | Левая | Монадное связывание, Монадное связывание (с передачей результата в следующую функцию) |
=<< | 1 | Правая | Обратное монадное связывание (аналогично предыдущему, но аргументы в обратном порядке) |
$ | 0 | Правая | Инфиксное применение функции (аналогично "f x", но правоассоциативно, а не левоассоциативно, как обычно) |
Чтобы скомпилировать и запустить программу, введите примерно такие команды:
debian:/home/jdtang/haskell_tutorial/code# ghc -o hello_you listing2.hs debian:/home/jdtang/haskell_tutorial/code# ./hello_you Jonathan Hello, Jonathan
Опция -o
указывает имя исполняемого файла, который получается после компиляции, а дальше вы просто указываете имя файла с исходным текстом Haskell.
Упражнения |
---|
|