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 (), ведь мы хотим читать аргументы из командной строки и выводить на печать результаты, используя (), или пустое значение, в конце концов.

Есть два способа создать действие ввода/вывода (либо напрямую, либо вызывая функцию, которая сделает это):

  1. Передать обычное значение в монаду IO, используя функцию return.
  2. Объединить два существующих действия ввода/вывода.

Так как мы хотим сделать две вещи, мы выберем второй подход. Предопределенное действие getArgs читает аргументы командной строки и передает их далее, как список строк. Предопределенная функция putStrLn принимает строку и создает действие, которое выводит эту строку на консоль.

Чтобы объединить эти действия, мы используем конструкцию do-block. do-block состоит из нескольких строк, выровненных по первому значащему символу, после ключевого слова do. Каждая строка может иметь одну из двух форм:

  1. имя <- действие1
  2. действие2

Первая форма связывает результат действие1 с имя, чтобы он стал доступен в следующих действиях. Например, если действие1 имеет тип IO [String] (действие ввода/вывода, возвращающее список строк, как getArgs), то имя будет связано во всех последующих действиях со списком строк, который будет передан через использование "связующего" оператора >>= . Вторая форма просто выполняет действие2, переходя к следующей строке (она обязательно должна существовать) через оператор >> operator. Оператор связывания имеет разную семантику в разных монадах: в случае монады IO, он выполняет действие последовательно, производя разного рода побочные эффекты, как результаты действий. Поскольку семантика композиции действий зависит от текущей используемой монады, вы не можете смешивать действия из монад разных типов в одном do-block - может быть использована только монада IO (Это очень похоже на "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.

Упражнения
  1. Измените программу так, чтобы она читала два аргумента из командной строки выводила сообщение, используя оба из них
  2. Измените программу так, чтобы она выполняла простую арифметическую операцию с двумя аргументами и выводила результат. Можете использовать read, чтобы преобразовать значения из строки в число, и show, чтобы преобразовать число обратно в строку. Попробуйте поиграть с разными арифметическими операциями.
  3. getLine это действие ввода/вывода, которое читает строку с консоли и возвращает ее в виде строки. Измените программу так, чтобы запрашивала имя, читала введенное значение, а затем выводила его вместо переданных параметров