Pull to refresh

Введение в OCaml: Структура программ на OCaml [2]

Reading time12 min
Views8K
Original author: www.ocaml-tutorial.org
[прим. пер.: продолжение перевода, первая статья тут]
тизер к статье с графикой на OCaml

Структура программ на OCaml


Теперь мы потратим немного времени на высокоуровневый анализ некоторых настоящих программ на OCaml'е. Я хотел бы показать вам локальные и глобальные определения, разницу в использовании ;; и ;, модули, вложенные функции, ссылки. Из-за этого мы столкнёмся с множеством концепций OCaml'а, которые пока что не имеют смысла для начинающего изучать OCaml, так как мы не встречали их ранее. Не фокусируйтесь на них, сконцентрируйтесь вместо этого на общем представлении о формате программ и особенностях языка, на которые я буду указывать.

Локальные «переменные» (на самом деле локальные выражения)


Возьмём нашу функцию average на Си и добавим в неё локальные переменные (сравните её с примером в прошлой главе).

double average (double a, double b)
{
  double sum = a + b;
  return sum / 2;
}

Теперь посмотрим на это для OCaml:

let average a b =
  let sum = a +. b in
  sum /. 2.0;;

Стандартное выражение let name = expression in используется для определения локального именованного выражения и name может быть использовано в дальнейшем вместо expression вплоть до ;;, который означает окончание локального блока кода. Обратите внимание, мы даже не использовали отступ после объявления in. Просто воспринимайте let ... in так, как будто это один оператор.

Теперь, сравнение локальных переменных на Си и вышеприведённых локальных именованных выражений — вопрос ловкости рук. Хотя они в чём-то разнятся. Переменная sum в коде на Си использует память в стеке. Вы можете в дальнейшем присвоить sum любое значение, какое захотите, или даже получить адрес памяти, где хранится значение. Для версии на OCaml это не так. В ней sum — всего лишь краткое название выражения a +. b. Нет никакой возможности присвоить что-либо sum. (Чуть позже мы покажем, как создавать переменные, у которых можно менять значение).

Вот другой пример для окончательного прояснения. Эти два отрывка кода должны возвращать одно и то же значение ( (a+b) + (a+b)2):

let f a b =
  (a +. b) +. (a +. b) ** 2.
  ;;

let f a b =
  let x = a +. b in
  x +. x ** 2.
  ;;

Вторая версия должна быть по-идее быстрее (но большинство компиляторов сумеют осуществить этап под названием «уничтожение общих подвыражений» для вас), и оно однозначно легче для чтения. x во втором примере всего лишь краткое название для a +. b.

Глобальные «переменные» (на самом деле глобальные выражения)


Можно так же определить глобальные имена для различных вещей на верхнем уровне, и, подобно локальным «переменным» выше, они на самом деле не переменные, они — короткие названия для разных вещей. Вот реальный (хотя слегка урезанный) пример:

let html =
  let content = read_whole_file file in
  GHtml.html_from_string content
  ;;

let menu_bold () =
  match bold_button#active with
    true -> html#set_font_style ~enable:[`BOLD] ()
  | false -> html#set_font_style ~disable:[`BOLD] ()
  ;;

let main () =
  (* code omitted *)
  factory#add_item "Cut" ~key:_X ~callback: html#cut
  ;;


Это фрагмент реального кода. html — виджет для редактирования HTML (объект библиотеки lablgtk), созданный в начале программы выражением let html =. Потом, далее, он используется в нескольких функциях.

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

let-binding


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

Ссылки: настоящие переменные


А что, если вы хотите настоящую переменную, значение которой вы можете присваивать и менять в ходе выполнения программы? В этом случае, вам нужно использовать ссылку. Ссылки очень похожи на указатели в Си/Си++. В Java, все переменные, которые хранят объекты, на самом деле ссылки (указатели) на объекты. В Перле ссылки — это ссылки, так же как в OCaml.

Вот, как мы создаём ссылку на int в OCaml:

ref 0;;

На самом деле подобное выражение не слишком полезно. Мы создали ссылку и из-за того, что мы не дали ей имя, тут же пришёл сборщик мусора и собрал её! (На самом деле, вероятнее всего, её выкинут ещё на этапе компиляции). Давайте же дадим ссылке имя:

let my_ref = ref 0;;

Эта ссылка в настоящий момент хранит целый ноль. Давайте теперь запишем в неё другое значение (операция присвоения):

my_ref := 100;;

И давайте посмотрим, что ссылка хранит сейчас:

# !my_ref;;
- : int = 100

Итак, оператор := используется для присвоения ссылок, а оператор ! разыменовывает ссылки, возвращая содержимое. Вот грубое, но эффективное сравнение с Си/Си++:
OCaml C/C++
let my_ref = ref 0;;
my_ref := 100;;  
!my_ref

int a = 0; int *my_ptr = &a;
*my_ptr = 100;
*my_ptr


У ссылок есть своя область применения, но использовать их вы будете не слишком часто. Куда более частым будет применение let name=expression in для именования локальных выражений в ваших функциях.

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


В Си нет концепции вложенных функций. GCC поддерживает вложенные функции для программ на Си, но я не знаю ни одной программы, которая бы использовала это расширение. В любом случае, вот что пишет info-страница gcc о вложенных функциях:
«Вложенная функция» — это функция, определённая внутри другой функции (Вложенные функции не поддерживаются для GNU C++). Имена вложенных функций являются локальными для блока, в котором они были определены. Например, вот определение вложенной функции 'square', которая вызывается дважды:

foo (double a, double b)
{
  double square (double z) { return z * z; }

  return square (a) + square (b);
}

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

bar (int *array, int offset, int size)
{
  int access (int *array, int index)
    { return array[index + offset]; }
  int i;
  /* ... */
  for (i = 0; i < size; i++)
    /* ... */ access (array, i) /* ... */
}


Надеюсь, идея понятна. Вложенные функции, однако, весьма полезны и активно используются в OCaml. Вот пример использования вложенной функции из реального кода:

let read_whole_channel chan =
  let buf = Buffer.create 4096 in
  let rec loop () =
    let newline = input_line chan in
    Buffer.add_string buf newline;
    Buffer.add_char buf '\n';
    loop ()
  in
  try
    loop ()
  with
    End_of_file -> Buffer.contents buf;;

Пока не беспокойтесь, если не понимаете этот код целиком — он содержит множество концепций, о которых мы пока не говорили. Вместо этого сконцентрируйтесь на центральной вложенной функции loop, которая принимает аргумент с типом unit. Вы можете вызвать loop () из функции read_whole_channel, а за пределами функции она не определена. Вложенная функция имеет доступ к переменным, определённым в главной функции (так loop получает доступ к локальным переменным buf и chan).

Форма определения вложенной функции аналогична задаче локального именованного выражения: let name arguments = function-defenition in.

Обычно определение функции отбивается отступом на новой строке (как в примере выше). И помните, что нужно использовать let rec вместо let, если функция рекурсивна (как в примере выше).

Модули и команда open


Вместе с OCaml поставляется множество интересных модулей (библиотек с полезным кодом). Например, есть стандартные библиотеки для рисования графиков, создания GUI интерфейсов с помощью набора виджетов, обработки больших чисел, структур данных, совершения системных вызовов POSIX. Эти библиотеки находятся в /usr/lib/ocaml/VERSION (на юниксах). Для наших примеров мы сконцентрируемся на довольно простом модуле, именуемом Graphics.

Модуль Graphics состоит из 5 файлов (на моей системе):

/usr/lib/ocaml/3.08/graphics.a
/usr/lib/ocaml/3.08/graphics.cma
/usr/lib/ocaml/3.08/graphics.cmi
/usr/lib/ocaml/3.08/graphics.cmxa
/usr/lib/ocaml/3.08/graphics.mli

[прим. пер.: а на моей системе (Debian Sid) модули свалены прямо в /usr/lib/ocaml, без указания версии].

Сначала сконцентрируемся на файле graphics.mli. Это текстовый файл, так что вы можете легко посмотреть его содержимое. Прежде всего, обратите внимание, что имя graphics.mli, а не Graphics.mli. OCaml всегда делает первую букву имени файла заглавной, когда речь идёт про модули. Это может весьма путать, если только вы не знаете про это заранее.

Если мы хотим использовать функции в Graphics, то есть два пути. Либо в начале нашей программы мы пишем декларацию open Graphics;;, либо мы дополняем все вызовы соответствующих функций префиксом: Graphics.open_graph.open. open слегка напоминает функцию import в Java, и чуть больше (напоминает) выражение use в Перле.

[Для пользователей Окон: Для того, чтобы этот пример работал в интерактивном режиме в Окнах, вам потребуется создать отдельный верхний уровень (toplevel). Выполните команду из командной строки наподобие этой: ocamlmktop -o ocaml-graphics graphics.]

Пара следующих примеров должна всё прояснить (Эти два примера рисуют разное — попробуйте их оба). Обратите внимание, первый пример вызывает open_graph, второй вызывает Graphics.open_graph. [прим. пер.: В начале статьи скриншоты того, что делают программы].

(* To compile this example: ocamlc graphics.cma grtest1.ml -o grtest1 *)

open Graphics;;

open_graph " 640x480";;
for i = 12 downto 1 do
  let radius = i * 20 in
  set_color (if (i mod 2) = 0 then red else yellow);
  fill_circle 320 240 radius
done;;
read_line ();;




(* To compile this example: ocamlc graphics.cma grtest2.ml -o grtest2 *)

Random.self_init ();;
Graphics.open_graph " 640x480";;

let rec iterate r x_init i =
        if i = 1 then x_init
        else
                let x = iterate r x_init (i-1) in
                r *. x *. (1.0 -. x);;

for x = 0 to 639 do
        let r = 4.0 *. (float_of_int x) /. 640.0 in
        for i = 0 to 39 do
                let x_init = Random.float 1.0 in
                let x_final = iterate r x_init 500 in
                let y = int_of_float (x_final *. 480.) in
                Graphics.plot x y
        done
done;;

read_line ();;

Оба примера используют некоторые возможности языка, о которых мы пока не говорили: императивные циклы for, блоки if-then-else, рекурсию. Мы обсудим это позже. Не смотря на это вы всё-таки можете: (1) попытаться понять, как они работают (2) как выведение типов позволяет отлавливать ошибки.

Модуль Pervasives


Есть один модуль, который не надо «open». Это модуль Pervasives (находится в /usr/lib/ocaml/3.08/pervasives.mli [прим. пер.: у меня /usr/lib/ocaml/pervasives.mli]). Все символы из модуля Pervasives автоматически импортируются во все программы на OCaml.

Переименование модулей


Что, если вы хотите использовать символы из Graphics, но не хотите импортировать их все, и вам лениво печатать Graphics каждый раз? Просто переименуйте модуль, используя этот приём:

module Gr = Graphics;;

Gr.open_graph " 640x480";;
Gr.fill_circle 320 240 240;;
read_line ();;

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

Когда использовать, а когда пропускать ;; и ;


Теперь мы рассмотрим очень важный вопрос. Когда следует использовать ;;, когда следует использовать ;, а когда следует пропускать оба варианта? Это хитрый вопрос остаётся до тех пор, пока вы в нём хорошо не разберётесь. Беспокоил он длительное время и автора, пока пока тот изучал OCaml.

Правило №1 — вы должны использовать ;; для отделения утверждений в верхнем уровне вашего кода (top-level) и никогда внутри определения функций или иного рода утверждений.

Взглянем на фрагмент когда из второго примера с графикой:

Random.self_init ();;
Graphics.open_graph " 640x480";;

let rec iterate r x_init i =
        if i = 1 then x_init
        else
                let x = iterate r x_init (i-1) in
                r *. x *. (1.0 -. x);;


У нас есть два утверждения на верхнем уровне и определение функции iterate. Каждое из них завершается ;;.

Правило №2 — иногда вы можете пропускать ;;. Как неофит, вам не следует особо задумываться об этом правиле и всегда писать ;;, как предписывает правило №1. Но если вы будете читать чужой код, вы будете иногда встречаться с отсутствием ;;. Места, в которых можно опускать ;;:
  • Перед ключевым словом let;
  • Перед ключевым словом open;
  • Перед ключевым словом type;
  • В самом конце файла
  • В нескольких других (весьма редких) случаях, где OCaml может «предположить», что следующий элемент — это начало нового утверждения, а не продолжение текущего.

Вот пример кода, где ;; опущено всюду, где возможно:

open Random                   (* ;; *)
open Graphics;;

self_init ();;
open_graph " 640x480"         (* ;; *)

let rec iterate r x_init i =
        if i = 1 then x_init
        else
                let x = iterate r x_init (i-1) in
                r *. x *. (1.0 -. x);;

for x = 0 to 639 do
        let r = 4.0 *. (float_of_int x) /. 640.0 in
        for i = 0 to 39 do
                let x_init = Random.float 1.0 in
                let x_final = iterate r x_init 500 in
                let y = int_of_float (x_final *. 480.) in
                Graphics.plot x y
        done
done;;

read_line ()                  (* ;; *)

Правила №3 и №4 относятся к ;. Они полностью отличаются от правил для ;;. Единичная точка с запятой (;) называется точкой последовательности [прим. пер.: могу ошибаться с переводом sequence point], которая играет ровно такую же роль, как одиночная точка с запятой в языках Си, Си++, Java или Перл. Она значит «сделай всё после этого места когда сделаешь всё до него». Спорим, вы не знали этого.

Правило №3: Считайте let ... in утверждением и никогда не ставьте ; после него.

Правило №4: Все прочие утверждения в блоке кода завершайте ;, кроме самого последнего.

Внутренний цикл for выше — хороший пример. Обратите внимание, что мы ни разу не использовали одинарную ; в коде.

        for i = 0 to 39 do
                let x_init = Random.float 1.0 in
                let x_final = iterate r x_init 500 in
                let y = int_of_float (x_final *. 480.) in
                Graphics.plot x y
        done

Единственное место, где возможно использование ; — это строчка Graphics.plot x y, но так как это последняя строчка блока, то согласно правилу №4, её ставить не надо.

Замечание относительно ";"


Брайн Харт поправил меня:
; — такой же оператор, как, например, оператор сложения (+). Ну, не совсем как +, но по сути — именно так. + имеет тип int -> int -> int — он берёт два целых и возвращает целое (их сумму). ; имеет тип unit -> 'b -> 'b — он берёт два зачения и просто возвращает второе. В отличие от запятой в Си. Вы можете написать a;b;c;d так же просто, как вы можете написать a + b + c + d.

Это одна из тех фундаментальных концепций, понимание которых даёт понимание языка, но которая никогда особо не проговаривается вслух — в OCaml буквально всё — это выражение. if/then/else — это выражение. a;b — это выражение. match foo with ... — это выражение. Нижеприведённый код — совершенно правилен (и они все делают одно и то же):

 let f x b y = if b then x+y else x+0
 
 let f x b y = x + (if b then y else 0)
 
 let f x b y = x + (match b with true -> y | false -> 0)
 
 let f x b y = x + (let g z = function true -> z | false -> 0 in g y b)
 
 let f x b y = x + (let _ = y + 3 in (); if b then y else 0)

Особенно присмотритесь к последнему — я использую ; как оператор для «объединения двух утверждений. Все функции в OCaml могут быть выражены как:
 let name [parameters] = expression

Определение „выражения“ в OCaml несколько шире, чем в Си. На самом деле, Си имеет концепцию „утверждений“, но все утверждения в Си — всего лишь выражения в OCaml (объединённые ;).

Единственным различием между ; и + является возможность ссылки на +, как на функцию. Например, я могу определить функцию sum_list для суммирования списков целых как:
let sum_list = List.fold_left ( + ) 0



Всё вместе: немного реального кода


В этом разделе мы покажем некоторые фрагменты реального кода из библиотеки labgtk 1.2 (Labgtk — это интерфейс в OCaml для нативных виджетов Unix). Предупреждение: этот код полон вещей, о которых мы пока не говорили. Не вдавайтесь в детали, посмотрите вместо этого на общую структуру кода, места, где авторы использовали ;;, где они использовали ;, где они использовали open, как они отбивали текст, как они использовали локальные и глобальные выражения.

… Однако, я дам вам несколько подсказок, чтобы не потеряться совсем.

  • ?foo и ~foo — способы указывать опциональные и именованные аргументы функций в OCaml. Прямой параллели с Си-подобными языками провести не удастся, но в Perl, Python и Smalltalk есть эта концепция. Вы можете называть аргументы при вызове функции, опускать некоторые из них и передавать оставшиеся в том порядке, как вам удобно.
  • foo#bar — вызов метода (вызов метода, называющегося bar в отношении объекта, называющегося foo). Это подобно foo->bar, foo.bar, $foo->bar (Си++, Java, Perl).


Первый фрагмент: Программист открывает пачку стандартных библиотек (опуская ;;, потому что следующие ключевые слова — это open или let). Так же он создаёт функцию, называющуюся file_dialog. Внутри этой функции он определяет именованное выражение с названием sel используя двустрочное утверждение let sel = .. in. Затем он вызывает несколько методов в отношении sel.

(* First snippet *)
open StdLabels
open GMain

let file_dialog ~title ~callback ?filename () =
  let sel =
    GWindow.file_selection ~title ~modal:true ?filename () in
  sel#cancel_button#connect#clicked ~callback:sel#destroy;
  sel#ok_button#connect#clicked ~callback:do_ok;
  sel#show ()

Второй фрагмент: Просто большой список глобальных имён на верхнем уровне. Обратите внимание, что автор опустил все ;; согласно правилу №2.

(* Second snippet *)

let window = GWindow.window ~width:500 ~height:300 ~title:"editor" ()
let vbox = GPack.vbox ~packing:window#add ()

let menubar = GMenu.menu_bar ~packing:vbox#pack ()
let factory = new GMenu.factory menubar
let accel_group = factory#accel_group
let file_menu = factory#add_submenu "File"
let edit_menu = factory#add_submenu "Edit"

let hbox = GPack.hbox ~packing:vbox#add ()
let editor = new editor ~packing:hbox#add ()
let scrollbar = GRange.scrollbar `VERTICAL ~packing:hbox#pack ()

Третий фрагмент: Автор импортирует все символы из модуля GdkKesyms. Потом следует необычный let-binding. let _ = expression означает „рассчитать значение выражения (включая выполнение всех побочных эффектов), но выкинуть результат“. В данном случае „рассчитать значение выражения“ означает выполнение Main.main (), которая является основным циклом GTK, побочным эффектом которого является появление окна на экране и выполнения всего приложения. „Результат“ вызова Main.main () незначим. Вероятнее всего это unit, но я не проверял — и оно точно не возвращается до завершения приложения.

Обратите внимание, что этот фрагмент содержит длинные цепочки, фактически, процедруных команд. Это настоящая классическая императивная программа.

(* Third snippet *)

open GdkKeysyms

let _ =
  window#connect#destroy ~callback:Main.quit;
  let factory = new GMenu.factory file_menu ~accel_group in
  factory#add_item "Open..." ~key:_O ~callback:editor#open_file;
  factory#add_item "Save" ~key:_S ~callback:editor#save_file;
  factory#add_item "Save as..." ~callback:editor#save_dialog;
  factory#add_separator ();
  factory#add_item "Quit" ~key:_Q ~callback:window#destroy;
  let factory = new GMenu.factory edit_menu ~accel_group in
  factory#add_item "Copy" ~key:_C ~callback:editor#text#copy_clipboard;
  factory#add_item "Cut" ~key:_X ~callback:editor#text#cut_clipboard;
  factory#add_item "Paste" ~key:_V ~callback:editor#text#paste_clipboard;
  factory#add_separator ();
  factory#add_check_item "Word wrap" ~active:false
    ~callback:editor#text#set_word_wrap;
  factory#add_check_item "Read only" ~active:false
    ~callback:(fun b -> editor#text#set_editable (not b));
  window#add_accel_group accel_group;
  editor#text#event#connect#button_press
    ~callback:(fun ev ->
      let button = GdkEvent.Button.button ev in
      if button = 3 then begin
        file_menu#popup ~button ~time:(GdkEvent.Button.time ev); true
      end else false);
  editor#text#set_vadjustment scrollbar#adjustment;
  window#show ();
  Main.main ()


[прим. пер.: если кто-то видит ошибки или речекряки, пишите, исправлю]
Tags:
Hubs:
+19
Comments24

Articles