350 likes | 564 Views
Функциональные языки. Взгляд изнутри Турдаков Денис ВМиК МГУ / ИСП РАН. План доклада. Краткое введение в Scheme Проблемы и их решения Пример. Краткое введение в Scheme [1]. Код программы на языке Scheme состоит из выражений, которые записываются в префиксной форме
E N D
Функциональные языки Взгляд изнутри Турдаков Денис ВМиК МГУ / ИСП РАН
План доклада • Краткое введение в Scheme • Проблемы и их решения • Пример
Краткое введение в Scheme [1] Код программы на языке Scheme состоит из выражений, которые записываются в префиксной форме (имя_функции аргументы) Например, математическое выражение 2 + 3 * 4 в Scheme запишется как(+ 2 (* 3 4)) C/C++: ((lower < k) && (k < upper) Scheme: (< lower k upper) C/C++: ((2 * 3) + (5 * 6)) Scheme: (+ (* 2 3) (* 5 6)) C/C++: func(x, y) Scheme: (func x y) C/C++: int sq(int x) {return (x * x)} Scheme: (define (sq x) (* x x))
Краткое введение в Scheme [2] • Функции Scheme – объекты первого класса • (lambda (аргументы) (тело)) • (lambda (x) (* x x)) • (define (sq x) (* x x)) • (define sq (lambda (x) (* x x))) • Пример функции высокого порядка • (map (lambda (x) (* x 2))(list 1 2 3 4 5)) • > результат (2 4 6 8 10)
Краткое введение в Scheme [3] • Определение глобальных переменных: • C/C++: int k 10; • Scheme: (define k 10) • Определение локальных переменных: • C/C++: { • int k 10; • int m 11; • /* некоторый код */ • } • Scheme: (let ((k 10) • (m 11)) • ; некоторый код • )
Краткое введение в Scheme [4] • Условные выражения: • C/C++: if (a > 0) • {/* ветвь 1 */} • else • {/* ветвь 2 */} • Scheme: (if (> a 0) • ( ; ветвь 1) • ( ; ветвь 2) • ) • Массивы • C/C++: int m[] = { 1, 2, 3, 4, 5 }; • m[2] • Scheme: (define m (vector 1 2 3 4 5)) • (vector-ref m 2) ; вернет «3» - элемент массива m с номером 2 (нумерация с нуля)
Основные принципы компиляции программ на языке Scheme Есть в Scheme и нет в С • функции высокого порядка и коррелирующая с ними проблема связывания переменных; • требование эффективно выполнять хвостовые вызовы и связанная с этим проблема роста остаточных вычислений; • возможность работать с остаточными вычислениями как с объектами первого класса, что усложняет предыдущую проблему; • а также, автоматическое управление памятью и сборка мусора.
Связывание переменных • (let ((n 1)) (let ((f (lambda (x) (+ x n))))) • (let ((n 10)) • (f 1))) • Для функции (lambda (x) (+ x n)) • x – связанная переменная • n – свободная переменная • Статическое связывание: => 2 • Динамическое связывание: => 11
Проблема свободных переменных • Проблема: доступ к свободным переменным (lambda (x y) (let ((f (lambda (a b) (+ (* a x) (* b y))))) (- (f 1 2) (f 3 4)))) • Как получить доступ к переменным x и y из тела функции f? • Поместим значения свободных переменных в объект, в котором также будет содержаться код функции. • Поставим задачу реализовать замыкания средствами самого Scheme.
Lambda lifting • Первое решение: добавить значения свободных переменных как параметры (lambda (x y) (let ((f (lambda (x y a b) (+ (* a x) (* b y))))) (- (f x y 1 2) (f x y 3 4)))) • Такое преобразование называется lambda lifting и работает хорошо не всегда (lambda (x y) (let ((f (lambda (a b) (+ (* a x) (* b y))))) f))
Преобразование в замыкания • Второе решение: Построить структуру содержащую свободные переменные и передать ее в функцию как параметр при вызове • Такой объект называетсязамыкание (closure). (lambda (x y) (let ((f (vector (lambda (self a b) (+ (* a (vector-ref self 1)) (* b (vector-ref self 2)))) x y))) (- ((vector-ref f 0) f 1 2) ((vector-ref f 0) f 3 4))))
Правила преобразования • (lambda (P1 … Pn) E)= • (vector (lambda (self P1 … Pn) E) v…) • где v… список свободных переменных функции (lambda (P1 … Pn) E) • v = (vector-ref self i) • где vсвободная переменная, аi– позиция v в списке свободных переменных незамкнутого lambda-выражения • (f E1... En) = ((vector-reff 0) f E1 … En )
Рост остаточных вычислений (define fact-iter (lambda (n) (fact-iter-acc n 1))) (define fact-iter-acc (lambda n a) (if (zero? n) a (fact-iter-acc (- n 1) (* n a))))) (fact-iter 4) =(fact-iter-acc 4 1) =(fact-iter-acc 3 4) =(fact-iter-acc 2 12) =(fact-iter-acc 1 24) =(fact-iter-acc 0 24) =24 (define fact (lambda (n) (if (zero? n) 1 ( * n (fact (- n 1)))))) (fact 4) =(* 4 (fact 3)) =(* 4 (* 3 (fact 2))) =(* 4 (* 3 (* 2 (fact 1)))) =(* 4 (* 3 (* 2 (* 1 (fact 0))))) =(* 4 (* 3 (* 2 (* 1 1)))) =(* 4 (* 3 (* 2 1))) =(* 4 (* 3 2)) =(* 4 6) =24
Рост стека тв3 Первый случай 2 тв2 тв2 3 3 тв1 тв1 тв1 … 4 4 4 Второй случай 1 4 12 24 24 4 4 3 2 1 0
Критерии роста • Остаточные вычисления не растут тогда и только тогда, когда все вызовы хвостовые • Строгое определение хвостовых вызовов задается синтаксически
Хвостовые вызовы • Последнее выражение в теле lambda-выражения – хвостовое <tail expression> • (lambda <formals> <definition>* <expression>* <tail expression>) • Если одно из следующих выражений хвостовое, то подвыражение, показанное, как <tail expression>, тоже хвостовое • (if <expression> <tail expression> <tail expression>) • (if <expression> <tail expression>) • (cond <cond clause>+) • (cond <cond clause>* (else <tail sequence>)) • (case <expression> <case clause>+) • (case <expression> <case clause>* (else <tail sequence>)) • (and <expression>* <tail expression>) • (or <expression>* <tail expression>) • (let (<binding spec>*) <tail body>) • (let <variable> (<binding spec>*) <tail body>) • (let* (<binding spec>*) <tail body>) (letrec (<binding spec>*) <tail body>) (let-syntax (<syntax spec>*) <tail body>) (letrec-syntax (<syntax spec>*) <tail body>) (begin <tail sequence>) (do (<iteration spec>*) (<test> <tail sequence>) <expression>*) где <cond clause> ---> (<test> <tail sequence>) <case clause> ---> ((<datum>*) <tail sequence>) <tail body> ---> <definition>* <tail sequence> <tail sequence> ---> <expression>* <tail expression>
Остаточные вычисления • Остаточные вычисления (Continuations) – что-то, что ждет значение. Например, следующая программа: (sqrt (+ (read) 1)) • будет ждать, пока пользователь не введет число. • Остаточные вычисления для функции foo в выражении (* (foo 1) (+ 5 1)) – (lambda (x) (* x (+ 5 1))) • Остаточные вычисления – динамические объекты. На каждом шаге программы существуют текущие (current) остаточные вычисления • Остаточные вычисления в Scheme являются объектами первого класса
Остаточные вычисления,как объекты первого класса • (call/cc (lambda (cont) <body>)) – превращает остаточные вычисленияв функцию c одним параметром (sqrt (+ (call/cc (lambda (cont) (* 2 (cont 8)))) 1)) (sqrt (+ 8 1)) => (define saved-cont #f ) (display (call/cc (lambda (cont) (set! saved-cont cont)))) (saved-cont 8) (saved-cont 16)
Пример 1 (define saved-cont #f ) (display (* 2 (call/cc (lambda (cont) (set! saved-cont cont))) )) (saved-cont 8); => 16 (saved-cont 16); => 32
Пример 2 Обработка исключений: (define (map-/ lst) (call/cc (lambda (return) (map (lambda (x) (if (= x 0) (return #f) (/ 1 x))) lst)))) (map-/ '(1 2 3)) => (1 1/2 1/3) (map-/ '(1 0 3)) => #f
CPS-преобразование (1) • Итог: мы показали, что • 1) Рост остаточных вычислений приводит к неэффективному использованию памяти • 2) Наличие call/cc приводит к тому, что остаточные вычисления • Могут вызываться более одного раза.Пример X2 = Y2 + Z2 • Существовать неограниченное время • Становится очевидной необходимость преобразования программы к виду, где все вызовы являются хвостовыми • Самое удачное решение – выделение остаточных вычислений в отдельные функции • Пример: • (let ((square (lambda (x) (* x x)))) • (display (+ (square 10) 1))) • Остаточные вычисления для(square 10)будут ждать значение, прибавит к нему 1 и выведет результат
CPS-преобразование (2) • Остаточные вычисленияпредставляются функцией • (lambda (r) (display (+ r 1))) • Эти остаточные вычислениянеобходимо передать square, и функция сможет переслать результат • Итак, мы должны добавить continuation-параметр всем lambda-выражениям, изменить вызовы функций для передачи continuation и использовать continuation каждый раз, когда должны вернуть результат. • (CPS = Continuation-Passing Style) • (let ((square (lambda (k x) (k (* x x))))) • (square (lambda (r) (display (+ r 1))) • 10))
СущностьCPS-преобразования Пример: (let ((mult (lambda (a b) (* a b)))) (let ((square (lambda (x) (mult x x)))) (display (+ (square 10) 1)))) преобразовывается в (let ((mult (lambda (k a b) (k (* a b))))) (let ((square (lambda (k x) (mult k x x)) (square (lambda (r) (display (+ r 1))) 10))) вызов mult в square – хвостовой, поэтому mult имеет такие же остаточные вычисления, как и square, а для нехвостовых вызовов через continuation-параметр передаются все остаточные вычисления
РезультатCPS-преобразования • Когда CPS-преобразование будет окончено все вызовы функций приобретут хвостовую форму • Вызовы функций могут быть легко представлены в виде переходов (jumps) с текущим набором параметров, то есть не надо хранить в стеке (при стековой модели вычислений) точку возврата и предыдущий контекст
Правила CPS-преобразования E С Обозначение: - CPS-преобразование выражения E, где С – continuation E Е – исходное выражение (может содержать не хвостовые вызовы). С – выражение в CPS-форме (содержит только хвостовые вызовы) С – либо переменная либо lambda-выражение program = program (lambda (r) (halt r)) Первое правило: Означает что самый первый continuation программы получает r, результат программы, и вызывает операцию (halt r) завершающую вычисления.
Правила CPS-преобразования • v = (С v) • C • (set! v E1) = E1 • C (lambda (r) • (C (set! v r))) • (if E1 E2 E3) = E1 • C (lambda (r) (if r E2 E3 )) • C C • (begin E1 E2) = E1 • C (lambda (r) E2 ) • C • (lambda (P1 … Pn) E0) = • C (C (lambda (k P1 … Pn) E0 ) • k
Правила CPS-преобразования • (+ E1 E2) = E1 • C (lambda (r1) E2 ) • (lambda (r2) (C (+ r1 r2))) • (E0) = E0 • C (lambda (r) (r C)) • (E0 E1) = E0 • C (lambda (r0) E1 ) • (lambda (r1) (r0 C r1)) • (E0 E1 E2) = E0 • C (lambda (r0) E1 ) • (lambda (r1) E2 ) • и т. д. (lambda (r2) (r0 C r1 r2))
Правила CPS-преобразования • ((lambda () E0)) = E0 • C C • ((lambda (P1) E0) E1) = E1 • C (lambda (P1) E0) • C • ((lambda (P1 P2) E0) E1 E2) = • C • E1 • (lambda (P1) E2 ) • (lambda (P2) E0 ) • C • и т. д.
сall/cc и CPS-преобразование • (sqrt (+ (call/cc • (lambda (cont (* 2 (cont 8)))) 1)) • Преобразование call/cc: • (define (cps-call/cc k consumer) • (let ((reified-current-continuation • (lambda (k1 v) (k v)))) • (consumer k reified-current-continuation)) • Это определение добавляется в текст программы если используется call/cc • (call/cc-cps (lambda (r0) • (lambda (r1) (sqrt r1)) (+ r0 1)) • (lambda (C cont) (C (* 2 • (cont (lambda (x) x) 8)))))
Трансляция в С • closure-passing style • GambitScheme • GVM • Регистры: В R0 – метка, В R1..RN – параметры • Глобальные переменные и метки заносятся в таблицы с помощью специальных команд • В начале каждой функции ставится глобальная метка, с помощью которой можно вызвать функцию. • После завершения работы операции, результат возвращается в регистре R1.
Пример [1] • Исходный текст: • (define square (lambda (x) (* x x)) • (+ (square 5) 1) • После CPS-преобразования: • (define square (lambda (r1 x) (r1 (* x x)))) • (square (lambda (r3) • (let ((r2 (+ r3 1))) (halt r2))) 5) • После преобразования замыканий • (define square (vector (lambda (self1 r1 x) • ((vector-ref r1 0) r1 (* x x))))) • ((vector-ref square 0) square • (vector (lambda (self2 r3) (let ((r2 (+ r3 1))) (halt r2)))) 5)
Пример [2] #include “gambit.h” – содержит описание всех используемых макросов … - некоторые определения, не существенные для данной статьи Далее идут команды виртуальной машины, которые являются макросами языка C. Их расшифровка представляет собой чисто техническую задачу, и любознательный читатель может посмотреть исходный код Gambit Scheme. BEGIN_P_COD – начало программы DEF_GLBL (L_MAIN) – метка на начало программы SET_STK (1, R0) – поместить в вершину стека регистр R0 SET_R1 (FIX (5L)) – положить в регистр R1 значение 5 SET_R0 (LBL (1)) – после выполнения операции совершим переход на метку 1 OP_JMP (PARAMS (2), G_SQUARE) – выполнить двухместную операцию G_SQUARE и перейти на метку, содержащуюся в R0 DEF_SLBL (1, _MAIN_1) – локальная метка в теле программы с номером 1 SET_R2 (FIX (1L)) – положить в регистр R2 значение 1 SET_R0 (STK (-1)) – взять из стека метку конца программы и положить ее в R0 OP_JMP (PARAMS (2), _plus) – сложить значения в регистрах R1 и R2, результат поместить в регистр R1 и прейти по метке в регистре R0 (конец программы) END_P_COD – конец программы BEGIN_P_COD – начало функции SQUARE DEF_GLBL (L_SQUARE) – метка на начало функции SQUARE SET_R2 (R1) – поместить в регистр R2 такое же значение как и в R1 OP_JMP (PARAMS (2), mul) – перемножить регистры R1 и R2, результат положить в регистр R1 и перейти по метке в регистре R0 (на метку _MAIN_1) END_P_COD – конецфункции SQUARE
Литература • “Essential Of Programming Languages”, second edition, Daniel P. Friedman, Mitchell Wand, Christopher T. Haynes, The MIT Press, Cambridge, Massachusetts, London, Engalnd, 2001. • “Continuations”, Shriram Krishnamurthi, 2001-10-12 • “CONS Should not CONS its Arguments, or, a Lazy Alloc is a Smart Alloc”, Henry G. Baker, ACM Sigplan Notices 27, 3 (Mar 1992), 24-34 • “The 90 minute Scheme to C compiler”, Mark Feeley, Universite de Montreal • “Rabbit: A compiler for Scheme”, Guy Lewis Steele, MIT Artificial Intelligence Laboratory • “A Runtime System”, Andrew W. Appel, Princeton University, CS-TR-220-89, May 1989 • “On the Overhead of CPS”, Oliver Danvy, Belmina Dzafic, Frank Pfenning, 1996-11-18 • “Implementation Strategies for Continuations”, Willam D. Ctinger, Erie M. Ost, 1988 ACM 0-09791-273-X/88/0007/0124 • “Orbit: An Optimizing Compiler for Scheme”, David Kranz, Richard Kelsey, Jonathan Rees, Paul Hudak, ACM Sigplan, Best of PLDI 1979-1999, 175-191 • “CPS Recursive Ascent Parsing”, Arthur Nunes-Harwitt • “Shift to control”, Chung-chieh Shan, Harvard University • “Continuation-Passing, Closure-Passing Style”, Andrew W. Appel, Trevor Jimt, CS-TR-183-88, 1988 • Three Steps for the CPS Transformation (detail abstract)”, Oliver Danvy, Department of Computing and Information Science, Kansas State University, 1991-12
Спасибо • e-mail: turdakov@gmail.com • Презентация доступна на сайте http://modis.ispras.ru/turdakov/fl.ppt