Я подписан на список рассылки главы SigPLAN моей школы, и мы только что получили по почте задачу реализовать язык стека в OCaml.

Нас не интересует парсинг или что-то в этом роде; мы просто заинтересованы в том, чтобы реализовать эту функциональность в интерпретаторе OCaml (то есть, в основном, чтобы OCaml использовался для синтаксического анализа). Задача возникла с учетом определенного синтаксиса, но я вернусь к нему позже.

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

type command = Push of int | Pop | Add

и иметь рекурсивную функцию, которая просто выполняет их список

let rec run’ cmds stack =
  match cmds with
  | Push x :: rest -> run’ rest (x :: stack)
  | Pop :: rest -> run’ rest (tl stack)
  | Add :: rest -> run’ rest (((hd stack) + (hd (tl stack))) :: (tl (tl stack)))
  | [] -> hd stac

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

and run cmds = run’ cmds []

У меня есть отзывы об использовании сопоставления с образцом для предложения add, которое действительно выглядит намного лучше (или я так думаю)

let rec run’ cmds stack =
  match cmds with
  | Push x :: rest -> run’ rest (x :: stack)
  | Pop :: rest -> run’ rest (tl stack)
  | Add :: rest -> let (a :: b :: stack’) = stack in run’ rest (a + b :: stack’)
  | [] -> hd stack

В общем, это довольно простой код

(*
Example execution:
# run [Push 1; Push 2; Push 3; Pop; Push 4; Add; Add] ;;
— : int = 7
*)
open List
type command = Push of int | Pop | Add
let rec run’ cmds stack =
  match cmds with
  | Push x :: rest -> run’ rest (x :: stack)
  | Pop :: rest -> run’ rest (tl stack)
  | Add :: rest -> let (a :: b :: stack’) = stack in run’ rest (a + b :: stack’)
  | [] -> hd stack
and run cmds = run’ cmds []

Отлично, отлично работает. Пока я не увидел предполагаемый синтаксис:

# start push 1 push 2 add stop;;
— : int = 3
# start push 1 push 2 push 3 pop push 4 add add stop;;
— : int = 7

Сказать что? Я не совсем понимал, на какую черную магию я смотрел сначала. start просто казался какой-то волшебной функцией, которая берет кучу аргументов и выполняет их как программу.

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

Каррирование и частичное нанесение

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

Рассмотрим такую ​​функцию:

Обычно мы вызываем это так:

Но то, что здесь происходит на самом деле, немного отличается от того, что вы могли ожидать. На самом деле все функции OCaml принимают только один аргумент. Итак, на самом деле f 1 2 оценивается как (f 1) 2. То есть мы применяем f к 1, берем результат (который является другой функцией) и применяем его к 2.

Итак, следующее абсолютно законно:

f ’- это функция, которая принимает только один параметр и добавляет к нему еще один. Итак, f ’x - это то же самое, что и f 1 x.

Продолжение и объединение вызовов функций

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

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

let add_continuation a b k = k (a + b)

Мы вызываем add_continuation с двумя числами, которые нужно добавить, а также с третьим аргументом k, который представляет собой функцию, вызываемую в результате добавления (т. Е. K - это «k’продолжение»).

let square x = x * x in add_continuation 4 5 square

Здесь k связывается с квадратом. Вышеупомянутое выражение оценивается как квадрат (4 + 5).

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

let mult a b = a * b in add_continuation 4 5 mult

Это выражение оценивается как mult (4 + 5), но поскольку mult ожидает 2 аргумента, это всего лишь частичное применение mult. То есть mult (4 + 5) представляет функцию, которая умножает все, что вы ей передаете, на 9.

Вернуться в стек

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

Во-первых, у нас есть функция start, которая запускает весь этот процесс, инициализируя наше состояние и передавая его

Все, что делает start, - это берет функцию продолжения k и передает ей пустой список, который представляет наш начальный пустой стек.

Скажем, у нас была какая-то функция

let push5 stack = 5 :: stack

Теперь мы можем сделать что-то вроде

который оценивается как [5].

Проблема с push5 в том, что он завершает цепочку. Нам нужно изменить его так, чтобы вместо того, чтобы просто возвращать измененный стек, он передавал измененный стек какой-либо другой функции.

let better_push5 stack k = k (5 :: stack)

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

Итак, теперь мы можем сделать

start better_push5 better_push5 stop

чтобы получить [5; 5].

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

let push stack x k = k (x :: stack)

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

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

let pop (_ :: stack’) k = k stack’

и добавить выглядит как

let add (a :: b :: stack’) k = k (a + b :: stack’)

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

let start k = k []
let push stack x k = k (x :: stack)
let pop (_ :: stack’) k = k stack’
let add (a :: b :: stack’) k = k (a + b :: stack’)
let stop (x :: _) = x

Первоначально опубликовано на alecb.me.