Функциональное программирование. Конспект лекций
.pdfкоторое «сохраняется» в монаде State. Для запуска процесса необходимо задать начальное состояние!!!
8.5. Mонада ST
Существуют алгоритмы, которые проще всего реализуются с использованием изменяемых значений переменных. Однако обновление значения является побочным эффектом, так как появляются фазы: до обновления и после обновления. Если же обновление происходит локально, меняется только одно значение, за время обновления ни одна другая переменная не может пользоваться промежуточными значениями, и обновления происходят с помощью чистых функций, то такую функцию можно считать чистой. Для реализации изменяемых значений и обеспечения чистоты функций имеется «монадический» тип ST, по структуре похожий на монаду State, но для хранения изменяемого значения используется монада STRef. Причем типы ST и STRef имеют общий параметр состояния s: ST s (STRef s a). Начальное значение состояния s берется «из воздуха». В Haskell специально для этого типа сделано исключение. Это чисто фиктивный параметр, он скрыт от пользователя и нигде не может использоваться.
Тип ST (State Transformer) является экземпляром Monad и определен в модуле Control.Monad.ST. У него два параметра и нет конструкторов [1]:
data ST s a.
Для хранения значений используется тип Data.STRef (ST reference). Основные функции для работы с хранимыми значенями:
newSTRef :: a -> ST s (STRef s a) readSTRef :: STRef s a -> ST s a writeSTRef :: STRef s a -> a -> ST s ().
Такие функции иногда называют смышлеными конструкторами (smart constructors), так как они позволяют строить значение, но скрывают от пользователя реализацию за счет скрытия конструкторов типа (модуль экспортирует лишь имя типа STRef).
С помощью этих функций можно реализовать функцию обновления значения по ссылке (функцию из модуля Data.STRef):
– 91 –
modifySTRef :: STRef s a -> (a -> a) -> ST s () modifySTRef ref f = writeSTRef ref . f =<< readSTRef ref.
Так же, как и для State, для ST определены экземпляры классов
Functor, Applicative и Monad. Функция runST возвращает «чистое» значение из монады:
runST :: (forall s. ST s a) -> a.
Слово forall означает «для любых». Это слово называется квантором всеобщности и говорит о том, что функция извлечения значения не может делать никаких предположений о типе фиктивного состояния. Функция runST забывает тип фиктивного состояния s из типа ST, но в типе STRef этот параметр строго задан. По типу переменной v :: STRef s a компилятор может сказать, что существует такое s, для которого значение v имеет смысл (правильно типизировано). Но оно не любое! Тем самым компилятор будет следить за «чистотой» обновлений. С помощью монады ST s (STRef s a) можно реализовать циклы, аналогичные циклам в императивных языках программирования. Например, цикл for:
import Control.Monad import Data.STRef import Control.Monad.ST
forLoop :: i -> (i -> Bool) -> (i -> i) -> (i -> s -> s) -> s -> s
forLoop i0 pred next update s0 = runST $ do refI <- newSTRef i0
refS <- newSTRef s0 iter refI refS readSTRef refS
where iter refI refS = do i <- readSTRef refI s <- readSTRef refS when (pred i) $ do
writeSTRef refI $ next i writeSTRef refS $ update i s iter refI refS
– 92 –
Параметрами функции forLoop являются:
•i0 – начальное значение индекса,
•pred – предикат окончания цикла,
•next – функция увеличения индекса,
•update – функция изменения состояния s,
•s0 – начальное состояние.
8.5.1. Монада изменяемых массивов STArray
В Haskell есть несколько типов изменяемых массивов [1]. Следующий класс определяет общий интерфейс к различным массивам:
class (HasBounds a, Monad m) => MArray a e m where newArray :: Ix i => (i, i) -> e -> m (a i e)
newArray_ :: Ix i => (i, i) -> m (a i e).
Метод newArray создает массив типа a, который «завернут» в тип-монаду m. (i, i) – диапазон значений индексов массива, e – элемент, который будет записан во все ячейки массива. Для работы с массивом используются функции:
readArray :: (MArray a e m, Ix i) => a i e -> i -> m e writeArray :: (MArray a e m, Ix i) => a i e -> i -> e -> m ().
При использовании монады ST для получения неизменяемого массива из монадного изменяемого используется функция:
runSTArray :: Ix i => (forall s . ST s (STArray s i e)) -> Array i e.
Для создания изменяемого монадного массива типа ST s (STArray s i e) из списка необходимо использовать функцию
newListArray (i, j) list,
где (i,j) – диапазон индексов массива, list – список.
Ниже приводится пример создания изменяемого массива из списка, запись нового значения элемента и печать элементов массива.
import Control.Monad import Data.STRef import Control.Monad.ST
– 93 –
import Data.Array import Data.Array.ST
createArr::[Int] -> ST s (STArray s Int Int) createArr xs = do
arr <- newListArray (0, length xs - 1) xs writeArray arr 0 777
return arr printArr::[Int] -> [Int]
printArr xs = elems $ runSTArray $ arr where arr = createArr xs
В результате выполнения функции printArr [1,2,3,4,5] будет получено
*Qsort> printArr [1,2,3,4,5] [777,2,3,4,5]
Как видно, значение нулевого элемента массива было заменено на 777.
Ниже представлен фрагмент кода программы быстрой сортировки массива с использованием «монадического» типа ST s (STArray s i e):
import Control.Monad import Data.STRef import Control.Monad.ST import Data.Array import Data.Array.ST
forLoop :: i -> (i -> Bool) -> (i -> i) -> (i -> s -> s) -> s -> s
forLoop i0 pred next update s0 = runST $ do refI <- newSTRef i0
refS <- newSTRef s0 iter refI refS readSTRef refS
– 94 –
where iter refI refS = do i <- readSTRef refI s <- readSTRef refS when (pred i) $ do
writeSTRef refI $ next i writeSTRef refS $ update i s iter refI refS
swapElems :: Ix i => i -> i -> STArray s i e -> ST s ()
swapElems i j arr = do
vi <- readArray arr i vj <- readArray arr j writeArray arr i vj writeArray arr j vi
qsortST :: Ord a => Int -> Int -> (forall s. STArray s Int Int) -> ST s ()
qsortST left right arr = do when (left <= right) $ do
swapElems left (div (left + right) 2) arr vLeft <- readArray arr left
(last, _) <- forLoop (left + 1) (<= right) succ (update vLeft) (return (left, arr))
swapElems left last arr qsortST left (last - 1) arr qsortST (last + 1) right arr where update vLeft i st = do
(last, arr) <- st
vi <- readArray arr i if (vi < vLeft)
then do
swapElems (succ last) i arr return (succ last, arr)
else do
return (last, arr) qsort :: Ord a => [a] -> [a]
– 95 –
qsort xs = elems $ runSTArray $ do
arr <- newListArray (left, right) xs qsortST1 left right arr
return arr where left = 0
right = length xs - 1
Результат работы программы сортировки выглядит следующим образом:
*Qsort> qsort [5,3,1,8,4] [1,3,5,8,4]
– 96 –
Лекция 9. Отладка в Haskell
Рассматривается технология и особенности отладки программ в Haskell. Описываются шаги и действия разработчика при отладке программ.
Ключевые слова: отладчик, debug, точки прерывания, trace.
Введение. По сравнению с современными средами разработки ПО, такими как Microsoft Visual Studio, Eclipse, PyCharm, Spyder и
другие, режим отладки в Haskell не очень удобен. Отладчик интегрирован в интерпретатор GHCi, включается по умолчанию и позволяет выполнять пошаговое выполнение программы и анализировать на каждом шаге значение связанных переменных. Точки останова (breakpoints) и пошаговая отладка доступны только в режиме интерпретатора. Скомпилированный код недоступен отладчику.
Итак, отладчик обеспечивает следующую функциональность: установка точек прерывания программ (breakpoints) на опреде-
лениях функций или на выражениях;
пошаговая отладка (single-stepped) программы с остановкой после каждой редукции выражений и с возможностью анализа значений связанных переменных;
трассировка программы (tracing mode), при которой интерпретатор запоминает каждый шаг редукции, но не останавливает вычисления до достижения точки прерывания (breakpoint);
трактовка исключений (ошибки сопоставления, error) как точек прерывания, что позволяет локализовать место исключения в программе.
9.1. Установка точек прерывания
Для установки точек прерывания желательно знать номера строк файла, в которых записана та или иная функция. Напомним, что программа на языке Haskell – это обычный текстовый файл (удобнее использовать Notepad++ вместо обычного блокнота). Поэтому после загрузки программы необходимо выдать команду
:list func,
– 97 –
для того чтобы получить информацию о номерах строк функции func; например, чтобы получить информацию о расположении функции qsort, дадим команду :list qsort:
*TestTrace> :list qsort
4
5qsort [] = []
6qsort (a:as) = trace ("left:" ++ show left) (qsort left) ++ [a] ++ trace ("right:" ++ show right) (qsort right)
7where (left,right) = (filter (<=a) as, filter (>a) as)
В результате можно увидеть, что функция qsort располагается в строках 5-7.
Для установки точек прерывания в режиме интерпретатора можно ввести следующие команды:
:break identifier :break line :break line column :break module line
:break module line column.
В качестве identifier указывается имя отлаживаемой функции. Параметры line и column указывают на номер строки и столбца выражения. Если имя модуля (module) не указано, то подразумевается последний загруженный модуль.
Просмотреть установленные точки прерывания можно с помощью команды
:show breaks.
Например, в приведенном ниже примере расставлены две точки прерывания в строках 5 и 6:
*TestTrace> :break 5
Breakpoint 0 activated at testTrace.hs:5:12-13 *TestTrace> :break 6
– 98 –
Breakpoint 1 activated at testTrace.hs:6:16-111 *TestTrace> :show breaks
[0]TestTrace testTrace.hs:5:12-13
[1]TestTrace testTrace.hs:6:16-111
Порядковые номера точек прерывания указаны в квадратных скобках (в данном случае указаны две точки прерывания [0] и [1]).
Для удаления точки прерывания используется команда
:delete n,
где n – номер точки прерывания.
Для удаления всех точек прерывания используется команда
:delete *.
9.1.1. Вложенные точки прерывания
Когда отладчик останавливается в точке прерывания, можно ввести выражение, типа
:step qsort [1,3],
которое запускает новую вложенную точку прерывания. Эта новая точка прерывания становится «текущей», а старая сохраняется в стеке. Таким образом, можно создать произвольное количество контекстов точек прерывания.
9.1.2. Переменная _result
При остановке в точке прерывания отладчик связывает переменную _result со значением текущего активного выражения. Значение _result может быть не доступно, так как была остановка его оценки. Однако если тип _result известен и доступен для отображения, то, просто введя _result в приглашении, можно увидеть его значение. Однако выполнение _result может вызвать дальнейшие точки прерывания, начиная с точки прерывания, на которой произошла остановка в данный момент. Поэтому необходимо будет выполнить команду :continue сразу после оценки _result.
– 99 –
9.2. Пошаговая отладка
Для пошаговой отладки имеются команды:
:step [expr] :steplocal [expr] :stepmodule [expr].
Команда :step используется, чтобы включить все точки прерывания в программе, и выполняется, пока не будет достигнута следующая точка прерывания (параметры, указанные в квадратных скобках, могут отсутствовать).
Команда :steplocal ограничивает набор разрешенных точек прерывания точками в текущей функции верхнего уровня.
Команда :stepmodule используется для пошагового выполнения только для точек прерывания, содержащихся в текущем модуле.
Команда :step expr выполняет оценку выражения expr в одношаговом режиме (single-stepping mode). Если выражение expr опущено, то выполняется один шаг от текущей точки останова.
Для определения точки прерывания в программе после очередного шага используется команда :list. Интерпретатор выделяет специальными значками типа «^» те выражения, которые редуцируются на данном шаге. Чтобы выделение было корректным, очень важно при написании программы при создании отступов, блоков do или where использовать пробел, а не знак табуляции. Кроме того, при добавлении новой инструкции в середину блока do или where редактор, как правило, выравнивает инструкции с помощью табуляции, приводя к тому, что Haskell считает эту инструкцию уже не находящейся в блоке, и выдает сообщение об ошибке!
Для удобства рекомендуется перед началом отладки дать команды:
:set stop :list
:set -fprint-evld-with-show.
Первая обеспечит выдачу выражения, которое редуцируется на текущем шаге, а вторая позволит использовать команду
:force var
вместо команды
:print var
– 100 –
