Представление и выполнение функциональных программ
Ранее было сказано, что любую формулу можно представить в виде S-выражения. Одним из способов такого представления является язык программирования Scheme, развившийся из языка LISP. В языке Scheme различаются строчные и прописные буквы (в LISP и ранних версиях Scheme – не различаются), идентификатором является любая последовательность букв, цифр и спецсимволов, не начинающаяся с цифры. Способ перевода легко понять из таблицы 1.
Компилируемые функциональных языков являются типизированными, при этом тип переменной часто определяется по типу инициализирующего выражения и в большинстве случаев его можно не указывать. Обычно тип указывается только для параметров функции. В таблице 1 показан способ перевода на набирающий популярность язык Kotlin.
Таблица 1
Группа | Абстрактная форма | Представление на языке Scheme | Представление на языке Kotlin |
Переменные | x | x | x |
Константы Числовые Булевы | 'x '(1 2 3), '() 10, 1.25 #T, #F | 'x или (quote x) '(1 2 3), '() 10, 1.25 #t или #T, #f или #F | "x" listOf(1,2,3), listOf<T>() 10, 1.25 true, false |
Арифметические операции | x+y, x–y, x⋅y, x/y, –x, xy, x mod y | (+ x y), (– x y), (* x y), (/ x y), (– x), (expt x y), (remainder x y) | x+y, x-y, x*y, x/y, –x, x.pow(y), x%y |
Сравнения | x = y, x ≠ y, x < y, x > y, x ≤ y, x ≥ y | (= x y), (not (= x y)), (< x y), (> x y), (<= x y), (>= x y) | x==y, x!=y, x<y, x>y, x<=y, x>=y |
Логические операции | не x, x и y, x или y | (not x), (and x y), (or x y) | !x, x && y, x || y |
Обработка списков | car(x), cdr(x), cons(x,y), atom(x), eq(x,y), null(x), number(x) | (car x), (cdr x), (cons x y), (not (pair? x)), (eqv? x y), (null? x), (number? x) | x.first() или x[0], x.drop(1), listOf(x)+y, x !is List<*>, x===y, x.isEmpty(), x is Int, x is Double |
Вызов функции | f(x1,x2,…,xn) | (f x1 x2 … xn) | f(x1,x2,...,xn) |
Условное предложение | если e1 то e2 иначе e3 | (if e1 e2 e3) | if (e1) e2 else e3 |
Блоки локальных определений | {e где x1=e1; …; xk=ek} {пусть x1=e1; …; xk=ek в e} {e гдерек x1=e1; …; xk=ek} {пустьрек x1=e1; …; xk=ek в e} | (let ((x1 e1) … (xk ek)) e) (letrec ((x1 e1) … (xk ek)) e) | {val x1=e1;...; val xk=ek e} нужно добавить () после } если не часть if/while через использование lateinit var |
λ-выражения | λ(x1,x2,…,xn)e | (lambda (x1 x2 … xn) e) | {x1,x2,...,xn -> e} или просто {e} для одного аргумента с именем it |
Определения | f(x1,x2,…,xn)=e g=e | (define (f x1 x2 … xn) e) (define g e) | fun f(x1:T1,...,xn:Tn):T = e val g=e (с явным указанием типа val g:T=e) |
Отложенные вычисления | задержать e возобновить e | (delay e) (force e) | lazy {e} e.value |
Многие функции в языке Scheme можно вызывать с более чем двумя аргументами, например, (+ 1 2 3 4) (сумма аргументов) или (< 1 2 3 4) (список аргументов упорядочен по возрастанию?).
Функция отобр после перевода на язык Scheme будет выглядеть следующим образом:
(define (отобр x f)
(if (null? x) '()
(cons (f (car x)) (отобр (cdr x) f))))
(отобр '(1 2 3) (lambda (x) (* x x)))
А на языке Kotlin для списка значений типа X так:
fun<X,Y> отобр(x:List<X>, f:(X)->Y):List<Y> =
if(x.isEmpty()) listOf() else listOf(f(x.first()))+отобр(x.drop(1),f)
отобр(listOf(1,2,3),{it*it})
Чтобы понять, как выполняется функциональная программа, введем понятие контекста. Контекст – это соответствие между переменными и их значениями, которые являются S-выражениями. Каждое выражение в функциональной программе вычисляется в некотором контексте. Например, в контексте {x → (A . B), y → (C D), z → 127} выражение cons(z+1, cdr(x)) имеет значение (128 . B).
Блоки "пусть" и "где" используются, чтобы установить новые связи. Выражение для значения новой локальной переменной вычисляется в некотором внешнем контексте. Определяемое блоком выражение вычисляется в новом контексте, полученном изменением внешнего контекста за счет добавления новых связей или вытеснения старых связей новыми в случае совпадения имен локальной переменной и переменной в контексте. Например, если выражение {пусть u=cdr(x); z=z+1 в cons(z,cons(u,y))} вычисляется в контексте {x → (A . B), y → (C D), z → 127}, то для вычисления определяемого выражения используется контекст {u → B, x → (A . B), y → (C D), z → 128} и получается (128 B C D).
Рассмотрим выполнение функции на примере выражения {пусть f=λ (x,y) 2⋅x+y в f(2,3)+f(3,2)}. Аргументы функции f вычисляются в момент ее вызова, и каждое значение связывается с соответствующим формальным параметром. Таким образом, тело функции вычисляется в контексте, включающим связи каждого из параметров с фактическими значениями. В примере тело функции вычисляется дважды, сначала в контексте {x → 2, y → 3}, затем в контексте {x → 3, y → 2}. Первый вызов функции f дает результат 7, второй – 8, а вычисление всего выражения – 15.
Если λ-выражение содержит свободные переменные, то для корректного вычисления функции необходимо знать значения, с которыми были связаны свободные переменные при определении функции. Это делается с помощью замыкания, которое состоит из λ-выражения и контекста, определяющего значения свободных переменных. Например, в выражении {пусть y=3 в {пусть f=λ(x)2⋅x+y в {пусть y=4 в f(2)}}} вычисление выражения f(2) идет в контексте {f → [λ(x)2⋅x+y, {y → 3}], y → 4}.
Можно отметить эквивалентность блока "пусть" и λ-выражения: {пусть x1=e1; …; xk=ek в e} ≡ (λ(x1, …, xk) e)(e1, …, ek).
Теперь рассмотрим блоки "пустьрек" и "гдерек", в которых определения взаимно рекурсивны. Если все выражения e1, e2, …, ek являются λ-выражениями, то значением для каждого из xi является замыкание [ei, α], где α – это контекст {x1 → [e1, α], x2 → [e2, α], …, xk → [ek, α]}. Таким образом, контекст рекурсивного блока определений содержит сам себя в качестве составной части.
Упражнения
Переведите несколько программ из предыдущих упражнений на язык Scheme и Kotlin.