А здесь не будет программы. Здесь я опишу небольшой кусок
Prelude
подробно.
Буль-буль
Как уже писал, у
Bool
два конструктора,
True
и
False
, и на них мы можем матчить, так что могли бы и сами определить операции
&&
,
||
и функцию
not
как-то так:
(&&) :: Bool -> Bool -> Bool
False && _ = False
True && a = a
(||) :: Bool -> Bool -> Bool
False && _ = a
True || _ = True
not :: Bool -> Bool
not True = False
not False = True
Ещё немного о классах
Bool
входит в несколько экземпляров классов, и это тот способ, с помощью которого я коснусь сейчас их смысла:
Eq Bool
— значения можно сравнивать на равенство с помощью
==
и
/=
.
Ord Bool
— значения линейно упорядочены, и можно использовать
<
,
<=
,
>
,
>=
. Есть ещё две бинарные функции
max
и
min
и редко используемая функция
compare
, возвращающая значение типа
Ordering
—
LT
,
EQ
или
GT
. Для определений порядка на своих типах она полезна.
Здесь
True > False
Read Bool
,
Show Bool
— как уже описано выше, говорят, что можно взаимно конвертировать значения
Bool
и
String
.
Bounded Bool
— есть самое большое из значений
maxBound
и самое маленькое
minBound
.
Enum Bool
— значения можно конвертировать в целые числа и обратно с помощью
fromEnum :: Enum a => a -> Int
и
toEnum :: Enum a => Int -> a
.
fromEnum False = 0
,
fromEnum True = 1
.
Ещё
Enum
определяет методы получения предыдущего или следующего значения
pred :: Enum a => a -> a
и
succ :: Enum a => a -> a
, например:
ghci> succ 'h'
'i'
ghci> pred (minBound :: Bool)
*** Exception: Prelude.Enum.Bool.pred: bad argument
И ещё: так же как монады имеют синтаксический сахар в виде блоков do, с экземплярами
Enum
связан особый синтаксис записи списков, целых четыре:
[m ..] = enumFrom m
даст список, начинающийся с
m
, в котором следующие элементы больше на 1, 2, 3 и т. д.. Он может оказаться и бесконечным.
[m .. n] = enumFromTo m n
даст список, заканчивающийся на элементе, не большем
n
.
[m, r ..] = enumFromThen m r
аналогично первому, но промежутком между элементами будут не единицы, а «разность» между
r
и
m
. Она может быть и нулём, и отрицательным числом, и даже нецелой, и не иметь в последнем случае никакой связи со значениями
fromEnum
от
m
и
r
*. Видимо, из-за такой непрочной связи класс
Enum
стоит в будущем разбить на несколько.
[m, r .. n] = enumFromThenTo m r n
сочетает в себе преимущества и недостатки второго и третьего вариантов.
Экземпляры перечисленных шести классов для какого-нибудь своего нового типа можно записать механически, исходя из его определения (если не нужно что-то особое). Потому, чтобы не мучить людей, можно поручить это компилятору, после
data ... = ...
написав
deriving (Eq, Ord, Show)
или, например,
deriving Enum
(т. е. для одного класса без скобок).
Характер
Хочу поскорей покончить с описанием типа символов. Это
Char
. Он может содержать любой один символ уникода, при этом
fromEnum
и
toEnum
связывают символы и их коды, так что можно гарантировать, что
fromEnum 'A' = 65
(неужели помню?
), но всё равно пользуйтесь для прозрачности методами
Enum
. Ну и, разумеется,
String
— это синоним
[Char]
, и вот как это объявить:
type String = [Char]
Всё, всё, довольно. 1114112 — это много, слишком много…
Скобочки
У единичного типа есть братья-кортежи, которые намного интереснее, и ещё в лучших традициях математики нет брата-кортежа из одного элемента.
Типы кортежей, как и тип списков, можно обозначать некрасиво, но справедливо, и красиво, но сладко:
- Справедливо:
(,) Int Char
, (,,) Bool Bool Bool
, (,,,) a b c d
. Аналогия — [] Char
. Конструктор типа спереди, типы-аргументы после — как и с другими типами, конструкторы которых по случайности пишутся буквами: IO Int
, например.
- Сладко:
(Int, Char)
, (Bool, Bool, Bool)
, (a, b, c, d)
. Аналогия — [Char]
.
Даже интерпретатор будет пользоваться вторым способом.
Практически никаких особых функций для кортежей, не связанных с какими-то другими особыми типами, нет. Типу
(,) a b
повезло немного больше, чем остальным: есть две функции-проекции
fst :: (a, b) -> a
fst (a, _) = a
snd :: (a, b) -> b
snd (_, b) = b
Остальные проекции можно сляпать и на коленке вот так:
\(_, _, a, _) -> a
. (Как это «ты не говорил про шаблоны вместо параметра в лямбда-выражении»?)
В категорном смысле кортеж — это тип-произведение (а
()
— это терминальный объект, выступающий для произведения единицей). С произведением типов связано произведение морфизмов (которые здесь — функции), которое придётся написать самим:
(⊗) :: (a -> a') -> (b -> b') -> (a, b) -> (a', b')
(f ⊗ g) (a, b) = (f a, g b)
Есть ещё его урезанный вариант
(∧) :: (a -> b) -> (a -> c) -> a -> (b, c)
(f ∧ g) a = (f a, g a)
(Для приложений им надо будет приделать ещё левоассоциативность и приоритет, но не хочется смотреть, какой из десяти.) Кстати, ещё есть
curry :: ((a, b) -> c) -> a -> b -> c
curry f a b = f (a, b)
uncurry :: (a -> b -> c) -> (a, b) -> c
uncurry f (a, b) = f a b
И снова остальные типы кортежей обделены.
Ещё можно написать функции с типами
(a -> a') -> (a, b) -> (a', b)
и
(b -> b') -> (a, b) -> (a, b')
. Что они могли бы делать? Кроме того, ну почти естественна ещё и функция-селектор из типа с
n значениями и
n-элементного кортежа. Для пары её можно написать как-то так:
uncurry . (\a b c -> if a then b else c)
или так:
\a -> if a then fst else snd
Выведите её тип и напишите, что именно с ней не в порядке!
**
Смысловое: типы кортежей стоит использовать, например, для возврата нескольких значений (когда есть причины вернуть их из одной функции, а не каждое из своей), и если значения не связаны друг с другом так, что для них есть смысл наопределять функций и представить каким-нибудь новым типом данных с объявлением
data Date = Date Int Month Int
(обычно, если конструктор данных один и экспортируется из модуля, его называют так же как конструктор типа — они всё равно находятся в разных пространствах имён, а запоминать-выдумывать меньше). К тому же, свой тип можно объявить и так:
data Date = Date { year :: Int, month :: Month, day :: Int }
При этом, кроме обычных вещей, автоматически определятся функции
year :: Date -> Int
,
month :: Date -> Month
и
day :: Date -> Int
, а ещё можно будет описывать значения этого типа вот так:
Date { year = 2014, month = March, day = 23 }
, матчить вот так:
Date { y, m, d }
и — внимание! — модифицировать значение вот так:
myDate { year = 1985 }
. Последнее эквивалентно
(\(Date d m y) -> Date d m 1985) myDate
.
Левое и правое
Посмотрев на типы-произведение, логично бы сразу рассмотреть стандартный тип-сумму. Он всего один —
Either a b
. Ну, точнее, их столько же, сколько
(a, b)
— много-много, но конструктор типа всего один.
Either a b
имеет два конструктора
данных Left :: a -> Either a b
и
Right :: b -> Either a b
, так что его значения — это дизъюнктное объединение значений типа
a
и типа
b
. А можно ли определить сумму функций? Да:
(⊕) :: (a -> a') -> (b -> b') -> Either a b -> Either a' b'
(f ⊕ g) Left a = Left $ f a
(f ⊕ g) Right b = Right $ g b
И урезанный вариант:
(∨) :: (a -> c) -> (b -> c) -> Either a b -> c
(f ∨ g) Left a = f a
(f ∨ g) Left b = g b
…и этот вариант из всех четырёх ⊕, ⊗, ∧, ∨ был выбран находиться в
Prelude
под именем
either
. Подобная (но ничуть не аналогичная) функция есть, чтобы разобрать
Maybe
, и называется она угадайте как (или не стоит: она будет описана ниже).
Как и
Maybe v
,
Either err v
используется подчас при столкновении с ошибками, только вместо безликого
Nothing
у нас появляется
Left сообщение
, куда мы можем записать нужную информацию (но лучше всё-таки
Right answer
). Если она есть — например, ошибка при взятии головы списка может быть только одна — пустота, так что
Either
для описания результата подобной функции будет совершенно лишним.
А в
Data.Either
есть ещё три функции:
rights :: [Either a b] -> [b]
— отделить все вершки,
lefts :: [Either a b] -> [a]
— отделить все корешки,
partitionEithers :: [Either a b] -> ([a], [b])
— эти туда, те сюда (да нет же, наоборот!).
Эти выглядят на первый взгляд более практичными, но почему не переэкспортированы?..
Может быть
Уже много раз упоминавшийся тип
Maybe
… Чтобы работать с его значениями эффективнее, есть немало готовых функций. В первую очередь, это
maybe :: b -> (a -> b) -> Maybe a -> b
maybe d _ Nothing = d
maybe _ f (Just x) = f x
Она обработает переданное может-быть-значение функцией или вернёт нам
деньги значение по умолчанию, переданное в самом начале. А вообще, лучше просто использовать то, что
Maybe
— монада, но о монадах будет не сейчас.
Остальные функции придётся тащить из
Data.Maybe
. Наиболее интересные из них — это
catMaybes :: [Maybe a] -> [a]
— удаляет
Nothing
и из списка, а остальные за ненадобностью разворачивает (
catMaybes [Just 2, Nothing, Just 3] = [2, 3]
) и
mapMaybe :: (a -> Maybe b) -> [a] -> [b]
mapMaybe = catMaybes . map
Списки
Есть два конструктора у списков,
Один из них зовётся
[] :: [a] -- nil
,
Другой из них зовётся
(:) :: a -> [a] -> [a] -- cons
,
На том и кончится анонс.
Нет, вру, не кончится. Перед тем как погружаться в пучину
(Functor, Applicative, Alternative, Monad, Monoid, Foldable, Traversable, Arrow, …)
, было бы крайне дурным тоном не рассмотреть конкретные вещи.
Начнём с нашей несравненной
map:
map :: (a -> b) -> [a] -> [b]
map _ [] = []
map f (x:xs) = f x : map f xs
map
можно даже рассматривать как вид свёртки, но я не буду (потому что не помню как
). Ещё можно упомянуть, что многие-многие хорошие вещи имеют тип
(a -> b) -> (f a -> f b)
,
и зовут их эндофункторами молчу, молчу.
(++) :: [a] -> [a] -> [a]
— конкатенация списков.
filter :: (a -> Bool) -> a -> a
оставит в списке только элементы, удовлетворяющие предикату-первому-параметру.
null :: [a] -> Bool
проверяет список на пустоту.
head, last :: [a] -> a
и
tail, init :: [a] -> [a]
— частичные функции и не работают на пустом списке, и потому из лучших побуждений
я не стану говорить, что они делают!!!
length :: [a] -> Int
работает за O(n), и я снова не сказал бы, что она делает, но другого способа узнать длину односвязного списка в природе не существует…
(!!) :: [a] -> Int -> a
— 0-based индексирование.
xs !! n
даст
n
-й элемент, если он есть, за O(n). Нет, списки для этого не предназначены. Есть
Array
и и всякие
Set
ы-
Map
ы. Осторожно, ведь эта функция тоже частичная.
reverse :: [a] -> [a]
поиграет с пользователем в реверси. (Внутри используется
unsafePerformIO
, и потому тип результата — не
IO [a]
.) Если список бесконечный, игра никогда не закончится.
Остальное можно прочитать
здесь, а я лучше добавлю про list comprehensions.
[ f a | a <- as ] = map f as = map (\a -> f a) as
[ f a b | a <- as, b <- bs ] = concatMap (\a -> map (f a) bs) as
[ f a | a <- as, p a ] = filter p $ map f as
[ f a b c | a <- as, b <- bs, p a b, q a, c <- cs, r c b ] = wtf' -- зачем они тогда были бы нужны?
Если использовать расширения языка
ParallelListComp
и
TransformListComp
, синтаксис этих штук обогатится. Можно будет написать такое:
ghci> :set -XParallelListComp
ghci> [ (a,b) | a <- [1,3,5] | b <- [2,4..] ]
[(1,2),(3,4),(5,6)]
и, со вторым расширением, SQL-подобные «запросы».
Кстати, список — тоже монада, и оччень забавная.
Числа и их классы — тема настолько запутанная, что лучше её обсуждать отдельно
P. S. Кто не боится переполнения,
вот.
* Смотрите:
fromEnum 2.1 = fromEnum 2.9 = 2
, но при этом
[2.1, 2.2 .. 2.9]
срабатывает и даёт (ну почти что)
[2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9]
.
** А ещё напишите как можно короче выражение с типом
a -> b -> c -> d -> (a, b, c, d)
. Надеюсь, мне не придётся писать, во сколько символов можно уложиться?