Стеки и чебуреки

Предположим, мы хотим смоделировать стек. Стек – это структура данных, которая содержит набор элементов и поддерживает ровно две операции:

• проталкивание элемента в стек (добавляет элемент на верхушку стека);

• выталкивание элемента из стека (удаляет самый верхний элемент из стека).

Для представления нашего стека будем использовать список, «голова» которого действует как вершина стека. Чтобы решить эту задачу, создадим две функции:

• функция pop будет принимать стек, выталкивать один элемент и возвращать его в качестве результата. Кроме того, она возвращает новый стек без вытолкнутого эле мента;

• функция push будет принимать элемент и стек, а затем проталкивать этот элемент в стек. В качестве результата она будет возвращать значение () вместе с новым стеком.

Вот используемые функции:

type Stack = [Int]

pop :: Stack –> (Int, Stack)

pop (x:xs) = (x, xs)

push :: Int –> Stack –> ((), Stack)

push a xs = ((), a:xs)

При проталкивании в стек в качестве результата мы использовали значение (), поскольку проталкивание элемента на вершину стека не несёт какого-либо существенного результирующего значения – его основная задача заключается в изменении стека. Если мы применим только первый параметр функции push, мы получим вычисление с состоянием. Функция pop уже является вычислением с состоянием вследствие своего типа.

Давайте напишем небольшой кусок кода для симуляции стека, используя эти функции. Мы возьмём стек, протолкнём в него значение 3, а затем вытолкнем два элемента просто ради забавы. Вот оно:

stackManip :: Stack –> (Int, Stack)

stackManip stack = let

   ((), newStack1) = push 3 stack

   (a , newStack2) = pop newStack1

   in pop newStack2

Мы принимаем стек, а затем выполняем выражение push 3 stack, что даёт в результате кортеж. Первой частью кортежа является значение (), а второй частью является новый стек, который мы называем newStack1. Затем мы выталкиваем число из newStack1, что даёт в результате число a (равно 3), которое мы протолкнули, и новый стек, названный нами newStack2. Затем мы выталкиваем число из newStack2 и получаем число и новый стек. Мы возвращаем кортеж с этим числом и новым стеком. Давайте попробуем:

ghci> stackManip [5,8,2,1]

(5,[8,2,1])

Результат равен 5, а новый стек – [8,2,1]. Обратите внимание, как функция stackManip сама является вычислением с состоянием. Мы взяли несколько вычислений с состоянием и как бы «склеили» их вместе. Хм-м, звучит знакомо.

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

stackManip = do

   push 3

   a <– pop

   pop

Ла-адно, монада State позволит нам делать именно это!.. С её помощью мы сможем брать вычисления с состоянием, подобные этим, и использовать их без необходимости управлять состоянием вручную.