Дисхлюймер Это не учебник по OСaml, не «вводная статья» или что-то другое в этом роде. Это обычные «путевые» заметки нужные, чтобы было над чем посмеяться, оглянувшись назад через месяц-другой :)
В Caml встроены типы int – целое число и float – число с плавающей точкой. Все типы знаковые – беззнаковых встроенных типов нет. int соответствует обычному типу int – как в C++. Правда, Caml нагло хавает у значений типа int 1 бит для собственных нужд – поэтому диапазон целых чисел у Caml меньше, чем в C++: у C++ что-то около 2 миллиардов с каждого конца числовой прямой, а у Caml что-то около миллиарда. Также целые числа в Caml храняется в немного преобразованном виде – поэтому целочисленные вычисления в Caml медленнее, чем в C++. Но, конечно, ценность этого языка не в скорости обработки целых чисел.
И для целых и для дробных чисел определены стандартные арифметические операции: сложение, вычитание, умножение, деление. Однако, обозначаются они по разному. Например, сложение целых чисел выглядит таким образом:
А сложение дробных таким:
Это связано с тем, что в Caml отсутствуют как класс неявные преобразования типов. Также, возможно, это сильно упрощает существующий в Caml вывод типов выражений.
Имеются типы char и string. Т.е. string – тип встроенный в язык. Для конкатенации строк используется оператор "^".
Имеется тип boolean, принимающий значения true или false.
Тип unit. Этот тип соответствует типу void в C++. Он обозначается так:
Этот тип возвращается процедурами и также может передаваться как аргумент функции – для функции без аргументов.
Кортеж – это просто набор значений различных типов. Данный набор может передаваться и приниматься как одно значение. Доступ к значениям кортежа можно получать только через паттерн-матчинг (средство анализа значений):
Ближайшим аналогом кортежей в C++ являются типы std::pair и boost::tuple. К сожалению, без паттерн-матчинга эти типы не слишком удобно использовать. Для std::pair необходимо использовать поля first и second. А для boost::tuple нужно использовать тоже не слишком хороший вариант: t.get<N> () или get<N> (t). Т.е. просто доступ к члену кортежа по номеру.
Вообще, мне кажется введение в STL типа pair не очень хорошей идеей, которая приводит к появлению неудобочитаемого кода. Наверное, этот тип появился при реализации контейнеров типа map и multimap, чтобы можно было итерировать контейнеры как обычные – только в качестве элементов у нас пары «ключ-значение». Мне кажется, было бы лучше определение специального типа для подобных пар:
Работа с именами key и value куда нагляднее, чем с именами first и second. Или более ужасный пример – результат возврата функции insert для контейнера map:
Конечно, в примере выше можно ввести несколько промежуточных переменных с хорошими названиями. Но зачем?... Вот гипотетический пример на Caml:
boost::tuple же это вообще библиотека для определения структур при помощи неудобного синтаксиса и с неудобочитаемыми методами доступа к членам структуры. Наиболее естественное применение boost::tuple, как мне кажется, – автоматическая генерация структур по определенным параметрам вместе с кодом, который потом будет подобные структуры обрабатывать. Для «человеческого» кода boost::tuple, ИМХО, подходит мало.
Записи – это просто кортежи с именованными элементами. Аналог в C++ – обычная структура. Объявление записи:
По умолчанию все элементы записи – неизменяемы. Чтобы создать запись с изменяемыми полями, нужно использовать mutable:
Кстати, в Caml нет переменных. let a = 5 просто присваивает имя значению 5. Это своего рода аналог объявления константы в C++. Однако, в Caml далее можно написать, например, let a = a + 3. Это не приведет к «модификации» «переменной» – это приведет к созданию имени a, которое теперь будет обозначать значение 8. Соответственно, циклы на подобных выражениях не построишь. Однако, т.к. Caml не чисто функциональный язык, то для него иногда нужны переменные. Для эмуляции переменных в Caml используется запись с единственным мутабельным полем:
Как можно видеть, конструкция «let a = ref 0" привела к созданию записи с единственным полем contents. Теперь можно работать со значением: читать и модифицировать его:
При помощи функции !" мы получаем доступ к значению, при помощи функции «:=" мы его модифицируем.
Впрочем, утверждают, что, как правило, любой алгоритм можно переписать в функциональном стиле без использования переменных.
Считается, вроде бы, что списки – неотъемлемая часть любого функционального языка. Что ж, в Caml «список» – это встроенный в язык тип, для которого имеются конструкции, облегчающие работу с ним.
Создание пустого списка:
Добавление нового элемента:
Выше мы создали новый список с единственным элементом и обозначили его тем же именем, что имел пустой список, созданный ранее.
Анализ списков:
В примере выше мы анализируем два варианта: список пуст и список состоит из головного элемента под именем head и хвоста, обозначенного именем rest. В примере первый элемент выводится на печать. rest можно передать любой другой функции или даже текущей функции для рекурсивного анализа, что и происходит в примере. Аналог на C++ мог бы, наверное, выглядеть так:
Правда, вряд ли рекурсивная обработка списков является широко используемой практикой в C++ – в зависимости от используемого компилятора мы можем получать как полностью работоспособные варианты кода, так и варианты, которые приводят к аварийному завершению программы при анализе больших списков.
Это довольно любопытный тип. По сути это высокоуровневый аналог union в C++. Однако, в Caml с ним работать гораздо удобнее. Объявление объединения выглядит следующим образом:
ConstructorN – называется конструктором типа. С его помощью создаются и анализируются значения составного типа. Условно говоря, объединение, описанное выше, можно рассматривать как пару значений: некоторую константу, идентифицирующую тип (Constructor1...ConstructorN), и значение некоторого абстрактного типа, зависимое от того, каким конструктором мы создаем значение.
Предположим, для радиоуправляемого танка предусмотрен следующий набор состояний: «Отключен», «Двигается со скоростью Х км/ч», «Выполняет поворот с угловой скоростью Х», «Отбросил гусеницы». Возможный тип в Caml:
Создание нового значения:
Здесь мы создали два значения: текущее состояние (отключен) и следующее состояние: движение со скоростью 40 км/ч.
Предположим управление танка производится при помощи некоторой программы – конечного автомата. Внутри программы должен производиться анализ текущего состояния танка и, при необходимости, переключение состояния:
А теперь страшное! Аналог вышенаписанного на C++. Слабонервным просьба отойти от экранов.
Как можно видеть, Caml позволяет избежать возни с огромным количествов ненужных подробностей. Еще более наглядный пример. Вот BNF-описание грамматики простейшего бейсика:
А вот типы данных Caml для описания этой грамматики:
Если кого не впечатлил пример с танком, то можете попробовать реализовать вышеприведенное на C++. Данный пример показателен тем, что для реализации структур, реализующих представление грамматики, на C++ уже будет недостаточно констант и простых числовых типов. Потребуется добавить создание и удаление объектов, что усложнит еще в пару раз и без того сложную задачу. А в Caml управление памятью автоматическое, что еще более упрощает жизнь.
Таким образом, получается (с моей точки зрения), что Caml гораздо лучше подходит для обработки сложных иерархических (и возможно циклических) структур данных, чем C++.
Итак, преимущества объединений в Caml: легко создавать, легко анализировать, исключена ситуация обращения к значению одного типа как к значению другого типа.