Я вот говорю-говорю, а вдруг я вас обманываю, и ничего этого нет. В этой главе мы перейдём к программированию и запустим нашу первую программу в 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
Мы набираем в интерпретаторе какое-нибудь сложное выражение, или составной синоним, интерпретатор проводит редукцию и выводит ответ на экран. Откуда интерпретатор знает как отображать значения типа 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
:
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
. Посмотрим на его определение:
*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 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
:
*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 |
Деление |
Запись применения функции:
Префиксная | Инфиксная |
---|---|
|
|
|
|
Также мы научились приводить одни численные типы к другим и пользоваться документацией.
Напишите функцию beside :: Nat -> Nat -> Bool
, которая будет возвращать True
только в том случае, если два аргумента находятся рядом, то есть один из них можно получить через другой операцией Succ
.
Напишите функцию beside2 :: Nat -> Nat -> Bool
, которая будет возвращать True
только если аргументы являются соседями через некоторое другое число.
Мы написали очень неэффективную функцию сложения натуральных чисел. Проблема в том, что число рекурсивных вызовов функции зависит от величины второго аргумента. Если мы захотим прибавить единицу к сотне, то порядок следования аргументов существенно повлияет на скорость вычисления. Напишите функцию, которая лишена этого недостатка.
Напишите функцию возведения в степень pow :: Nat -> Nat -> Nat
.
Напишите тип, описывающий бинарные деревья BinTree a
. Бинарное дерево может быть либо листом со значением типа a
, либо хранить два поддерева.
Напишите функцию reverse :: BinTree a -> BinTree a
, которая переворачивает дерево. Она меняет местами два элемента в узле дерева.
Напишите функцию depth :: BinTree a -> Nat
, которая вычисляет глубину дерева, то есть самый длинный путь от корня дерева к листу.
Напишите функцию leaves :: BinTree a -> [a]
, которая переводит бинарное дерево в список, возвращая все элементы в листьях дерева.
Обратите внимание на раздел List Operations
в Prelude
. Посмотрите на функции и их типы. Попробуйте догадаться по типу функции и названию что она делает.
Попробуйте разобраться по документации с классами Ord
(сравнение на больше/меньше), Enum
(перечисления) и Integral
(целые числа). Также стоит отметить класс Floating
. Если у вас не получится, не беда, они обязательно встретятся нам вновь. Там и разберёмся.
Найдите функцию, которая переставляет элементы пары местами (элементы могут быть разных типов).
Потренируйтесь с кортежами. Определите аналоги функций fst
и snd
для не пар. Обратите внимание на то, что сочетание символов (,)
это функция-конструктор пары:
Prelude> (,) "Hi" 101 ("Hi",101) Prelude> :t (,) (,) :: a -> b -> (a, b)
Также определены (,,)
, (,,,)
и другие.