Краткое пособие по языку программирования OCaml

Содержание

Введение

Цель данного пособия - познакомить с основами OCaml, который относится к разряду функциональных (аппликативных) языков программирования.

У функциональных языков, ведущих свою "родословную" от Лиспа, сложная и интересная судьба. Эти языки, любимые в университетах, научных институтах и центрах, почти незнакомы широким массам разработчиков.

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

OCaml достаточно широко используется на Западе для обучения языкам программирования, курсы по нему можно найти во многих университетах мира. На странице https://ocaml.org/learn/teaching-ocaml.html приведен внушительный список университетских курсов, изучающих OCaml.

Приходится выразить сожаление, что OCaml в России почти неизвестен и практически нет книг и пособий, посвящённых ему, за исключением перевода документации и книг…

Особенности Ocaml

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

Загрузка и установка

Основной ресурс OCaml расположен по адресу https://ocaml.org, там же можно найти ссылки на установочные пакеты для большого числа операционных систем. После загрузки и развёртывания пакета разработчика (можно произвести установку на обычный флеш-накопитель и использовать как portable-приложение) пользователю доступны следующие программы:

  • ocaml - интерпретатор;
  • ocamlc - компилятор в байт-код;
  • ocamlopt - компилятор в машинный (native) код;

Представляет интерес использование редактора MS Visual Studio Code, далее сокращенно vscode для разработки. Необходимо установить расширение редактора https://github.com/hackwaly/vscode-ocaml или https://github.com/reasonml-editor/vscode-reasonml для комфортной работы. В качестве интерпретатора можно использовать ocaml или (что еще лучше)- utop.

Менеджер пакетов OCaml называется opam. С помощью этой программы устанавливается большинство библиотечных пакетов.

Настройка в Unix

Для Unix-систем, после установки соответствующих пакетов, основные программы доступны в виде команд. Можно запустить интерпретатор ocaml и работать в REPL, а можно компилировать программу и запускать как обычное приложение.

Для редактирования исходных файлов чаще всего применяют редактор Emacs с программным режимом Tuareg, что обеспечивает подсветку исходного кода, передачу выражений в REPL и отладку.

Настройка в Windows

В операционных системах Windows можно работать в командном интерпретаторе (cmd) или, как и в любой системе, в текстовом редакторе Emacs. Нужно отметить, что требуется изменить значение системных переменных (Мой компьютер->Свойства системы->Дополнительно->Переменные среды).

OCAMLLIB=C:\Ocaml\Lib (или другой путь)
PATH=PATH:c:\Ocaml\Bin (или другой путь)

После запуска cmd можно запускать ocaml как команду.

Более подробная информация об установки и настройке приведена на странице https://ocaml.org/docs/install.html

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

Для перехода в режим интерпретатора нужна запустить программу ocaml. Появится приглашение командной строки (#) и система переходит в режим REPL.

OCaml version 4.07.0

# 

Read Eval Print Loop является одним из самых популярных режимов работы с программами на OCaml. Вы просто набираете в интерпретаторе выражения, а потом видите результат их исполнения.

Рассмотрим примеры использования OCaml в качестве калькулятора:

# 3 + 5;;

Результат выполнения:

  • : int = 8

OCaml является строготипизированным языком, так что появление int в ответе не должно удивлять. Тип результата выражения должен совпадать с типами операндов. Попробуйте смешать в одном выражение целочисленную и вещественную константы и вы немедленно получите сообщение об ошибке:

# 3 + 5.7;;
Characters 2-5:
  3+5.7;;
    ^^^
Error: This  expression has type float 
but an expression was expected of type
int

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

Знаки операций для целых и вещественных чисел отличаются: + и +.

# 1.7 +. 7.1;;
- : float = 8.7999999999999989

Для преобразования значений из одного типа в другой существуют специальные функции int_of_float и float_of_int:

# int_of_float 1.7 + 4;;
- : int = 5
# 1.7 +. float_of_int 4;;
- : float = 5.7

Для выхода из интерпретатора в ОС нужно выполнить команду exit 0;;

Встроенные типы данных

В качестве основных типов используются:

Название Обозначение Пример
Целый int 8
Вещественный float 3.1
Логический bool true
Строковый string "Hello"
Символьный char 'a'
     

Операции над целыми значениями:

Название Обозначение Пример
Сложение + 8+6
Вычитание - 3-4
Умножение * 9*9
Деление / 5/2
Остаток от деления mod 5 mod 2

Операции над вещественными значениями:

Название Обозначение Пример
Сложение +. 8 +. 6
Вычитание -. 3 -. 4
Умножение *. 9 *. 9
Деление /. 5 /. 2
Возведение в степень ** 5 ** 2

Для работы с вещественными числами можно использовать встроенные функции:

Название Обозначение Пример
Ближайшее целое, превосходящее x ceil ceil x
Ближайшее целое, не превосходящее x floor floor x
Квадратный корень sqrt sqrt x
Экспонента exp exp x
Натуральный логарифм log log x
Десятичный логарифм log10 log10 x
Синус sin sin x
Косинус cos cos x
Тангенс tan tan x
Арксинус asin asin x
Арккосинус acos acos x
Арктангенс atan atan x
     

Дополнительно к типам (множественным) относят:

  • кортежи
  • списки
  • массивы

Пример действий над строками (конкатенации):

# "Hello " ^ "world!";;
- : string = "Hello world!"

Преобразование из строки в целое и обратно:

# string_of_int 12;;
- : string = "12"
# int_of_string "-56";;
- : int = -56

Пример действий с символьным типом:

# int_of_char('A');;
- : int = 65

Для работы с логическим типом имеются логические операции:

Название Обозначение Пример
Отрицание not not true
Конъюнкция && true && false
Дизъюнкция ll true ll false

Для сравнения значений между собой используют такие операции:

Название Обозначение
Структурное равенство =
Физическое равенство ==
Отрицание = <>
Отрицание == !=
   

Чем отличаются структурное и физическое равенства?

Структурное равенство позволяет сравнить значение полей структуры (при множественных типах), в то время как физическое сравнивает адреса в памяти. Следует помнить, что вещественные числа и строки относятся к структурным типам.

Кортежи

Кортеж представляет собой набор величин разных типов, например:

# (1,4.5,"OK");;
- : int * float * string = (1, 4.5, "OK")

Здесь имеется три величины разных типов, которые OCaml определил как int*float*string

Для кортежей длины 2 (пар) можно использовать функции fst и snd:

# fst ("hello","world");;
- : string = "hello"
# snd ("hello","world");; 
- : string = "world"

С кортежами также может использоваться сопоставление с образцом:

# let tpl=("Вася",1,("Петя",34));;
val tpl : string * int * (string * int) = ("Вася", 1, ("Петя", 34))
# let (name1,age1,(name2,age2))=tpl;;
val name1 : string = "Вася"
val age1 : int = 1
val name2 : string = "Петя"
val age2 : int = 34

Списки

Списком называется набор данных одного типа.

# [1;2;3];;
- : int list = [1; 2; 3]

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

# 0::[1;2;3;4];;
- : int list = [0; 1; 2; 3; 4]
# 1::2::3::4::5::[];;
- : int list = [1; 2; 3; 4; 5]

Вторая операция @ объединяет несколько списков в один:

# [1;2;3]@[4;5;6];;
- : int list = [1; 2; 3; 4; 5; 6]

Массивы

Массивом называется разновидность списка фиксированного размера

# [|1;2;3|];;
- : int array = [|1; 2; 3|]

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

# [|1;2;3|].(0);;
- : int = 1

Индексация, как и во многих других языках, производится, начиная с нуля.

Следует заметить, что строки в OCaml тоже представляют собой разновидность массивов (как в Си), так что допускается ображение к отдельным символам по индексам:

# "hello".[0];;
- : char = 'h'

Условные конструкции

Как и во многих других языках в OCaml существует конструкция if..else для ветвления процесса вычисления

# if a=b then 1 else 2;;
- : int = 1

Можно вкладывать условные конструкции друг в друга:

# if 3=3 then if 2=2 then 1 else 2 else 3;;
- : int = 1
# if 3=4 then if 2=2 then 1 else 2 else 3;;
- : int = 3

Конструкция let

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

Создадим именованную константу:

# let pi=3.14;;
val pi : float = 3.14

В дальнейшем можно использовать имена для построения других выражений:

# let pi2=2. *. pi;;
val pi2 : float = 6.28

Приведённые примеры использования let задают глобальные объявления, которые будут действовать на всём протяжении программы.

Одной из форм использования let является группировка глобальных объявлений:

# let a=1 and b=2 and c=3;;
val a : int = 1
val b : int = 2
val c : int = 3

Локальное объявление создаёт связь между именем и значеним в определённой области кода:

# let a=2 in a*a;;
- : int = 4

Аналогично можно использовать группировку при локальных обхявлениях:

# let a=2 and b=3 in a*b;;
- : int = 6

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

let a = 5;;
Printf.printf "%i\n" a;;
let a = 7 in 
  let b = a * a in
    Printf.printf "%i %i\n" a b;;
Printf.printf "%i\n" a;;
5
7 49
5

Функции

Обычные функции

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

# let square x = x * x;;
val square : int -> int = <fun>
# square 2;;
- : int = 4

В приведенном примере компилятор сам вывел тип функции, предположив тип входного параметра как int. Это позволило опустить описание типов.

В следующем примере компилятор тоже сам выводит все типы:

# let average a b =
  (a +. b) /. 2.0;;

Рекурсивные функции

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

Определим рекурсивную функцию для вычисления факториала:

# let rec factor n =
  if n = 1 then 1
  else  n * factor (n - 1) ;;
val factor : int -> int = <fun>

Теперь найдем факториал числа 5:

# factor 5;;
- : int = 120

Рассмотрим задачу вычисления n-ого числа Фибоначчи:

let rec fib n = 
  match n with 
    1 -> 1
  |  2 -> 1
  |  _ -> fib (n-1) + fib (n-2) ;;
val fib : int -> int = <fun>

Данная реализация страдает от эффекта экспоненциальной рекурсии и может сильно замедлять работу компьютера при больших значениях параметра.

Более экономное решение:

let fib n =
  let rec fib_iter i j k =
     if k=0 then i
     else fib_iter (i+j) i (k-1) in
     fib_iter 0 1 n ;;

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

Рассмотрим в качестве примера функцию нахождения суммы элементов целочисленного списка

# let rec sumlist x =
    if x=[] then 0 else
      List.hd x + sumlist (List.tl x);;
val sumlist : int list -> int = <fun>
# sumlist [1;2;3;4;5];;
- : int = 15

Основная идея данной реализации: выделение первого элемента списка и суммирование его с результатом нахождения суммы оставшейся части. Разбить список на две части помогают операции из стандартного модуля List: hd и tl (переводится как голова и хвост соответственно). С помощью рекурсии мы каждый раз будем обрабатывать всё меньшее и меньшее число элементов, пока список не станет пустым.

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

Вложенные функции

Рассмотрим задачу проверки числа на простоту. Рассмотрим случай, когда решение состоит из двух функций:

let rec isprime_iter n j=
  if j*j>n then
    true
  else if n mod j==0 then
      false
      else isprime_iter n (j+1) ;;

let isprime n = 
  if n<=1 then 
    false
  else 
    isprime_iter n 2 ;;

Одна из функций рекурсивная, другая - нет. Проблема лишь в том, что первая функция является вспомогательной и нужна только внутри второй. Решением будет вложить описание вспомогательной функции в тело основной:

let isprime n = 
  let rec isprime_iter n j=
    if j*j>n then
      true
    else if n mod j==0 then
        false
        else isprime_iter n (j+1)  in
  if n<=1 then 
    false
  else 
    isprime_iter n 2 ;;

Безымянные функции

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

 # (fun x-> x+1);;
- : int -> int = <fun>

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

# (fun x-> x+1) 5;;
- : int = 6

Пример использования лямбда-функции для каждого элемента списка

# List.map (fun x -> x*2) [1;2;3];;
- : int list = [2; 4; 6]

Пример определения функции для сложения двух чисел:

# fun x y -> x+y;;
- : int -> int -> int = <fun>

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

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

# fun x -> (fun y -> x+y);;
- : int -> int -> int = <fun>

Разумеется, внутреннюю функцию (по y) нужно вызывать для определённого x.

В следующем примере мы создаём безымянную функцию и тут же её вызываем для двух аргументов (5 и 6):

# (fun x y -> x+y) 5 6;;
- : int = 11

Используя let, можно связать с функцией имя:

# let summa = (fun x y -> x+y);;
...
# summa 5 6;;
- : int = 11

Поскольку создание именных функций является наиболее распространённой практикой, существует более простая конструкция с let (которую мы уже рассматривали выше):

# let summa x y = x + y;;

Пример с проверкой числа на простоту

Рассмотрим пример функции, которая получает целое число на вход и проверяет, является ли переданное число простым.

Первая реализация:

# let is_prime n =
    let n = abs n in
    let rec is_not_divisor d =
      d * d > n || (n mod d <> 0 && is_not_divisor (d+1)) in
    n <> 1 && is_not_divisor 2;;

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

Вторая реализация:

let prime n =
    let rec checkZero x d = match d with
        | 1 -> true    
        | _ -> (x mod d <> 0) && checkZero x (d-1)
    in match n with
    | 0 | 1 -> false
    | _ -> checkZero n (n-1) ;;

Во втором способе активно используются конструкции match.

Функции высших порядков

Если функция оперирует другими функциями как данными (например, принимает в качестве параметров), то её называют функцией высших порядков, или функционалом. Разработка и использование функционалов составляет важную часть программирования в функциональном стиле.

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

# List.map (fun x -> x+1) [1;2;3;4;5];;
- : int list = [2; 3; 4; 5; 6]

Функция map, относящаяся к стандартному модулю List принимает в качестве первого параметра функцию, а в качестве второго - список и применяет функцию к каждому элементу списка.

Конструкция соспоставления с образцом (match)

Очень важную роль в программах имеет конструкция match, сопоставляющую выражение с набором образцов:

# let test x=
  match x with
  | 1 -> "hello" 
  | 2 -> "by"    
  | _ -> "unknown";;             
val test : int -> string = <fun>
# test 1;;
- : string = "hello"

Пример с использованием сопоставления при работе со списками:

# let rec len x =
  match x with
  [] -> 0
  | hd :: tl -> 1+ len(tl);;
val len : 'a list -> int = <fun>
# len [1;2;3;4];;
- : int = 4

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

# let rec sumlist x = 
  match x with
  [] -> 0
  | hd::tl -> hd+sumlist tl;;
val sumlist : int list -> int = <fun>
# sumlist [1;2;3;4;5];;
- : int = 15

Пример: сумма элементов списка

Вот еще одна конструкция с вложенной функцией:

let sumlist x =
  let rec sumlist' x res = 
    match list with
    | [] -> res
    | hd::tl -> sumlist' tl (res + hd)
  sumlist' x 0

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

Работа с source-файлами

Режим REPL - не единственный режим работы OCaml. Можно подготовить файл с исходным кодом и загрузить его в интерпретатор, а можно скомпилировать, причём компиляция осуществляется либо в бай-код, либо в машинный код.

Рассмотрим, как можно загрузить файл в интерпретатор и выполнить его.

(* Содержимое hello.ml *)
Printf.printf "Hello, world!\n" 

Далее, мы загружаем подготовленный файл из интерпретатора:

# #use "путь/hello.ml";;
Hello, world!
- : unit = ()

При использовании команды use нужно указать либо абсолютный либо относительный путь к файлу с исходным кодом.

Можно вызвать интерпретатор и передать ему имя файла (а при необходимости ещё и параметры):

$ ocaml hello.ml
Hello, world!

Список источников

Книги

  • Hickey J. Introduction to Objective Caml, 2008.
  • Harrop J. OCaml for scientists, 2005.
  • Chailloux E., Manoury P., Pagano B. Developing applications with Objective Caml, O'Reilly, 2000.
  • Chailloux E., Manoury P., Pagano B. Разработка программ с помощью Objective Caml, (русский перевод), 2007.
  • Smith J. Practical OCaml, APress, 2006.
  • Downey A., Monje N. Think OCaml, Green Tea Press, 2008.
  • Whitington J. OCaml from the Very Beginning, Coherent press. Cambridge, 2013.
  • Whitington J. More OCaml: Algorithms, Methods, and Diversions, Coherent press. Cambridge, 2014.
  • Leroy X., Remy D. Unix system programming in OCaml, 2013.
  • Minsky Y., Madhavapeddy A., Hickey J. Real world OCaml, O'Reilly, 2013.
  • Didier Rémy Using, Understanding, and Unraveling OCaml, 2001.

Автор: Anton Shtanyuk

Created: 2019-07-25 Thu 15:28

Emacs 26.1 (Org mode 9.1.14)

Validate