Previous Up Next

Chapter 7  Библиотеки

Введение

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

Большое количество библиотек поставляется в дистрибутиве Objective CAML. Они распространяются в виде скомпилированных файлов. Однако, любознательный читатель может найти исходные файлы библиотек в дистрибутиве исходников языка.

Библиотеки Objective CAML организованы по модулям, которые в свою очередь являются элементами компиляции. Каждый из них содержит глобальные определения типов, исключений и значений, которые могут быть использованы в программе. В данной главе, мы не будем рассматривать построение подобных модулей, а лишь использование существующих. В 13 главе мы вернемся к концепции модуля (логический элемент) и элемента компиляции и опишем язык описания модулей в Objective CAML. А в 11 мы обсудим включение кода написанного на другом языке в библиотеки Objective CAML, в частности интеграция кода на C в Objective CAML.

В дистрибутив Objective CAML входит preloaded библиотека (модуль Pervasives), набор базовых модулей, называемых стандартной библиотекой и много других библиотек, добавляющих дополнительные возможности. Некоторые библиотеки лишь упоминаются в данной главе или они описываются в следующих главах.

План главы

В этой главе мы опишем библиотеки входящие в дистрибутив Objective CAML. Некоторые из них уже были представлены в предыдущих главах, например Graphics (см. главу 4) или лишь упомянуты, как библиотека Array. В первом разделе описана организация различных библиотек. Во втором разделе мы рассмотрим preloaded модуль Pervasives. В третьем разделе мы сделаем классификацию множества модулей, сгруппированных в стандартной библиотеки. И наконец в четвертом разделе подробно рассмотрим библиотеки точной арифметики и динамической загрузки кода.

7.1  Классификация и использование библиотек

Библиотеки дистрибутива Objective CAML могут быть разделены на три части. В первой содержатся preloaded глобальные объявления. Вторая, так называемая стандартная библиотека, имеет достаточно высокий уровень стабильности. Она разделена на 4 части:

В третьей части библиотек находятся модули расширяющие возможности языка, как например библиотека Graphics (см. главу 4). В этой части мы можем найти библиотеки для обработки регулярных выражений (Str), точной арифметики (Num), системных вызовов Unix (Unix), нитей (Threads) и динамической загрузки byte–code (Dynlink).

Операции ввода/вывода и системный интерфейс стандартной библиотеки совместимы с различными операционными системами: Unix, Windows, MacOS. Не все библиотеки третьей группы обладают такой особенностью. Также существует множество библиотек поставляемых независимо от дистрибутива Objective CAML.

Использование

Для того, чтобы использовать модуль или библиотеку в программе, используют синтаксис с точкой, указав имя модуля, а затем имя объекта. К примеру, если необходим объект f из модуля Name, то мы пишем Name.f. Чтобы постоянно не добавлять префикс из имени модуля, можно открыть модуль и вызывать f напрямую.




Синтаксис 10cmopen Name

С этого момента, все объявления модуля Name будут рассматриваются как глобальные в текущем окружении. Если какое-то объявление имеет одно и то же имя в двух библиотеках, то имя последней загруженной библиотеки перекрывает предыдущее объявление. Чтобы вызвать первое объявление, необходимо использовать синтаксис с точкой.

7.2  Автоматически загруженные библиотеки

Библиотека Pervasives всегда автоматически подгружается, как в интерактивной среде, так и при компиляции нативным компилятором. Эта библиотека всегда связывается (linked) и является начальным окружением языка. В ней содержатся следующие объявления:

типы
: базовые типы (int, char, string, float, bool, unit, exn, 'a array, 'a list) и типы 'a option (см. стр. ??) и ('a, 'b, 'c) format (см. стр. ??).
исключения
: около десятка исключений, возбуждаемых execution library. Вот наиболее общие из них:
функции
: около 140, половина из них соответствует функциям C из execution library. Среди них мы можем найти арифметические операторы и операторы сравнения, целочисленные функции и функции над числами с плавающей запятой, строковые функции, функции над указателями (references) и ввода/вывода. Необходимо заметить, что некоторые из этих объявлений на самом деле лишь синонимы объявлений из других модулей, но здесь они находятся по историческим и implementation reasons.

7.3  Стандартная библиотека

В стандартной библиотеке сгруппированы стабильные, платформонезависимые модули. На данный момент библиотека содержит 29 модулей, которые в свою очередь состоят из 400 функций, 30 типов, половина из которых абстрактные, 8 исключений, 10 под–модулей и 3 параметризованных модуля. Конечно, мы не будем детально рассматривать все объявления модулей, для этого существует справочное руководство [LRVD99]. Представим лишь модули вводящие новые концепции или сложные в использовании.

Стандартная библиотека может быть разбита на 4 большие части:

линейные структуры данных
(15 модулей), некоторые из них мы уже видели в первой части.
ввод/вывод
(4 модуля), для форматирования вывода, сохраняемость и создание криптографических ключей.
синтаксический и лексический анализ
(4 модуля), их описание дано в главе 10 на стр. ??.
системный интерфейс
при помощи которого мы можем передавать, анализировать параметры передане команде, перемещаться в каталогах и обращаться к файлам.

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

7.3.1  Утилиты

Модули, которые мы назовем <<полезными>>, содержат:

Генератор случайных чисел

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

Ниже приведены функции модуля:

7.3.2  Линейные структуры данных

В следующем списке представлены модули линейных структур данных:

Параметризованные модули построены на основе других модулей, благодаря чему они становятся еще более общими (generic). Создание параметризованных модулей будет представлено в главе 13 на стр. ??.

Линейные структуры простых данных

Имя модуля указывает на структуру данных, которыми он умеет манипулировать. Если тип абстрактный, то есть представление типа спрятано, то в соответствии с принятым соглашением, подобный тип именуется t внутри модуля. Такие модули реализуют следующие структуры:

Укажем так же последний модуль манипулирующий линейными структурами данных:

Семейство общих функций

За исключением модуля Sort, все остальные модули определяют структуры данных, функции создания таких структур и доступа к элементам этих структур, а так же функции манипуляции, включая функцию перевода в другие типы данных. Только модуль List обходится без физического изменения данных. Мы не станем давать полное описание всех функций, ограничимся лишь семейством функций, используемых этими модулями. Модули List и Array будут рассмотрены в деталях, так как это наиболее часто встречающиеся структуры в императивном и функциональном программировании.

Следующие функции можно найти во всех или почти всех модулях:

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

Для структур с индексированными элементами существуют следующие функции.

Модули List и Array

Описывая функции этих библиотек, мы сакцентируем внимание на особенности и общие черты каждой из них. Для функций одинаковых для обоих модулей тип t означает 'a list или a' array. Для функций специфичных одному модулю воспользуемся синтаксисом с точкой.

Схожие или аналогичные функции

Первая из них - подсчет длинны.

lenght:'a t-> int

Две функции для конкатенации двух структур или всех структур списка.

append:'a t-> 'a t-> 'a t
concat:'a t list -> 'a t

Каждый модуль включает функцию доступа к элементу структуры по позиции.

List.nth:'a list -> int -> 'a
Array.get:'a array -> int -> 'a

В связи с тем что Функция для доступа к i-тому элементу вектора t часто используется у нее есть укороченный синтаксис: t.(i).

Две функции, позволяющие выполнить определенное действие (функцию) над всеми элементами структуры.

iter:('a -> unit) -> 'a t -> unit
map:('a -> 'b) -> 'a t -> 'b t

Чтобы вывести все элементы списка или вектора, воспользуемся iter.

# let print_content iter print_item xs = iter (fun x -> print_string"("; print_item x; print_string")") xs; print_newline() ;; val print_content : (('a -> unit) -> 'b -> 'c) -> ('a -> 'd) -> 'b -> unit = <fun> # print_content List.iter print_int [1;2;3;4;5] ;; (1)(2)(3)(4)(5) - : unit = () # print_content Array.iter print_int [|1;2;3;4;5|] ;; (1)(2)(3)(4)(5) - : unit = ()

Функция map создает новую структуру, которая содержит результаты применения функции к элементам списка или вектора. Проверим это на векторе с изменяемым содержимым:

# let a = [|1;2;3;4|] ;; val a : int array = [|1; 2; 3; 4|] # let b = Array.map succ a ;; val b : int array = [|2; 3; 4; 5|] # a, b;; - : int array * int array = [|1; 2; 3; 4|], [|2; 3; 4; 5|]

При помощи следующих итераторов можно создать частичное применение функции для каждого элемента вектора:

fold_left:(('a -> 'b -> 'a) -> 'a -> 'b t -> 'a
fold_right:(('a -> 'b -> 'b) -> 'a t -> 'b -> 'b

Этим итераторам необходимо указать базовый случай со значением по умолчанию, которое будет возвращено в случае если структура пустая.

fold_left f r [v1; v2; ...; vn] = f ... ( f (f r v1) v2 ) ... vn fold_right f [v1; v2; ...; vn] r = f v1 ( f v2 ... (f vn r) ... )

При помощи этих функций можно легко превратить бинарную функцию в n–арную. Для коммутативной и ассоциативной операции итерация слева на право или справа налево одинакова:

# List.fold_left (+) 0 [1;2;3;4] ;; - : int = 10 # List.fold_right (+) [1;2;3;4] 0 ;; - : int = 10 # List.fold_left List.append [0] [[1];[2];[3];[4]] ;; - : int list = [0; 1; 2; 3; 4] # List.fold_right List.append [[1];[2];[3];[4]] [0] ;; - : int list = [1; 2; 3; 4; 0]

Отметим, что пустой лист является нейтральным элементом слева и справа для двоичного сложения. Для этого частного случая следующие выражения эквивалентны:

# List.fold_left List.append [] [[1];[2];[3];[4]] ;; - : int list = [1; 2; 3; 4] # List.fold_right List.append [[1];[2];[3];[4]] [] ;; - : int list = [1; 2; 3; 4]

Таким образом мы “создали” функцию List.concat.

Операции над списками

Следующие полезные функции находятся в модуле List.

List.hd:'a list -> 'a
  первый элемент списка
List.tl:'a list -> 'a
  список, без первого элемента
List.rev:'a list -> 'a list
  список в обратном порядке
List.mem:'a -> 'a list -> bool
  тест на принадлежность списку
List.flatten:'a list list -> 'a list
  “разложить” список списков
List.rev_append:'a list -> 'a list -> 'a list
  то же что и append (rev l1) l2

Первые две функции являются частичными, если им передать пустой список, то исключение Failure будет возбуждено. Существует вариант mem: memq, использующий физическое равенство.

# let c = (1,2) ;; val c : int * int = 1, 2 # let l = [c] ;; val l : (int * int) list = [1, 2] # List.memq (1,2) l ;; - : bool = false # List.memq c l ;; - : bool = true

В модуле List имеется два особенных итератора, обобщающих булеву конъюнкцию и дизъюнкцию (and/or): List.for_all и List.exists определяются следующим образом.

# let for_all f xs = List.fold_right (fun x -> fun b -> (f x) & b) xs true ;; val for_all : ('a -> bool) -> 'a list -> bool = <fun> # let exists f xs = List.fold_right (fun x -> fun b -> (f x) or b) xs false ;; val exists : ('a -> bool) -> 'a list -> bool = <fun>

В модуле List имеется различные варианты итераторов (iter2, map2, etc.), которые принимают на входе два листа и обходят их параллельно. В случае если листы имеют разную длину, возбуждается исключение Invalid_argument.

Следующие функции позволяют осуществлять поиск элемента в списке при помощи критерия в виде булевой функции:

List.find:('a -> bool) -> 'a list -> 'a
List.find_all:('a -> bool) -> 'a list -> 'a list

У функции find_all существует псевдоним filter.

A variant of the general search function is the partitioning of a list:

List.partition:('a -> bool) -> 'a list -> 'a list * 'a list

Часто используемые функции из модуля List для разбиения или создания списка из списка пар:

List.split:('a * 'b) list -> 'a list * 'b list
List.combine:'a list -> 'b list -> ('a * 'b) list

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

List.assoc:'a -> ('a * 'b) list -> 'b
  извлекает информацию связанную с ключом
List.mem_assoc:'a -> ('a * 'b) list -> bool
  проверяет существование ключа
List.remove_assoc:'a -> ('a * 'b) list -> ('a * 'b) list
  удаляет элемент связанный с ключом

У каждой из этих функций существует аналог, использующий физическое равенство вместо структурного: List.assq, List.mem_assq и List.remove_assq

Манипуляции над векторами

Векторы, так часто используемые в императивном программировании, являются физически изменяемыми структурами. В модуле Array имеется функция для изменения значения элемента:

Array.set:'a array -> int -> 'a -> unit

Как и функция get, у функции set имеется укороченная запись: t.(i) <- a

Существует 3 функции выделения памяти под вектор:

Array.create:int -> 'a -> 'a array
  создать вектор заданного размера, все элементы которого инициализированы одинаковым значением
Array.make:int -> 'a -> 'a array
  укороченная запись для create
Array.init:int -> (int -> 'a) -> 'a array
  создать вектор заданного размера, все элементы которого инициализированы результатом функции от индекса инициализируемого элемента

Так как матрицы это часто встречающаяся структура, в модуле Array имеется две функции создания матриц:

Array.create_matrix:int -> int -> 'a -> 'a array array
Array.make_matrix:int -> int -> 'a -> 'a array array

Функция set превращается в функцию изменяющую значения интервала, который указан индексом началом и длиной:

Array.fill:'a array -> int -> int -> 'a -> unit

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

Array.copy:'a array -> 'a array
Array.sub:'a array -> int -> int -> 'a array

Копия или вырезка могут реализовываться и в существующий вектор:

Array.blit:'a array -> int -> 'a array -> int -> int -> unit

Первый целочисленных аргумент является индексом в первом векторе, второй аргумент - индекс второго вектора и третий число копируемых элементов. Функции blit, sub и fill возбуждают исключения Invalid_argument.

В связи с тем что к элементам массива обычно доступаются при помощи индекса, были определены следующие два итератора:

Array.iteri:(int -> 'a -> unit) -> 'a array -> unit
Array.mapi:(int -> 'a -> 'b) -> 'a array -> 'b array

Они применяют функцию, первый аргумент которой является индекс желаемого элемента.

# let f i a = (string_of_int i) ^ ":" ^ (string_of_int a) in Array.mapi f [| 4; 3; 2; 1; 0 |] ;; - : string array = [|"0:4"; "1:3"; "2:2"; "3:1"; "4:0"|]

В модуле Array не имеется функции–аналога map для списков, которая бы могла изменить содержимое каждой ячейки вектора на результат функции от значения этой ячейки. Но мы можем получить тоже самое при помощи функции iteri:

# let iter_and_set f t = Array.iteri (fun i -> fun x -> t.(i) <- f x) t ;; val iter_and_set : ('a -> 'a) -> 'a array -> unit = <fun> # let v = [|0;1;2;3;4|] ;; val v : int array = [|0; 1; 2; 3; 4|] # iter_and_set succ v ;; - : unit = () # v ;; - : int array = [|1; 2; 3; 4; 5|]

И наконец, модуль Array предоставляет две функции для создания списка из вектора и наоборот.

Array.of_list:a list -> 'a array
Array.to_list:a array -> 'a list

7.3.3  Ввод/Вывод

В стандартой библиотеке имеется четыре модуля ввода–вывода

Детали модуля Marshal будут приведены позже в этой главе, когда мы займемся обработкой постоянных данных (persistent data structures) (см. стр. ??).

Модуль Printf

При помощи модуля Printf мы можем форматировать текст на манер функции printf из стандартной библиотеки языка C. Желаемый формат представлен строкой, которая будет обработана в соответствии со стандартами функции printf, то есть используя символ %. Этот символ, если за ним следует буква, определяет тип данных на данной позиции. Следующая запись “(x=%d, y=%d)” означает что вместо %d нужно вывести два целых числа.

Спецификация формата

Формат указывает параметры для выводимой строки. Базовые типы: int, float, char и string будут преобразованы в строки и подставлены на их места в распечатываемой строке. В результате передачи значений 77 и 43 формату “(x=%d, y=%d)” получим строку “(x=77, y=43)”. Основные символы, определяющие тип преобразования приведены в таблице 7.1.


ТипСимволРезультат
целоеd или iдесятичное со знаком
 uдесятичное без знака
 xшестнадцатеричное без знака в нижнем регистре
 Xто же самое в верхнем регистре
символcсимвол
строкаsстрока
действительноеfдесятичное
 e или Eэкспоненциальная запись
 g или Gто же самое
логическийbtrue или false
специальныйa или tфункциональный параметр
  типа (out_channel -> 'a -> unit) -> 'a -> unit
  или out_channel -> unit
Table 7.1: Символы преобразования типов

В формате можно так же установить выравнивание выводимых строк. Для этого необходимо указать размер в символах преобразования типа. Делается это так: между символом % и типом преобразования вставляется число, например %10d, после чего мы получаем выравнивание справа по десяти символам. Если размер результата превосходит размер ограничения, то это ограничение будет проигнорировано. Отрицательное значение реализует выравнивание слева. Для перевода действительных чисел стоит указать точность результата. В этом случае после % вставим точку и целое число. Следующая запись %.5f указывает, что мы желаем пять чисел после запятой.

Существует два специальных формата: a и t, они указывают функциональный аргумент. Typically, a print function defined by the user. В этом заключается специфичность Objective CAML.

Функции модуля

Тип пяти функций модуля Printf приведен в таблице 7.2.


fprintf:out_channel -> ('a, out_channel, unit) format -> 'a
printf:('a, out_channel, unit) format -> 'a
eprintf:('a, out_channel, unit) format -> 'a
sprintf:('a, unit, string) format -> 'a
bprintf:Buffer.t -> ('a, Buffer.t, string) format -> 'a
Table 7.2: Функции форматирования printf

Функции fprintf ожидает три параметра: имя канала, формат и аргументы с типами, описанными форматом. Функции еprintf и printf есть специализированные версии для стандартного выхода и стандартного выхода ошибок. И наконец, sprintf и bprintf не выводят результат на экран, а записывают его в строку.

Вот несколько примеров использования различных форматов:

# Printf.printf "(x=%d, y=%d)" 34 78 ;; (x=34, y=78)- : unit = () # Printf.printf "name = %s, age = %d" "Patricia" 18 ;; name = Patricia, age = 18- : unit = () # let s = Printf.sprintf "%10.5f\n%10.5f\n" (-.12.24) (2.30000008) ;; val s : string = " -12.24000\n 2.30000\n" # print_string s ;; -12.24000 2.30000 - : unit = ()

В следующем примере, создадим функцию для распечатки матрицы вещественных чисел в заданном формате:

# let print_mat m = Printf.printf "\n" ; for i=0 to (Array.length m)-1 do for j=0 to (Array.length m.(0))-1 do Printf.printf "%10.3f" m.(i).(j) done ; Printf.printf "\n" done ;; val print_mat : float array array -> unit = <fun> # print_mat (Array.create 4 [| 1.2; -.44.22; 35.2 |]) ;; 1.200 -44.220 35.200 1.200 -44.220 35.200 1.200 -44.220 35.200 1.200 -44.220 35.200 - : unit = ()
Замечания по типу формата

Описание формата задается в виде символьной строки, но оно не является типом string. При распознавании формата создается значение с типом format, в котором параметр 'a реализуется либо в unit, если формат не указывает параметр, либо в функциональный тип, который соответствует функции с нужным количеством аргументов и возвращаюшей значение unit.

Проиллюстрируем это на примере, частично применив функцию к формату.

# let p3 = Printf.printf "begin\n%d is val1\n%s is val2\n%f is val3\n" ;; begin val p3 : int -> string -> float -> unit = <fun>

Таким образом мы получили функцию ожидающую три аргумента. Заметьте, что слово “begin” уже было выведено. Другой формат приводит к созданию функции другого типа.

# let p2 = Printf.printf "begin\n%f is val1\n%s is val2\n";; begin val p2 : float -> string -> unit = <fun>

Передавая аргументы, одни за одним функции p3 получим постепенный результат:

# let p31 = p3 45 ;; 45 est val1 val p31 : string -> float -> unit = <fun> # let p32 = p31 "hello" ;; hello est val2 val p32 : float -> unit = <fun> # let p33 = p32 3.14 ;; 3.140000 est val3 val p33 : unit = () # p33 ;; - : unit = ()

Последнее получено значение ничего не выводит, оно возвращает значение () типа unit.

Мы не сможем создать значение format используя значения типа string:

# let f d = Printf.printf (d^d);; Characters 27-30: This expression has type string but is here used with type ('a, out_channel, unit) format

Компилятор не знает значение строки, переданной в аргументе, в следствии чего он не знает тип который реализует параметр 'a типа format.

С другой стороны, строки символов являются физически изменяемым типом, то есть мы можем поменять %d на другую букву, таким образом динамически изменить формат вывода. Это не совместимо со статической генерацией функции перевода.

Модуль Digest

Из символьной строки, какого угодно размера, хэш функции возвращает строку фиксированного размера, чаще всего меньшего размера. Такие функции возвращают отпечаток (digest).

Такие функции используются для создания хэш таблиц, как на пример модуль Hashtbl, позволяющий проверить присутствует ли определенный элемент в таблице прямым доступом (direct access) используя отпечаток. Например, функция f_mod_n подсчитывает остаток от деления суммы кодов сомволов ASCII строки на n и является хэш функция. Если мы создадим таблицу размером n, то благодаря отпечаткам получим прямой доступ к элементам. Однако, две строки могут иметь одинаковый отпечаток. В случае подобных коллизий, в хэш таблицу добавляется дополнительное “хранилище” для хранения таких элементов. Когда таких коллизий становится много, доступ к таблице становится малоэффективным. Если n — размер отпечатка a, тогда вероятность возникновения коллизий между двумя разными строка равна 1/2n.

Вероятность возникновения коллизий у non–reversible хэш функции очень мала, таким образом трудно подобрать строку к конкретному отпечатку. Конечно, функция f_mod_n таковой не является. Таким образом non-reversible хэш функции позволяют идентифицировать (authentification) символьную строку полученную по Интернету, файл, и т.д.

Модуль Digest использует алгоритм MD5, как аббревиатура Message Digest 5. Размер возвращаемого отпечатка 128 бит. Несмотря на то, что алгоритм свободно доступен, на сегодняшний день невозможно создать строку имея отпечаток. В этом модуле определен тип Digest.t как аббревиатура string. В таблице 7.3 приведены основные функции этого модуля.


string:string -> t
  возвращает отпечаток строки
file:string -> t
  возвращает отпечаток файла
Table 7.3: Функции модуля Digest

В следующем примере мы используем функцию string на небольшой строке и на большой строке, построенной из первой. Заметьте, что размер отпечатка остается такой же:

# let s = "The small cat is dead...";; val s : string = "The small cat is dead..." # Digest.string s;; - : Digest.t = "xr6\127\171(\134=\238`\252F\028\t\210$" # let r = ref s in for i=1 to 100 do r:= s^ !r done; Digest.string !r;; - : Digest.t = "\232\197|C]\137\180{>\224QX\155\131D\225"

Применяя отпечатки программ, мы тем самым гарантируем, что версия используемой программы правильная. К примеру, во время динамической загрузки кода (см. стр. ??), используются отпечатки для загрузки файла с байт–кодом.

# Digest.file "basic.ml" ;; - : Digest.t = "\179\026\191\137\157Ly|^w7\183\164:\167q"

7.3.4  Persistence

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

Реализация и трудности линеаризации

При реализации механизма линеаризации данных необходимо сделать определенный выбор и встречаются следующие трудности:

чтение запись структур данных
Память компьютера может быть рассмотрена как вектор байтов, тогда значение можно представить как часть памяти которое оно занимает. В этом случае мы можем сжать значение, сохранив лишь полезную часть.
разделение или копия
При линеаризации данных нужно ли сохранять разделение (sharing). Типичный случай - бинарное дерево, у которого имеется два одинаковых (в физическом смысле) узла, здесь мы можем указать для второго узла, что он уже сохранен. Это свойство влияет на размер сохраняемых данных и на затраченное время. On the other hand, in the presence of physically modifiable values, this could change the behavior of this value after a recovery depending on whether or not sharing was conserved.
циклические структуры
В подобном случае, линеаризация без разделения рискует зациклиться. Таким образом необходимо сохранить это разделение.
функциональные значения
Функциональные значения, или замыкания, состоят из двух частей: окружения и кода. Часть кода состоит из адреса по которому находится код. Что в данном случае происходит с кодом? Мы конечно можем сохранить этот адрес, но в этом случае только та же программа сможет правильно распознать этот адрес. Мы так же можем сохранить последовательность инструкций этой функции, однако нам будет необходим механизм динамической загрузки кода.
гарантия типа при загрузке
В этом заключается основная трудность данного механизма. Статическая типизация гарантирует что значения не создадут ошибок типа во время выполнения, но это касается лишь значений принадлежащих исполняющейся программе. Какой тип нужно дать внешнему, по отношению к программе, значению, которое не было проверено анализатором типов. Лишь только для того чтобы, прочитаное значение имело мономорфный тип сгенерированный компилятором, необходима передача этого значения в момент сохранения и проверка в момент чтения.

Модуль Marshal

Механизм линеаризации модуля Marshal предоставляет нам выбор: сохранить или нет разделение для обрабатываемых значений. Он так же умеет обращаться с замыканиями, но здесь сохраняется лишь указатель на адрес по которому находится код.

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

type external_flag = No_sharing | Closures;;

Константный конструктор No_sharing указывает что разделение значений не должно быть сохранено, так как по умолчанию разделение сохраняется. Конструктор Closures необходим когда мы имеем дело с замыканиями, в этом случае сохраняется указатель на код. Если этот конструктор отсутствует и мы попытаемся сохранить функциональное значение, то будет возбуждено исключение.


Warning
Конструктор Closures не может использоваться в интерактивном режиме, а лишь в командной строке.
Функции записи и чтения данного модуля приведенные в таблице 7.4.


to_channel:out_channel -> 'a -> extern_flag list -> unit
to_string:'a -> extern_flag list -> string
to_buffer:string -> int -> int -> 'a -> extern_flag list -> unit
from_channel:in_channel -> 'a
from_string:string -> int -> 'a
Table 7.4: Функции модуля Marshal

У функции to_channel три аргумента: выходной канал, значение и список опций, она записывает переданное значение в указанный канал. Функция to_string создает строку, которая соответствует линеаризации указанного значения, тогда как to_buffer делает то же самое, изменяя часть строки переданной в аргументе. Функция from_channel возвращает прочитанное из канала линеаризованное значение. У функции from_string два аргумента: строка из которой необходимо прочитать значение и позиция в строке с которого начинается это значение. В файле или строке можно сохранить Несколько линеаризованных значений. В первом случае такие значения читаются последовательно,во втором достаточно указать правильный отступ от начала строки, чтобы прочитать необходимое значение.

# let s = Marshal.to_string [1;2;3;4] [] in String.sub s 0 10;; - : string = "\132\149\166\190\000\000\000\t\000\000"


Warning
Использование данного модуля приводит к потере гарантии статической типизации (см. стр. ??)
При перечитывании сохраняемого объекта получаем значение неопределенного типа:

# let x = Marshal.from_string (Marshal.to_string [1; 2; 3; 4] []) 0;; val x : '_a = <poly>

Эта неопределенность отмечена в Objective CAML переменной нестрого типа 'a. Поэтому желательно указать ожидаемый тип:

# let l = let s = (Marshal.to_string [1; 2; 3; 4] []) in (Marshal.from_string s 0 : int list) ;; val l : int list = [1; 2; 3; 4]

На странице ?? мы вернемся к этому моменту.


Замечание
Функция output_value подгружаемой библиотеки соответствует вызову to_channel с пустым списком опций. Функция input_value модуля Pervasives вызывает функцию from_channel. Они сохранены для совместимости со старыми программами.

Пример: сохранение экрана

Наша задача — сохранить bitmap всего экрана в виде матрицы цветов. Функция save_screen забирает bitmap, конвертирует его в вектор цветов и сохраняет в файле, имя которого преданно функции.

# let save_screen name = let i = Graphics.get_image 0 0 (Graphics.size_x ()) (Graphics.size_y ()) in let j = Graphics.dump_image i in let oc = open_out name in output_value oc j; close_out oc;; val save_screen : string -> unit = <fun>

Функция load_screen выполняет обратную операцию; открывает файл, имя которого указанно, считывает сохраненные данные, конвертирует полученную матрицу цветов в bitmap и затем рисует на экране.

# let load_screen name = let ic = open_in name in let image = ((input_value ic) : Graphics.color array array) in close_in ic; Graphics.close_graph(); Graphics.open_graph (" "^(string_of_int(Array.length image.(0))) ^"x"^(string_of_int(Array.length image))); let image2 = Graphics.make_image image in Graphics.draw_image image2 0 0; image2 ;; val load_screen : string -> Graphics.image = <fun>


Warning
Значения абстрактных типов не могут быть сохраняемыми.
По этой причине в предыдущем примере не используется абстрактный тип Graphics.image, а конкретный тип color array array. Абстракция типов обсуждается в главе 13.

Разделение

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

# let reset_s,new_s = let c = ref 0 in ( function () -> c := 0 ) , ( function s -> c:=!c+1; s^(string_of_int !c) ) ;; # let save = Marshal.to_string (new_s,reset_s) [Marshal.Closures;Marshal.No_sharing] ;; # let (new_s1,reset_s1) = (Marshal.from_string save 0 : ((string -> string ) * (unit -> unit))) ;; (* 1 *) Printf.printf "new_s : \%s\n" (new_s "X"); Printf.printf "new_s : \%s\n" (new_s "X"); (* 2 *) Printf.printf "new_s1 : \%s\n" (new_s1 "X"); (* 3 *) reset_s1(); Printf.printf "new_s1 (after reset_s1) : \%s\n" (new_s1 "X") ;; Characters 148-154: Unbound value new_s1

Первые два вывода (* 1 *) выводят правильный результат. Вывод (* 2 *), после прочтения замыкания тоже кажется корректным (после X2 следует X3). Но на самом деле разделение счетчика c между функциями new_s1 reset_s1 утеряно. Об этом мы можем судить по выводу X4, несмотря на то что мы обнулили перед этим счетчик. Каждое из замыканий имеет свой собственный счетчик и вызов reset_s1 не инициализирует счетчик new_s1. То есть мы не должны были указывать опцию No_sharing во время линеаризации.

Данный пример не работает, код соответствующий приведенному выше тексту следующий (прим. пер.):

let reset_s,new_s = let c = ref 0 in ( function () -> c := 0 ) , ( function s -> c:=!c+1; s^(string_of_int !c) ) ;; (* 1 *) Printf.printf "new_s : %s\n" (new_s "X");; (* 2 *) Printf.printf "new_s : %s\n" (new_s "X");; let save = Marshal.to_string (new_s,reset_s) [Marshal.Closures;Marshal.No_sharing] ;; let (new_s1,reset_s1) = (Marshal.from_string save 0 : ((string -> string ) * (unit -> unit))) ;; (* 3 *) Printf.printf "new_s1 : %s\n" (new_s1 "X");; reset_s1();; Printf.printf "new_s1 (after reset_s1) : %s\n" (new_s1 "X") ;;

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

# let copy_mat_f (m : float array array) = let s = Marshal.to_string m [Marshal.No_sharing] in (Marshal.from_string s 0 : float array array);; val copy_mat_f : float array array -> float array array = <fun>

Мы можем воспользоваться этой функцией для создания матрицы без разделения:

# let create_mat_f n m v = let m = Array.create n (Array.create m v) in copie_mat_f m;; val create_mat_f : int -> int -> float -> float array array = <fun> # let a = create_mat_f 3 4 3.14;; val a : float array array = [|[|3.14; 3.14; 3.14; 3.14|]; [|3.14; 3.14; 3.14; 3.14|]; [|3.14; 3.14; 3.14; 3.14|]|] # a.(1).(2) <- 6.28;; - : unit = () # a;; - : float array array = [|[|3.14; 3.14; 3.14; 3.14|]; [|3.14; 3.14; 6.28; 3.14|]; [|3.14; 3.14; 3.14; 3.14|]|]

Поведение этой функции больше сходит на Array.create_matrix чем Array.create.

Размер значений

Порой необходимо бывает знать размер сохраняемого значения. Если разделение сохранено, то указанный размер достаточно точно отображает информацию. Хоть кодировка порой оптимизирует размер непосредственных 2 (или элементарных) значений, информация о размере их кодировки позволяет нам сравнить различные реализации структур данных. С другой стороны, для программ, которые выполняются без остановки, как во встроенных системах или даже серверах, просмотр размер структур данных помогает отловить утечки памяти. Две функции для вычисления размера и константа модуля Marshal описаны в таблице 7.5.


header_size:int
data_size:string -> int -> int
total_size:string -> int -> int
Table 7.5: Функции модуля Marshal

Размер сохраняемого значения есть сумма размера его данных и размер заголовка.

В следующем примере мы приводим сравнение двух бинарных деревьев при помощи функции MD5:

# let size x = Marshal.data_size (Marshal.to_string x []) 0;; val size : 'a -> int = <fun> # type 'a bintree1 = Empty1 | Node1 of 'a * 'a bintree1 * 'a bintree1 ;; type 'a bintree1 = | Empty1 | Node1 of 'a * 'a bintree1 * 'a bintree1 # let s1 = Node1(2, Node1(1, Node1(0, Empty1, Empty1), Empty1), Node1(3, Empty1, Empty1)) ;; val s1 : int bintree1 = Node1 (2, Node1 (1, Node1 (0, Empty1, Empty1), Empty1), Node1 (3, Empty1, Empty1)) # type 'a bintree2 = Empty2 | Leaf2 of 'a | Node2 of 'a * 'a bintree2 * 'a bintree2 ;; type 'a bintree2 = | Empty2 | Leaf2 of 'a | Node2 of 'a * 'a bintree2 * 'a bintree2 # let s2 = Node2(2, Node2(1, Leaf2 0, Empty2), Leaf2 3) ;; val s2 : int bintree2 = Node2 (2, Node2 (1, Leaf2 0, Empty2), Leaf2 3) # let s1, s2 = size s1, size s2 ;; val s1 : int = 13 val s2 : int = 9

Полученые при помощи функции size значения совпадают с предполагаемым размером деревьев s1 и s2.

Проблемы типизации

Действительной проблемой сохраняемых значений является возможность нарушить механизм типизации Objective CAML. Функции записи создают правильный мономорфный тип: unit и string. Тогда как функции чтения возвращают полиморфный тип 'a. С таким сохранянемым значением мы можем сделать что угодно. Вот самый худший вариант (см. гл. 1 на стр. ??): создание функции magic_copy с типом 'a -> 'b.

# let magic_copy a = let s = Marshal.to_string a [Marshal.Closures] in Marshal.from_string s 0;; val magic_copy : 'a -> 'b = <fun>

Использование подобной функции приведет к аварийной остановке программы:

(magic_copy 3 : float) +. 3.1;; Segmentation fault

В интерактивной среде (в Линуксе) текущая сессия заканчивается с сообщением об ошибке памяти.

7.3.5  Системный интерфейс

В стандартной библиотеке содержится 6 модулей системного интерфейса:

Описание четырех первых модулей приведено ниже.

Модуль Sys

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


OS_type:string
 :тип системы
interactive:bool ref
 :истина, если интерактивный режим
word_size:string
 :размер слова (32 или 64 бита)
max_string_length:int
 :максимальный размер символьной строки
max_array_length:int
 :максимальный размер вектора
time:unit -> float
 :время в секундах, истекшее с начала выполнения программы
Table 7.6: Данные об операционной системе

Связь между программой и операционной системой осуществляется через командную строку, переменную окружения и запуском другой программы. Эти функции описаны в таблице 7.7.


argv:string array
 :содержит вектор параметров
getenv:string -> string
 :получить значение переменной окружения
command:string -> int
 :выполнения комманды с указанным именем
Table 7.7: Связь с операционной системой

При помощи Функции в таблице 7.8 можно “использовать” файловую систему.


file_exists:string -> bool
 :возвращает истина, если файл существует
remove:string -> unit
 :удалить файл
rename:string -> string -> unit
 :переименовать файл
chdir:string -> unit
 :сменить текущий каталог
getcwd:unit -> string
 :вернуть имя текущего каталога
Table 7.8: Операции с файловой системой

Обработка сигналов будет описано в специальной главе о системном программировании (17).

Напишем следующую программу, в ней мы вновь вернемся к примеру с сохранением графического окна в виде вектора цветов. Функция main проверяет не запущена ли она в интерактивном режиме, затем считывает имена файлов в командной строке, проверяет существуют ли они и затем выводит их на экран (при помощи функции load_screen). Между двумя выводами, мы вставляем ожидание нажатия на клавишу.

# let main () = if not (!Sys.interactive) then for i = 0 to Array.length(Sys.argv) -1 do let name = Sys.argv.(i) in if Sys.file_exists name then begin ignore(load_screen name); ignore(Graphics.read_key) end done;; val main : unit -> unit = <fun>

Модуль Arg

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

Элементы командой строки разделены между собой одним или несколькими пробелами. Все они хранятся в векторе Sys.argv. В синтаксисе определенном Sys.argv некоторые элементы начинаются знаком минус ('-'). Такие элементы называются ключевыми словами командной строки. Ключевым словам можно привязать определенное действие, которое ожидает аргумент с типом string, int или float. Значения аргументов инициализируются значениями командной строки, которые непосредственно следуют за ключевым словом. В таком случае вызывается функция перевода значения из строки в ожидаемый тип данных. Другие аргументы командной строки называются анонимными. Им ассоциируется общее действие, которое принимает их значение в виде аргумента. Если указана неопределенная опция, то на экран выводится небольшая помощь, которая определяется пользователем.

Действия, связанные с ключевыми словами определены следующим типом:

type spec = | Unit of (unit -> unit) (* Call the function with unit argument*) | Set of bool ref (* Set the reference to true*) | Clear of bool ref (* Set the reference to false*) | String of (string -> unit) (* Call the function with a string argument *) | Int of (int -> unit) (* Call the function with an int argument *) | Float of (float -> unit) (* Call the function with a float argument *) | Rest of (string -> unit) (* Stop interpreting keywords and call the function with each remaining argument*)

Для анализа командной строки существует следующая функция:

# Arg.parse ;; - : (string * Arg.spec * string) list -> (string -> unit) -> string -> unit = <fun>

Первым аргументом является список кортежей формы (key, spec, doc), где:

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

В модуле Arg имеется так же:

Примера ради, напишем функцию read_args, которая инициализирует игру Minesweeper, представленую в главе 5, страница ??. Возможные опции будут следующими: -col, -lin и -min. За ними буду следовать целые числа, указывающие число столбцов, число линий и число мин соответственно. Эти значения не должны быть меньше значений по умолчанию: 10,10 и 15.

Для этого создадим три функции обработки:

# let set_nbcols cf n = cf := {!cf with nbcols = n} ;; # let set_nbrows cf n = cf := {!cf with nbrows = n} ;; # let set_nbmines cf n = cf := {!cf with nbmines = n} ;;

У всех этих функций одинаковый тип: config ref -> int -> unit. Функции анализа командной строки можно написать следующимобразом:

# let read_args() = let cf = ref default_config in let speclist = [("-col", Arg.Int (set_nbcols cf), "number of columns (>=10)"); ("-lin", Arg.Int (set_nbrows cf), "number of lines (>=10)"); ("-min", Arg.Int (set_nbmines cf), "number of mines (>=15)")] in let usage_msg = "usage : minesweep [-col n] [-lin n] [-min n]" in Arg.parse speclist (fun s -> ()) usage_msg; !cf ;; val read_args : unit -> config = <fun>

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

Модуль Filename

В модуле Filename реализованы операции по доступу к файловой системе, независимо от операционной системы. Правила по которым называются имена в Windows, Unix и MacOS сильно различаются.

Модуль Printexc

3том совсем небольшой модуле из трех функций (таблица 7.9) предоставляет нам сборщик исключений. Он очень удобен, особенно для программ запускаемых с командной строки3, чтобы быть уверенным в том что ни одно исключение не будет упущено и это не приведет к остановке программы.


catch:('a -> 'b) -> 'a -> 'b
 :сборщик исключений
print:('a -> 'b) -> 'a -> 'b
 :вывести и пере–возбудить исключение
to_string:exn -> string
 :перевести исключение в строку
Table 7.9: Сборщик исключений

Функция catch применяет свой первый аргумент ко второму, это приведет к запуску основной функции программы. Если исключение будет возбуждено на уровне catch, то есть не выловленное внутри программы, то catch выведет свое имя и прекратит программу. Функция print ведет себя схожим образом, с разницей в том что после вывода на экран она снова возбуждает исключение. И наконец функция to_string переводит исключение в строку символов. Она используется двумя предыдущими функциями. Если бы мы переписывали программу просмотра bitmap, то мы бы скрыли функцию main внутри функции go:

# let go () = Printexc.catch main ();; val go : unit -> unit = <fun>

Это позволит нормально закончить программу и вывести при этом значение неотловленного исключения.

7.4  Другие библиотеки дистрибутива

Другие библиотеки, входящие в дистрибутив Objective CAML, затрагивают следующие расширения:

графика
состоящий из независящего от платформы модуля Graphics, который был описан в главе 4.
точная арифметика
несколько модулей для выполнения точных расчетов с целыми и рациональными числами. Числа представлены в виде целых Objective CAML когда это возможно.
фильтрация регулярных выражений
облегчает анализ строк и текста. Модуль Str будет описан в главе 10.
системные вызовы Unix
при помощи модуля Unix этой библиотеки можно вызывать из Objective CAML системные вызовы Unix. Большая часть этой библиотеки совместима с системой Windows. Мы воспользуеся этой библиотекой в главах 17 и 19
“легковесный” процесс
несколько модулей, которые будут подробно описаны в главе 18.
доступ к базам данных NDBD
работает только в Unix и не будет описан.
динамическая загрузка кода
состоит из одного модуля Dynlink.

При помощи приведенных примеров мы опишем бибиблиотеки для работы с большими числами и динамической загрузки.

7.4.1  Точная арифметика

Данная библиотека обеспечивает точную арифметику при работе с большими числами; целыли или рациональными. У значений типа int и float существует предел. Вычисления целых чисел реализуются по модулю самого большого положительного числа, что может привести к незамеченному переполнению. Результ вычисления чисел с плавающей запятой округляется, при распространении этот феномен также может привести к ошибкам. Данная библиотека смягчает выше указанные недостатки.

Эта библиотека написана на языке C, поэтому нам необходимо создать новую интерактивную среду, в которую включен следующий код:

ocamlmktop -custom -o top nums.cma -cclib -lnums

Билиотека состоит из несколькох модулей, два наиболее важных из них это Num для всех операций и Arith_status для контроля опций расчета. Общий тип num является тип сумма группирующая три следующих типа:

type num = Int of int | Big_int of big_int | Ratio of ratio

Типы big_int и ratio являются абстрактными.

После операций со значениями типа num ставится символ '/'. Например сложение двух значений num запишется как +/, тип этой операции num -> num -> num. Для операций сравнения применяется аналогичное правило. Вслеующем примере, мы вычисляем факториал:

# let rec fact_num n = if Num.(<=/) n (Num.Int 0) then (Num.Int 1) else Num.( */ ) n (fact_num ( Num.(-/) n (Num.Int 1)));; val fact_num : Num.num -> Num.num = <fun> # let r = fact_num (Num.Int 100);; val r : Num.num = Num.Big_int <abstr> # let n = Num.string_of_num r in (String.sub n 0 50) ^ "..." ;; - : string = "93326215443944152681699238856266700490715968264381..."

Предварительная загрузка модуля Num облегчает читаемость кода:

# open Num ;; # let rec fact_num n = if n <=/ (Int 0) then (Int 1) else n */ (fact_num ( n -/ (Int 1))) ;; val fact_num : Num.num -> Num.num = <fun>

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

e=
 
lim
m → ∞
(1+
1
m
)m

напишем такую функцию:

# let calc_e m = let a = Num.(+/) (Num.Int 1) ( Num.(//) (Num.Int 1) m) in Num.( **/ ) a m;; val calc_e : Num.num -> Num.num = <fun> # let r = calc_e (Num.Int 100);; val r : Num.num = Ratio <abstr> # let n = Num.string_of_num r in (String.sub n 0 50) ^ "..." ;; - : string = "27048138294215260932671947108075308336779383827810..."

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

# Arith_status.arith_status();; Normalization during computation --> OFF (returned by get_normalize_ratio ()) (modifiable with set_normalize_ratio <your choice>) Normalization when printing --> ON (returned by get_normalize_ratio_when_printing ()) (modifiable with set_normalize_ratio_when_printing <your choice>) Floating point approximation when printing rational numbers --> OFF (returned by get_approx_printing ()) (modifiable with set_approx_printing <your choice>) Error when a rational denominator is null --> ON (returned by get_error_when_null_denominator ()) (modifiable with set_error_when_null_denominator <your choice>) - : unit = ()

В зависимости от необходимости, эти опции могут быть изменены. К примеру если мы желаем выводить на экран приближенное значение предыдущего расчета:

# Arith_status.set_approx_printing true;; - : unit = () # Num.string_of_num (calc_e (Num.Int 100));; - : string = "0.270481382942e1"

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

7.4.2  Динамическая загрузка кода

Для динамической загрузки программ, в виде байт–кода, существует библиотека Dynlink. Преимущества у динамической загрузки кода следующие:

Интерактивная среда Objective CAML использует подобный механизм, почему бы и разработчик на Objective CAML не имел подобных удобств.

Во время загрузки объектного файла (с расширением .cmo), вычисляются различные выражения. Программа, которая загружает модуль, не имеет доступа к именам модуля, поэтому обновление таблицы имен основной программы лежит на ответственности загружаемого модуля.


Warning
Динамическая загрузка кода может быть действует лишь для байт–код объектов.

Описание модуля

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


init:unit -> unit
 :инициализация динамической загрузки
add_interfaces:string list -> string list -> unit
 :добавление имен модулей и путь по которому находится стандартная библиотека
loadfile:string -> unit
 :загрузка байт–код файла
clear_avalaible_units:unit -> unit
 :удаляет имена модулей и путь по которому находится стандартная библиотека
add_avalaibl_units:(string * Digest.t) list -> unit
 :добавить имя модуля и отпечаток для загрузки, без интерфейсного файла
allow_unsafe_modules:bool -> unit
 :загружать файлы содержащие внешние объявления
loadfile_private:string -> unit
 :загруженный модуль не будет доступен для следующих загруженных модулей
Table 7.10: Функции модуля Dynlink

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

Пример

Для того, чтобы проиллюстрировать динамическою загрузку, создадим три модуля:

Модуль F определен в файле f.ml:

let g () = print_string "I am the 'f' function by default\n" ; flush stdout ;; let f = ref g ;;

Модуль Mod1 определен в файле mod1.ml:

print_string "The 'Mod1' module modifies the value of 'F.f'\n" ; flush stdout ;; let g () = print_string "I am the 'f' function of module 'Mod1'\n" ; flush stdout ;; F.f := g ;;

Модуль Mod2 определен в файле mod1.m2:

print_string "The 'Mod2' module modifies the value of 'F.f'\n" ; flush stdout ;; let g () = print_string "I am the 'f' function of module 'Mod2'\n" ; flush stdout ;; F.f := g ;;

И наконец определим основную программу в файле main.ml, в которой вызовем функцию на которую указывает F.f, затем загрузим модуль Mod1, снова вызовем функцию F.f, загрузим модуль Mod2 и вызовем в последний раз F.f.

let main () = try Dynlink.init () ; Dynlink.add_interfaces [ "Pervasives"; "F" ; "Mod1" ; "Mod2" ] [ Sys.getcwd() ; "/usr/local/lib/ocaml/" ] ; !(F.f) () ; Dynlink.loadfile "mod1.cmo" ; !(F.f) () ; Dynlink.loadfile "mod2.cmo" ; !(F.f) () with Dynlink.Error e -> print_endline (Dynlink.error_message e) ; exit 1 ;; main () ;;

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

Скомпилируем созданные файлы.

\begin{lstlisting} $ ocamlc -c f.ml $ ocamlc -o main dynlink.cma f.cmo main.ml $ ocamlc -c f.cmo mod1.ml $ ocamlc -c f.cmo mod2.ml

Вызов программы main даст следующий результат:

$ main I am the 'f' function by default The 'Mod1' module modifies the value of 'F.f' I am the 'f' function of module 'Mod1' The 'Mod2' module modifies the value of 'F.f' I am the 'f' function of module 'Mod2'

Во время динамической загрузки, происходит выполнения кода. Это проиллюстрированно выводом на экран строк начинающихся на “The 'Mod…”. Возможные побочные эффекты данного модуля наследуются основной программой. По этой причине различные вызовы функции F.f приводят к разным вызовам функций.

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

7.5  Упражнения

7.6  Резюме

В этой главе мы ознакомились с различными библиотеками стандартной библитеки Objective CAML. Библиотеки представляют из себя множество простых модулей (или отдельных элементов компиляции). Здесь мы детально рассмотрели модули форматированного вывода (Printf), сохраняемости значений (Marshal), системного интерфейса (Sys) и отлавливания исключений (Printexc). Модули систаксического анализа, управления памятью, системное и сетевое програмирование, легковесные процессы будут рассмотрены в следующих главах.


1
В JAVA используется термин сериализация (serialization)
2
Например вектор символов
3
В интерактивном режиме есть свой сборщик, который выводит сообщение о неотловленом исключении

Previous Up Next