Первая программа

Я вот говорю-говорю, а вдруг я вас обманываю, и ничего этого нет. В этой главе мы перейдём к программированию и запустим нашу первую программу в Haskell. Будет много примеров, на которых мы закрепим наши знания.

Интерпретатор

Для запуска кода мы будем пользоваться приложением GHC (Glorious Glasgow Haskell Compiler) наиболее развитой системой интерпретации Haskell программ. В GHC есть компилятор ghc и интерпретатор ghci. Пока мы будем пользоваться лишь интерпретатором. Если вы не знаете как установить ghc загляните в приложение. Также нам понадобится текстовый редактор с подсветкой синтаксиса. Подсветка синтаксиса для Haskell по умолчанию есть в редакторах Vim, Emacs, gedit, geany, yi. Есть IDE для Haskell Leksah. Мы будем писать модули в файлах и загружать их в интерпретатор. Если вы не знаете продвинутых текстовых редакторов вроде Vim или Emacs, лучше всего будет начать с gedit.

Интерпретатор позволяет загружать модуль с определениями и набирать значения в командной строке. Мы набираем значение, а интерпретатор редуцирует его и показывает нам ответ. Интерпретатор запускается командой ghci в терминале. Определения из модуля могут быть загружены в интерпретатор двумя способами, либо при запуске интерпретатора командой ghci ИмяМодуля.hs либо в самом интерпретаторе командой :l ИмяМодуля.hs.

Рассмотрим некоторые полезные команды интерпретатора:

:?

Выводит на экран список доступных команд

:t Expression

Возвращает тип выражения.

:set +t

После выполнения команды интерпретатор будет выводить на экран не только результат вычисления выражения, но и его тип.

:set +s

После выполнения команды интерпретатор будет выводить на экран не только результат вычисления выражения, но и статистику вычислений.

:l ИмяМодуля

Загружает модуль в интерпретатор.

:cd Директория

Перейти в данную директорию.

:r

Перезагружает, последний загруженный модуль. Этой командой можно пользоваться после внесения в модуль изменений.

:q

Выход из интерпретатора.

У-вей

Согласно даосам основной принцип жизни заключается в недеянии (у-вей). Всё происходит естественно и словно само собой. Давайте создадим модуль который ничего не делает. Создадим пустой модуль и загрузим его в интерпретатор.

module Empty where

import Prelude()

Зачем мы написали import Prelude()? Этой фразой мы говорим, что не хотим ничего импортировать из модуля Prelude. По умолчанию в любой модуль загружается модуль Prelude, который содержит много полезных определений. К примеру там определяется тип Bool, списки и функции для них, символы, классы типов для сравнения на равенство и печати значений и много, много других определений. В первых главах я хочу сделать акцент на самом языке Haskell, а не на производных выражениях, поэтому пока мы будем в явном виде загружать из модуля Prelude лишь самые необходимые определения.

Сохраним модуль в файле Empty.hs, сделаем директорию модуля текущей и запустим интерпретатор командой ghci Empty (имя расширения можно не писать). Также можно просто запустить интерпретатор командой ghci, переключиться на директорию командой :cd и загрузить модуль командой :l Empty.

$ ghci
GHCi, version 7.4.1: http://www.haskell.org/ghc/  :? for help
Loading package ghc-prim ... linking ... done.
Loading package integer-gmp ... linking ... done.
Loading package base ... linking ... done.
Prelude> :cd ~/haskell-notes/code/ch-2/
Prelude> :l Empty.hs 
[1 of 1] Compiling Empty            ( Empty.hs, interpreted )
Ok, modules loaded: Empty.
*Empty> 

Слева от знака приглашения к вводу > отображаются загруженные в интерпретатор модули. По умолчанию загружается модуль Prelude. После выполнения команды :l мы видим, что Prelude сменилось на Empty.

Теперь давайте потренируемся перезагружать модули. Давайте изменим наш модуль, сделаем его не таким пустым, убрав последние две скобки от модуля Prelude в директиве import. Теперь сохраним изменения и выполним команду :r.

*Empty> :r
[1 of 1] Compiling Empty            ( Empty.hs, interpreted )
Ok, modules loaded: Empty.
*Empty>

Завершим сессию интерпретатора командой :q.

*Empty> :q
Leaving GHCi.

Внешние модули должны находится в текущей директории. Давайте потренируемся с подключением определений из внешних модулей. Создадим модуль близнец модуля Empty.hs:

module EmptyEmpty where

import Prelude()

И сохраним его в той же директории, что и модуль Empty, теперь мы можем включить все определения из модуля EmptyEmpty:

module Empty where

import EmptyEmpty

Когда у нас будет много модулей мы можем разместить их по директориям. Создадим в одной директории с модулем Empty директорию Sub, а в неё поместим копию модуля Empty. Существует одна тонкость: поскольку модуль находится в поддиректории, для того чтобы он стал виден из текущей директории, необходимо дописать через точку имя директории в которой он находится:

module Sub.Empty where

Теперь мы можем загрузить этот модуль из исходного:

module Empty where

import EmptyEmpty
import Sub.Empty

Обратите внимание на то, что мы приписываем к модулю в поддиректории Sub имя поддиректории. Если бы он был заложен в ещё одной директории, то мы написали бы через точку имя и этой поддиректории:

module Empty where

import Sub1.Sub2.Sub3.Sub4.Empty

Логические значения

Пустой модуль это хорошо, но слишком скучно. Давайте перепишем объявленные в этой главе определения в модуль, загрузим его в интерпретатор и понабираем значения.

Начнём с логических операций. Давайте не будем переопределять Bool, Show и Eq, а просто возьмём их из Prelude:

module Logic where

import Prelude(Bool(..), Show(..), Eq(..))

Две точки в скобках означают “все конструкторы” (в случае типа) и “все методы” (в случае класса типа). Строчку

import Prelude(Bool(..), Show(..), Eq(..))

Следует читать так: Импортируй из модуля Prelude тип Bool и все его конструкторы и классы Show и Eq со всеми их методами. Если бы мы захотели импортировать только конструктор True, мы бы написали Bool(True), а если бы мы захотели импортировать лишь имя типа, мы бы написали просто Bool без скобок.

Сначала выпишем в модуль наши синонимы:

module Logic where

import Prelude(Bool(..), Show(..), Eq(..))
  
true :: Bool 
true = True

false :: Bool
false = False

not :: Bool -> Bool
not True  = False
not False = True

and :: Bool -> Bool -> Bool
and False  _  = False
and True   x  = x

or  :: Bool -> Bool -> Bool
or True   _ = True
or False  x = x

xor :: Bool -> Bool -> Bool
xor a b = or (and (not a) b) (and a (not b))

ifThenElse :: Bool -> a -> a -> a
ifThenElse True   t  _ = t
ifThenElse False  _  e = e

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

*Logic> :l Logic
[1 of 1] Compiling Logic            ( Logic.hs, interpreted )
Ok, modules loaded: Logic.
*Logic> :set +t
*Logic> not (and true False)
True
it :: Bool
*Logic> or (and true true) (or False False)
True
it :: Bool
*Logic> xor (not True) (False)
False
it :: Bool
*Logic> ifThenElse (or true false) True False
True
it :: Bool

Разумеется в Haskell уже определены логические операции, здесь мы просто тренировались. Они называются not, (&&), ||. Операция xor это то же самое, что и (/=). Для Bool определён экземпляр класса Eq. Также в Haskell есть конструкция ветвления она пишется так:

x = if cond then t else e

Слова if, then и else – ключевые. cond имеет тип Bool, а t и e одинаковый тип.

В коде программы обычно пишут так:

x = if a > 3 
    then "Hello"
    else (if a < 0
          then "Hello"
          else "Bye")

Отступы обязательны.

Давайте загрузим в интерпретатор модуль Prelude и наберём те же выражения стандартными функциями:

*Logic> :m Prelude
Prelude> not (True && False)
True
it :: Bool
Prelude> (True && True) || (False || False)
True
it :: Bool
Prelude> not True /= False
False
it :: Bool
Prelude> if (True || False) then True else False
True
it :: Bool

Бинарные операции с символьными именами пишутся в инфиксной форме, то есть между аргументами как в a && b или a + b. Значение с буквенным именем также можно писать в инфиксной форме, для этого оно заключается в апострофы, например a `and` b или a `plus` b. Апострофы обычно находятся на одной кнопке с буквой “ё”. Также символьные функции можно применять в префиксной форме, заключив их в скобки, например (&&) a b и (+) a b. Попробуем в интерпретаторе:

Prelude> True && False
False
it :: Integer
Prelude> (&&) True False
False
it :: Bool
Prelude> let and a b = a && b
and :: Bool -> Bool -> Bool
Prelude> and True False
False
it :: Bool
Prelude> True `and` False
False
it :: Bool

Обратите внимание на строчку let and a b = a && b. В ней мы определили синоним в интерпретаторе. Сначала мы пишем ключевое слово let затем обычное определение синонима, как в программе. Это простое однострочное определение, но мы можем набирать в интерпретаторе и более сложные. Мы можем написать несколько строчек в одной, разделив их точкой с запятой:

Prelude> let not2 True = False; not2 False = True

Мы можем записать это определение более наглядно, совсем как в редакторе, если воспользуемся многострочным вводом. Для этого просто наберите команду :{. Для выхода воспользуйтесь командой :}. Отметим, что точкой с запятой можно пользоваться и в обычном коде. Например в том случае если у нас много кратких определений и мы хотим записать их покомпактней, мы можем сделать это так:

a1 = 1;   a2 = 2;   a3 = 3
a4 = 4;   a5 = 5;   a6 = 6

Класс Show. Строки и символы

Мы набираем в интерпретаторе какое-нибудь сложное выражение, или составной синоним, интерпретатор проводит редукцию и выводит ответ на экран. Откуда интерпретатор знает как отображать значения типа Bool? Внутри интерпретатора вызывается метод класса Show, который переводит значение в строку. И затем мы видим на экране ответ.
Для типа Bool экземпляр класса Show уже определён, поэтому интерпретатор знает как его отображать.

Обратите внимание на эту особенность языка, вид значения определяется пользователем, в экземпляре класса Show. Из соображений наглядности вид значения может сильно отличаться от его внутреннего представления.

В этом разделе мы рассмотрим несколько примеров с классом Show, но перед этим мы поговорим о строках и символах в языке Haskell.

Строки и символы

Посмотрим в интерпретаторе на определение строк (тип String), для этого мы воспользуемся командой :i (сокращение от :info):

Prelude> :i String
type String = [Char] 	-- Defined in `GHC.Base'

Интерпретатор показал определение типа и в комментариях указал в каком модуле тип определён. В этом определении мы видим новое ключевое слово type. До этого для определения типов нам встречалось лишь слово data. Ключевое слово type определяет синоним типа. При этом мы не вводим новый тип, мы лишь определяем для него псевдоним. String является синонимом для списка значений типа Char. Тип Char представляет символы. Итак строка – это список символов. В Haskell символы пишутся в ординарных кавычках, а строки в двойных:

Prelude> ['H','e','l','l','o']
"Hello"
it :: [Char]
Prelude> "Hello"
"Hello"
it :: [Char]
Prelude> '+'
'+'
it :: Char

Для обозначения перехода на новую строку используется специальный символ \n. Если строка слишком длинная и не помещается на одной строке, то её можно перенести так:

str = "My long long long long \
        \long long string"

Перенос осуществляется с помощью комбинации следующих друг за другом обратных слэшей.

Нам понадобится функция конкатенации списков (++), она определена в Prelude, с её помощью мы будем объединять строки:

Prelude> :t (++)
(++) :: [a] -> [a] -> [a]
Prelude> "Hello" ++ [' '] ++ "World"
"Hello World"
it :: [Char]

Пример: Отображение дат и времени

Приведём, пример в котором отображаемое значение не совпадает с видом значения в коде. Мы отобразим значения из мира календаря. Для начала давайте сохраним определения в отдельном модуле:

module Calendar where

import Prelude (Int, Char, String, Show(..), (++))

-- Дата
data Date = Date Year Month Day

-- Год
data Year  = Year Int       -- Int это целые числа

-- Месяц
data Month  = January    | February   | March    | April          
            | May        | June       | July     | August   
            | September  | October    | November | December

data Day = Day Int

-- Неделя
data Week  = Monday     | Tuesday   | Wednesday 
           | Thursday   | Friday    | Saturday     
           | Sunday   

-- Время
data Time = Time Hour Minute Second

data Hour   = Hour   Int    -- Час
data Minute = Minute Int    -- Минута
data Second = Second Int    -- Секунда

Теперь сохраним наш модуль под именем Calendar.hs и загрузим в интерпретатор:

Prelude> :l Calendar
[1 of 1] Compiling Calendar         ( Calendar.hs, interpreted )
Ok, modules loaded: Calendar.
*Calendar> Monday

<interactive>:3:1:
    No instance for (Show Week)
      arising from a use of `System.IO.print'
    Possible fix: add an instance declaration for (Show Week)
    In a stmt of an interactive GHCi command: System.IO.print it

Смотрите мы попытались распечатать значение Monday, но в ответ получили ошибку. В ней интерпретатор сообщает нам о том, что для типа Week не определён экземпляр класса Show, и он не знает как его распечатывать. Давайте подскажем ему. Обычно дни недели в календарях печатают не полностью, в имя попадают лишь три первых буквы:

instance Show Week where
    show Monday     = "Mon"
    show Tuesday    = "Tue"
    show Wednesday  = "Wed"
    show Thursday   = "Thu"  
    show Friday     = "Fri" 
    show Saturday   = "Sat"
    show Sunday     = "Sun"

Отступы перед show обязательны, но выравнивание по знаку равно не обязательно, мне просто нравится так писать. По отступам компилятор понимает, что все определения относятся к определению instance. Теперь запишем экземпляр в модуль, сохраним, и перезагрузим в интерпретатор:

*Calendar> :r
[1 of 1] Compiling Calendar         ( Calendar.hs, interpreted )
Ok, modules loaded: Calendar.
*Calendar> Monday
Mon
it :: Week
*Calendar> Sunday
Sun
it :: Week

Теперь наши дни отображаются. Я выпишу ещё один пример экземпляра для Time, а остальные достанутся вам в качестве упражнения.

instance Show Time where
    show (Time h m s) = show h ++ ":" ++ show m ++ ":" ++ show s

instance Show Hour where
    show (Hour h) = addZero (show h)

instance Show Minute where
    show (Minute m) = addZero (show m)

instance Show Second where
    show (Second s) = addZero (show s)

addZero :: String -> String
addZero (a:[]) = '0' : a : []
addZero as     = as

Функцией addZero мы добавляем ноль в начало строки, в том случае, если число однозначное, также в этом определении мы воспользовались тем, что для типа целых чисел Int экземпляр Show уже определён. Проверим в интерпретаторе:

*Calendar> Time (Hour 13) (Minute 25) (Second 2)
13:25:02
it :: Time

Автоматический вывод экземпляров классов типов

Для некоторых стандартных классов экземпляры классов типов могут быть выведены автоматически. Это делается с помощью директивы deriving. Она пишется сразу после объявления типа. Например так мы можем определить тип и экземпляры для классов Show и Eq:

data T = A | B | C
    deriving (Show, Eq)

Отступ за deriving обязателен, после ключевого слова в скобках указываются классы, которые мы хотим вывести.

Арифметика

В этом разделе мы обсудим основные арифметические операции. В Haskell много стандартных классов, которые группируют различные типы операций, есть класс для сравнения на равенство, отдельный класс для сравнения на больше/меньше, класс для умножения, класс для деления, класс для упорядоченных чисел, и много других. Зачем такое изобилие классов?

Каждый из классов отвечает независимой группе операций. Есть много объектов, которые можно только складывать, но нельзя умножать или делить. Есть объекты, для которых сравнение на равенство имеет смысл, а сравнение на больше/меньше – нет.

Для иллюстрации мы воспользуемся числами Пеано, у них компактное определение, всего два конструктора, которых тем не менее достаточно для описания множества натуральных чисел:

module Nat where

data Nat = Zero | Succ Nat
    deriving (Show, Eq, Ord)

Конструктор Zero указывает на число ноль, а (Succ n) на число следующее за данным числом n. В последней строчке мы видим новый класс Ord, этот класс содержит операции сравнения на больше/меньше:

Prelude> :i Ord
class (Eq a) => Ord a where
  compare :: a -> a -> Ordering
  (<) :: a -> a -> Bool
  (>=) :: a -> a -> Bool
  (>) :: a -> a -> Bool
  (<=) :: a -> a -> Bool
  max :: a -> a -> a
  min :: a -> a -> a

Тип Ordering кодирует результаты сравнения:

Prelude> :i Ordering
data Ordering = LT | EQ | GT 	-- Defined in GHC.Ordering

Он содержит конструкторы, соответствующие таким понятиям как меньше, равно и больше.

Класс Eq. Сравнение на равенство

Вспомним определение класса Eq:

class Eq a where
    (==) :: a -> a -> Bool
    (/=) :: a -> a -> Bool

    a == b = not (a /= b)
    a /= b = not (a == b)

Появились две детали, о которых я умолчал в предыдущей главе. Это две последние строчки. В них мы видим определение == через /= и наоборот. Это определения методов по умолчанию. Такие определения дают нам возможность определять не все методы класса, а лишь часть основных, а все остальные мы получим автоматически из определений по умолчанию.

Казалось бы почему не оставить в классе Eq один метод а другой метод определить в виде отдельной функции:

class Eq a where
    (==) :: a -> a -> Bool

(/=) :: Eq a => a -> a -> Bool
a /= b = not (a == b)

Так не делают по соображениям эффективности. Есть типы для которых проще вычислить /= чем ==. Тогда мы определим тот метод, который нам проще вычислять и второй получим автоматически.

Набор основных методов, через которые определены все остальные называют минимальным полным определением (minimal complete definition) класса. В случае класса Eq это метод == или метод /=.

Мы уже вывели экземпляр для Eq, поэтому мы можем пользоваться методами == и /= для значений типа Nat:

*Calendar> :l Nat
[1 of 1] Compiling Nat              ( Nat.hs, interpreted )
Ok, modules loaded: Nat.
*Nat> Zero == Succ (Succ Zero)
False
it :: Bool
*Nat> Zero /= Succ (Succ Zero)
True
it :: Bool

Класс Num. Сложение и умножение

Сложение и умножение определены в классе Num. Посмотрим на его определение:

*Nat> :i Num
class (Eq a, Show a) => Num a where
  (+) :: a -> a -> a
  (*) :: a -> a -> a
  (-) :: a -> a -> a
  negate :: a -> a
  abs :: a -> a
  signum :: a -> a
  fromInteger :: Integer -> a
  	-- Defined in GHC.Num

Методы (+), (*), (-) в представлении не нуждаются, метод negate является унарным минусом, его можно определить через (-) так:

negate x = 0 - x

Метод abs является модулем числа, а метод signum возвращает знак числа, метод fromInteger позволяет создавать значения данного типа из стандартных целых чисел Integer.

Этот класс устарел, было бы лучше сделать отдельный класс для сложения и вычитания и отдельный класс для умножения. Также контекст класса, часто становится помехой. Есть объекты, которые нет смысла печатать но, есть смысл определить на них сложение и умножение. Но пока в целях совместимости с уже написанным кодом, класс Num остаётся прежним.

Определим экземпляр для чисел Пеано, но давайте сначала разберём функции по частям.

Сложение

Начнём со сложения:

instance Num Nat where
    (+) a Zero     = a
    (+) a (Succ b) = Succ (a + b)

Первое уравнение говорит о том, что, если второй аргумент равен нулю, то мы вернём первый аргумент в качестве результата. Во втором уравнении мы “перекидываем” конструктор Succ из второго аргумента за пределы суммы. Схематически вычисление суммы можно представить так:

$$\textbf{3+2} \rightarrow 1+\textbf{(3+1)} \rightarrow 1+(1+\textbf{(3+0)})$$

$$1+(1+\textbf{3}) \rightarrow 1+(1+(1+(1+(1+0)))) \rightarrow 5$$

Все наши числа имеют вид $0$ или $1+n$, мы принимаем на вход два числа в таком виде и хотим в результате составить число в этом же виде, для этого мы последовательно перекидываем $(1+)$ в начало выражения из второго аргумента.

Вычитание

Операция отрицания не имеет смысла, поэтому мы воспользуемся специальной функцией error :: String -> a, она принимает строку с сообщением об ошибке, при её вычислении программа остановится с ошибкой и сообщение будет выведено на экран.

    negate _ = error "negate is undefined for Nat"

Умножение

Теперь посмотрим на умножение:

    (*) a Zero     = Zero
    (*) a (Succ b) = a + (a * b)     

В первом уравнении мы вернём ноль, если второй аргумент окажется нулём, а во втором мы за каждый конструктор Succ во втором аргументе прибавляем к результату первый аргумент. В итоге, после вычисления a * b мы получим аргумент a сложенный b раз. Это и есть умножение. При этом мы воспользовались операцией сложения, которую только что определили. Посмотрим на схему вычисления:

$$\textbf{3*2} \rightarrow 3+\textbf{(3*1)} \rightarrow 3+(3+\textbf{(3*0)}) \rightarrow 3+\textbf{(3+0)} \rightarrow \textbf{3+3} \rightarrow$$

$$1+\textbf{(3+2)} \rightarrow 1+(1+\textbf{(3+1)}) \rightarrow 1+(1+(1+\textbf{(3+0)})) \rightarrow$$

$$1+(1+1+\textbf{3}) \rightarrow 1+(1+(1+(1+(1+(1+0))))) \rightarrow 6$$

Операции abs и signum

Поскольку числа у нас положительные, то методы abs и signum почти ничего не делают:

    abs    x    = x 
    signum Zero = Zero
    signum _    = Succ Zero

Перегрузка чисел

Остался последний метод fromInteger. Он конструирует значение нашего типа из стандартного:

    fromInteger 0 = Zero
    fromInteger n = Succ (fromInteger (n-1))

Зачем он нужен? Попробуйте узнать тип числа 1 в интерпретаторе:

*Nat> :t 1
1 :: (Num t) => t

Интерпретатор говорит о том, тип значения 1 является некоторым типом из класса Num. В Haskell обозначения для чисел перегружены. Когда мы пишем 1 на самом деле мы пишем (fromInteger (1::Integer)). Поэтому теперь мы можем не писать цепочку Succ-ов, а воспользоваться методом fromInteger, для этого сохраним определение экземпляра для Num и загрузим обновлённый модуль в интерпретатор:

[1 of 1] Compiling Nat              ( Nat.hs, interpreted )
Ok, modules loaded: Nat.
*Nat> 7 :: Nat
Succ (Succ (Succ (Succ (Succ (Succ (Succ Zero))))))
*Nat> (2 + 2) :: Nat
Succ (Succ (Succ (Succ Zero)))
*Nat> 2 * 3 :: Nat
Succ (Succ (Succ (Succ (Succ (Succ Zero)))))

Вы можете убедиться насколько гибкими являются числа в Haskell:

*Nat> (1 + 1) :: Nat
Succ (Succ Zero)
*Nat> (1 + 1) :: Double
2.0
*Nat> 1 + 1
2

Мы выписали три одинаковых выражения и получили три разных результата, меняя объявление типов. В последнем выражении тип был приведён к Integer. Это поведение интерпретатора по умолчанию. Если мы напишем:

*Nat> let q = 1 + 1
*Nat> :t q
q :: Integer

Мы видим, что значение q было переведено в Integer, это происходит лишь в интерпретаторе, если такая переменная встретится в программе и компилятор не сможет определить её тип из контекста, произойдёт ошибка проверки типов, компилятор скажет, что он не смог определить тип. Помочь компилятору можно, добавив объявление типа с помощью конструкции (v :: T).

Посмотрим ещё раз на определение экземпляра Num для Nat целиком:

instance Num Nat where
    (+) a Zero     = a
    (+) a (Succ b) = Succ (a + b)

    (*) a Zero     = Zero
    (*) a (Succ b) = a + (a * b)   

    fromInteger 0 = Zero
    fromInteger n = Succ (fromInteger (n-1))

    abs    x    = x 
    signum Zero = Zero
    signum _    = Succ Zero

    negate _ = error "negate is undefined for Nat"

Класс Fractional. Деление

Деление определено в классе Fractional:

*Nat>:m Prelude
Prelude> :i Fractional
class Num a => Fractional a where
  (/) :: a -> a -> a
  recip :: a -> a
  fromRational :: Rational -> a
  	-- Defined in `GHC.Real'
instance Fractional Float -- Defined in `GHC.Float'
instance Fractional Double -- Defined in `GHC.Float'

Функция recip, это аналог negate для Num. Она делит единицу на данное число. Функция fromRational строит число данного типа из дробного числа. Если мы пишем 2, то к нему подспудно будет применена функция fromInteger, а если 2.0, то будет применена функция fromRational.

Стандартные числа

В этом подразделе мы рассмотрим несколько стандартных типов для чисел в Haskell. Все эти числа являются экземплярами основных численных классов. Тех, которые мы рассмотрели, и многих-многих других.

Целые числа

В Haskell предусмотрено два типа для целых чисел. Это Integer и Int. Чем они отличаются? Значения типа Integer не ограничены, мы можем проводить вычисления с очень-очень-очень большими числами, если памяти на нашем компьютере хватит. Числа из типа Int ограничены. Каждое число занимает определённый размер в памяти компьютера. Диапазон значений для Int составляет от $-2^{29}$ до $2^{29}-1$. Вычисления с Int более эффективны.

Действительные числа

Действительные числа бывают дробными (тип Rational), с ординарной точностью Float и с двойной точностью Double. Числа из типа Float занимают меньше места, но они не такие точные как Double. Если вы сомневаетесь, чем пользоваться, выбирайте Double, обычно Float используется только там, где необходимо хранить огромные массивы чисел. В этом случае мы экономим много памяти.

Преобразование численных типов

Во многих языках программирования при сложении или умножении чисел разных типов проводится автоматическое приведение типов. Обычно целые числа становятся действительными, Float превращается в Double и так далее. Это противоречит строгой типизации, поэтому в Haskell этого нет:

Prelude> (1::Int) + (1::Double)

<interactive>:2:13:
    Couldn't match expected type `Int' with actual type `Double'
    In the second argument of `(+)', namely `(1 :: Double)'
    In the expression: (1 :: Int) + (1 :: Double)
    In an equation for `it': it = (1 :: Int) + (1 :: Double)

Любое преобразование типов контролируется пользователем. Мы должны вызвать специальную функцию.

От целых к действительным: Часто возникает необходимость приведения целых чисел к действительным при делении. Для этого можно воспользоваться функцией: fromIntegral

Prelude> :i fromIntegral
fromIntegral :: (Integral a, Num b) => a -> b
  	-- Defined in `GHC.Real'

Определим функцию поиска среднего между двумя целыми числами:

meanInt :: Int -> Int -> Double
meanInt a b = fromIntegral (a + b) / 2

В этой функции двойка имеет тип Double. Обратите внимание на скобки: составной синоним всегда притягивает аргументы сильнее чем бинарная операция.

От действительных к целым: В этом нам поможет класс RealFrac. Методы говорят сами за себя:

Prelude GHC.Float> :i RealFrac
class (Real a, Fractional a) => RealFrac a where
  properFraction :: Integral b => a -> (b, a)
  truncate :: Integral b => a -> b
  round :: Integral b => a -> b
  ceiling :: Integral b => a -> b
  floor :: Integral b => a -> b
  	-- Defined in `GHC.Real'
instance RealFrac Float -- Defined in `GHC.Float'
instance RealFrac Double -- Defined in `GHC.Float'

Метод properFraction отделяет целую часть числа от дробной:

properFraction :: Integral b => a -> (b, a)

Для того, чтобы вернуть сразу два значения используется кортеж (кортежи пишутся в обычных скобках, значения следуют через запятую):

Prelude> properFraction 2.5
(2,0.5)

Для пар (кортеж, состоящий из двух элементов) определены две удобные функции извлечения элементов, их смысл можно понять по одним лишь типам:

fst :: (a, b) -> a
snd :: (a, b) -> b

Проверим:

Prelude> let x = properFraction 2.5
Prelude> (fst x, snd x)
(2, 0.5)

Мы бы и сами могли определить такие функции:

fst :: (a, b) -> a
fst (a, _) = a 

snd :: (a, b) -> b
snd (_, b) = b

Между действительными числами: Кто-то написал очень хорошую функцию, но она определена на Double, а вам приходится использовать Float. Как быть? Нам поможет функция realToFrac:

Prelude> :i realToFrac
realToFrac :: (Real a, Fractional b) => a -> b
  	-- Defined in `GHC.Real'

Она принимает значение из класса Real и приводит его к значению, которое можно делить. Что это за класс Real? Математики наверное смекнут, что это противоположность комплексным числам (где-то должен быть определён тип или класс Complex, и он правда есть, но об этом в следующем разделе). При переходе к комплексным числам мы теряем способность сравнения на больше/меньше, но сохраняем возможность вычисления арифметических операций, поэтому класс Real это пересечение классов Num и Ord:

Prelude> :i Real
class (Num a, Ord a) => Real a where
  toRational :: a -> Rational

Здесь “пересечение” означает “и тот и другой”. Пересечение классов кодируется с помощью контекста. Вернёмся к нашему первому примеру:

Prelude> realToFrac (1::Float) + (1::Double)
2.0

Отметим, что этой функцией можно пользоваться не только для типов Float и Double, в Haskell возможны самые экзотические числа.

Если преобразования между Float и Double происходят очень-очень часто, возможно имеет смысл воспользоваться специальными для GHC функциями: Они определены в модуле GHC.Float:

Prelude> :m +GHC.Float
Prelude GHC.Float> :t float2Double
float2Double :: Float -> Double
Prelude GHC.Float> :t double2float
double2Float :: Double -> Float

Документация

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

Но не стоит писать все функции самостоятельно, если функция достаточно общая её наверняка кто-нибудь уже написал. Самые полезные функции и классы определены в модуле Prelude и основных стандартных библиотечных модулях. Было бы излишним описывать каждую функцию, книга превратилась бы в справочник. Вместо этого давайте научимся искать функции в документации. Нам понадобится умение составлять типы функций и небольшое знание английского языка.

Для начала о том, где находится документация к стандартным модулям. Если вы установили ghc вместе с Haskell Platform под Windows скорее всего во вкладке Пуск, там где иконка ghc там же находится и документация. В Linux необходимо найти директорию с документацией, скорее всего она в директории /usr/local/share/doc/ghc/libraries. Также документацию можно найти в интернете, наберите в поисковике Haskell Hierarchical Libraries. На главной странице документации вы найдёте огромное количество модулей. Нас пока интересуют разделы Data и Prelude. Разделы расположены по алфавиту. То что вы видите это стандартный вид документации в Haskell. Документация делается с помощью специального приложения Haddock, мы тоже научимся такие делать, но позже, пока мы попробуем разобраться с тем как искать в документации функции.

Предположим нам нужно вычислить длину списка. Нам нужна функция, которая принимает список и возвращает целое число, скорее всего её тип [a] -> Int, обычно во всех библиотечных функциях для целых чисел используется тип Int, также на месте параметра используются буквы a, b, c. Мы можем открыть документацию к Prelude набрать в строке поиска тип [a] -> Int. Или поискать такую функцию в разделе функций для списков List Operations. Тогда мы увидим единственную функцию с таким типом, под говорящим именем length. Так мы нашли то, что искали.

Или мы ищем функцию, которая переворачивает список, нам нужна функция с типом [a] -> [a]. Таких функций в Prelude несколько, но имя reverse одной из них может намекнуть на её смысл.

Но одной Prelude мир стандартных функций Haskell не ограничивается, если вы не нашли необходимую вам функцию в Prelude её стоит поискать в других библиотечных модулях. Обычно функции разделяются по тому на каких типах они определены. Так например функция sort :: Ord a => [a] -> [a] определена не в Prelude, а в отдельном библиотечном модуле для списков он называется Data.List. Так же есть много других модулей для разных типов, таких как Data.Bool, Data.Char, Data.Function, Data.Maybe и многие другие. Не пугайтесь изобилия модулей постепенно они станут вашей опорой.

Для поиска в стандартных библиотеках есть замечательный интернет-сервис Hoogle (http://www.haskell.org/hoogle/). Hoogle может искать значения не только по имени, но и по типам. Например мы хотим узнать целочисленный код символа. Поиск по типу Char -> Int выдаёт искомую функцию digitToInt.

Краткое содержание

В этой главе мы познакомились с интерпретатором ghci и основными типами. Рассмотрели много примеров.

Типы

Bool Основные операции: &&, ||, not, if c then t else e
Char Значения пишутся в ординарных кавычках, как в 'H', '+'
String Значения пишутся в двойных кавычках, как в "Hello World"
Int Эффективные целые числа, но ограниченные
Integer Не ограниченные целые числа, но не эффективные
Double Числа с двойной точностью
Float Числа с ординарной точностью
Rational Дробные числа

Нам впервые встретились кортежи (на функции properFraction). Кортежи используются для возвращения из функции нескольких значений. Элементы кортежа могут иметь разные типы. Для извлечения элементов из кортежей-пар используются функции fst и snd. Кортежи пишутся в скобках, и элементы разделены запятыми:

(a, b)
(a, b, c)
(a, b, c, d)
...

Классы

Show Печать
Eq Сравнение на равенство
Num Сложение и умножение
Fractional Деление

Особенности синтаксиса

Запись применения функции:

Префиксная Инфиксная

add a b

a `add` b

(+) a b

a + b

Также мы научились приводить одни численные типы к другим и пользоваться документацией.

Упражнения

Зарегистрировано под лицензией Creative commons Attribution-NonCommercial-NoDerivs 3.0 Generic (CC BY-NC-ND 3.0)