Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
MakeevGA-Haskell-a4-shortcode_2014_05_31.doc
Скачиваний:
29
Добавлен:
19.01.2023
Размер:
1.79 Mб
Скачать

Чтение сигнатур типов

Теперь мы знаем достаточно, чтобы уметь читать любую сигнатуру типа. Или, другими словами, разбираться в типе любых значений, выражений и функций, которые нам могут встретиться. Давайте потренируемся в построении сложносочиненных и сложноподчиненных выражений русского языка. Как только поймете, что от скобок и стрелок кружится голова – переходите к следующему разделу: в конце концов, совсем уж сложные сигнатуры типов встречаются редко.

foo :: (a -> b) -> [a] -> [b]

Значение foo – это функция, берущая функцию, берущую значения типа a и возвращающую значение типа b, берущая также список значений типа a и возвращающая список значений типа b.

Громоздко звучит? Давайте подсократим и используем синонимы: foo – это функция, берущая функцию, отображающую тип a в тип b, берущая список типа a и возвращающая список типа b.

boo :: ((a,b) -> b) -> [b -> [a] -> b]

Значение boo – это функция, которая:

  • принимает функцию ((a,b) -> b), берущую кортеж (пару) из значения типа a и значения типа b и возвращающую значение типа b;

  • возвращает аж список функций [b -> [a] -> b], берущих значение типа b и список типа a и возвращающих значения типа b.

Простейшие функции и операторы

Ладно, с простыми и составными типами данных мы разобрались. Самое время разобраться, какие же элементарные функции нам предоставляет язык Haskell для работы с ними.

Арифметические функции

Ну, тут все просто. Арифметика обязана быть в любом языке, и Haskell тут не исключение. Может быть, вас, разве что, удивят типы некоторых операторов, – ну так самое время проверить, как вы научились читать типы данных, на примере следующих операторов и функций:

(+), (-), (*), (/), div, mod, (^), sum, product, max, min, maximum, minimum, even, odd, gcd, lcm

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

Короче говоря:

  • можно написать 5+3, а можно (+) 5 3, и это будет одним и тем же;

  • можно написать mod 5 3, а можно 5 `mod` 3, и это тоже будет одним и тем же (обратите внимание, это обратные апострофы, а не обычные кавычки).

Логические функции

С логическими функциями, то есть с функциями, возвращающими значения типа Bool, тоже все просто. Сравнения на больше-меньше, на равенство и неравенство, комбинация нескольких условий с помощью операций "и", "или" и "не". Немного на особицу стоят функции (именно функции, а не операторы) and и or, принимающие сразу список значений Bool:

(>), (<), (==), (/=), (>=), (<=), (&&), (||), not, and, or

Списочные функции

Простейшие списочные функции позволяют выполнять только несколько самых-самых простейших операций по синтезу и анализу списков. Начнем с анализа:

head :: [a] -> a

Функция head берет список и возвращает его первый элемент. Да, вот так просто. Например:

head [3,2,1] → 3

head [[3,2],[1,2]] → [3,2]

head "hello" → 'h'

Обратите внимание на тип функции head – да, она не накладывает вообще никаких ограничений на то, что может храниться в списке. Там могут лежать, в том числе, и функции:

head [sin,cos] → sin

Второй функцией для разбора списков на составные части является функция tail:

tail :: [a] -> [a]

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

tail [3,2,1] → [2,1]

tail [[3,2],[1,2]] → [[1,2]]

tail "hello" → "ello"

Может быть, вы ждете функцию, которая возвращает последний элемент списка? А нет такой! Ну ладно, ладно – есть такая функция. Но если в нее заглянуть, то ничего кроме head и tail мы не увидим. Потому что списки в Haskell действительно устроены так, что, имея список, можно обратиться только или к первому элементу (к голове), или к той части списка, что идет сразу за ним – к хвосту то бишь. А как же добраться до последней буквы в слове "hello"? Ответ прост – несколько раз применить head и tail:

head (tail (tail (tail (tail "hello")))) → 'o'

Не ошибитесь в количестве вызовов функций tail и head – если вызвать их от пустого списка, вы получите ошибку. Проверить, является ли список пустым, можно с помощью третьей списочной функции:

null :: [a] -> Bool

Функция null берет список и возвращает True, если этот список пуст, и False в любом другом случае.

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

(:) :: a -> [a] -> [a]

Встречайте операцию создания списков! Она берет элемент и уже готовый список, и возвращает новый список, добавив элемент в начало (обратите внимание – именно в начало):

6:[4,5] → [6,4,5]

Функция (:), как выясняется, выполняет операцию обратную тем действиям, что делают со списком функции head и tail. И действительно, для любого непустого списка xs следующее выражение всегда имеет значение True:

head xs : tail xs == xs

Я слышу голос какого-то зануды: "А если xs – это список функций, в котором лежат синусы и косинусы – разве получится сравнить этот список с самим собой – функции Float -> Float то ведь сравнивать нельзя, в отличие от самих значений Float!?". Не получится, это правда. Это больше, чем равенство – это тождество.

Подождите, мы создавали новый список из уже существующего списка [4,5]. А как был создан сам список [4,5]? Да в общем-то последовательным применением той же самой операции: 4:(5:[]). Никаких других возможностей создать список – нет.

Возможно, вы спросите, как добавить элемент в конец? А никак – только писать собственную функцию. А как слить два списка в один? Никак – только писать свою собственную функцию.

Ну ладно, ладно – можно воспользоваться стандартными. Но при этом важно понять, что стандартные функции будут как-то хитро использовать ту же самую операцию (:), потому что других способов создания списка – нет:

addToEnd 6 [4,5] → 4:(5:(6:[])) → [4,5,6]

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

Да, вы правы, добавление элемента к голове списка – операция с константной сложностью, тогда как операция добавление элемента к концу списка обязательно потребует линейной сложности от длины списка.

Теперь мы знаем четыре функции, на которых строятся все операции со списками: head, tail, null и (:). Конечно же, стандартная поставка языка включает в себя сотни уже написанных функций для работы со списками. Вот только часть из них:

last, init, length, (!!), (++), concat, take, drop, reverse, elem, replicate

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