Реализовать собственную ссылку / указатель на чистом / функциональном языке (Elm / Haskell) - PullRequest
4 голосов
/ 29 февраля 2020

Аннотация Проблема:

Я хотел бы реализовать самотсылку / указатель в Elm.

Specifi c Проблема:

Я пишу Игрушечный интерпретатор LISP в Elm, вдохновленный mal .

Я пытаюсь реализовать что-то вроде letre c для поддержки рекурсивных и взаимно-рекурсивных привязок (" self reference »и« pointers », о которых я упоминал выше).

Вот пример кода:

(letrec
  ([count (lambda (items)
            (if (empty? items)
                0
                (+ 1 (count (cdr items)))
            )
          )
  ])
  (count (quote 1 2 3))
)
;=>3

Обратите внимание, как тело лямбды относится к привязке count. Другими словами, функция нуждается в ссылке на себя.

Более глубокий фон:

Когда лямбда определена, нам нужно создать замыкание функции , которое состоит из трех компоненты:

  1. Тело функции (выражение, которое будет оцениваться при вызове функции).
  2. Список аргументов функции (локальные переменные, которые будут связаны при вызове).
  3. Замыкание (значения всех нелокальных переменных, на которые можно ссылаться в теле функции).

Из статьи в Википедии:

Замыкания обычно реализуются с [...] представлением лексической среды функции (т. Е. Набора доступных переменных) во время создания замыкания. Ссылочная среда связывает нелокальные имена с соответствующими переменными в лексической среде во время создания замыкания, дополнительно продлевая их время жизни, по крайней мере, до времени жизни самого замыкания. Когда замыкание вводится позднее, возможно, в другой лексической среде, функция выполняется с нелокальными переменными, относящимися к тем, которые были получены закрытием, а не к текущей среде.

На основе приведенного выше кода lisp при создании лямбды мы создаем замыкание, переменная count которого должна быть привязана к лямбде, создавая тем самым бесконечную / круговую / самоссылку. Эта проблема усложняется взаимно-рекурсивными определениями, которые также должны поддерживаться letre c.

Elm, будучи чисто функциональным языком, не поддерживает императивную модификацию состояния. Поэтому Я считаю, что в Elm невозможно представить самоссылающиеся значения. Можете ли вы дать некоторые рекомендации по альтернативам реализации letre c в Elm?

Исследования и попытки

Mal в Elm

Йос фон Бакель уже внедрил mal в Elm. Смотрите его заметки здесь и реализацию среды здесь . Он приложил немало усилий, чтобы вручную построить систему указателей с собственным внутренним механизмом G C. Хотя это работает, это похоже на огромную борьбу. Я жажду чисто функциональной реализации.

Mal в Haskell

В реализации mal в Haskell (см. Код здесь ) используется Data.IORef для эмуляции указателей. Это также кажется мне хаком.

Y-Combinator / Fixed Points

Кажется возможным, что Y-Combinator может использоваться для реализации этих собственных ссылок. Кажется, есть Y * Combinator , который работает и для взаимно рекурсивных функций. Мне кажется логичным, что также должен существовать комбинатор Z * (эквивалентный Y *, но поддерживающий модель оценки Elm ). Должен ли я преобразовать все мои letrec экземпляры так, чтобы каждая привязка была обернута вокруг Z *?

Y-Combinator является новым для меня, и мой интуитивный ум просто не понимает этого, поэтому я не уверен Если вышеуказанное решение будет работать.

Заключение

Большое спасибо за чтение! Я не могу хорошо спать в течение многих дней, так как я борюсь с этой проблемой.

Спасибо!

-Advait

Ответы [ 3 ]

2 голосов
/ 01 марта 2020

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

В любом случае, Haskell ответ может быть полезен для кого-то, так что вот так ...

По сути, само-ссылочное значение Haskell легко создается путем введения рекурсивного связывания, такого как:

let mylist = [1,2] ++ mylist in mylist

Тот же принцип можно использовать при написании интерпретатора для построения самоссылающихся значений.

Учитывая следующий простой язык S-выражений для построения потенциально рекурсивных / самоссылающихся структур данных с целочисленными атомами:

data Expr = Atom Int | Var String | Cons Expr Expr | LetRec [String] [Expr] Expr

мы можем написать интерпретатор для оценки его следующего типа, который не использует IORef s или ad ho c указатели или что-нибудь странное, подобное:

data Value = AtomV Int | ConsV Value Value deriving (Show)

Один из таких интерпретаторов:

type Context = [(String,Value)]

interp :: Context -> Expr -> Value
interp _ (Atom x) = AtomV x
interp ctx (Var v) = fromJust (lookup v ctx)
interp ctx (Cons ca cd) = ConsV (interp ctx ca) (interp ctx cd)
interp ctx (LetRec vs es e)
  = let ctx' = zip vs (map (interp ctx') es) ++ ctx
    in  interp ctx' e

Это фактически вычисление в монаде читателя, но я написал его ex неявно, потому что версия Reader потребует использования экземпляра MonadFix либо явно, либо с помощью синтаксиса RecursiveDo, что приведет к затенению деталей.

Ключевой бит кода имеет место для LetRec. Обратите внимание, что новый контекст создается путем введения набора потенциально взаимно рекурсивных привязок. Поскольку оценка ленива, сами значения можно вычислить с помощью выражения interp ctx' es, используя только что созданный ctx', частью которого они являются, связывая рекурсивный узел.

Мы можем использовать наш интерпретатор для создания себя -referencing value, например, так:

car :: Value -> Value
car (ConsV ca _cd) = ca

cdr :: Value -> Value
cdr (ConsV _ca cd) = cd

main = do
  let v = interp [] $ LetRec ["ones"] [Cons (Atom 1) (Var "ones")] (Var "ones")

  print $ car $ v
  print $ car . cdr $ v
  print $ car . cdr . cdr $ v
  print $ car . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr $ v

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

{-# LANGUAGE RecursiveDo #-}
{-# OPTIONS_GHC -Wall #-}

module SelfRef where

import Control.Monad.Reader
import Data.Maybe

data Expr = Atom Int | Var String | Cons Expr Expr | LetRec [String] [Expr] Expr
data Value = AtomV Int | ConsV Value Value deriving (Show)

type Context = [(String,Value)]

interp :: Context -> Expr -> Value
interp _ (Atom x) = AtomV x
interp ctx (Var v) = fromJust (lookup v ctx)
interp ctx (Cons ca cd) = ConsV (interp ctx ca) (interp ctx cd)
interp ctx (LetRec vs es e)
  = let ctx' = zip vs (map (interp ctx') es) ++ ctx
    in  interp ctx' e

interp' :: Expr -> Reader Context Value
interp' (Atom x) = pure $ AtomV x
interp' (Var v) = asks (fromJust . lookup v)
interp' (Cons ca cd) = ConsV <$> interp' ca <*> interp' cd
interp' (LetRec vs es e)
  = mdo let go = local (zip vs vals ++)
        vals <- go $ traverse interp' es
        go $ interp' e

car :: Value -> Value
car (ConsV ca _cd) = ca

cdr :: Value -> Value
cdr (ConsV _ca cd) = cd

main = do
  let u = interp [] $ LetRec ["ones"] [Cons (Atom 1) (Var "ones")] (Var "ones")
  let v = runReader (interp' $ LetRec ["ones"] [Cons (Atom 1) (Var "ones")] (Var "ones")) []

  print $ car . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr $ u
  print $ car . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr . cdr $ v
2 голосов
/ 29 февраля 2020

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

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

Обратите внимание, что если присваивания выполняются в порядке слева направо, и одна из лямбд отправляется во время инициализация, а затем происходит прямой вызов к одной из лямбд через не назначенную переменную, что будет проблемой; например,

(letrec
  ([alpha (lambda () (omega)]
   [beta (alpha)] ;; problem: alpha calls omega, not yet stored in variable.
   [omega (lambda ())])
  ...)

Обратите внимание, что в отчете о схеме R7RS , P16-17, letrec фактически задокументировано, что оно работает следующим образом. Все переменные связаны, а затем им присваиваются значения. Если оценка выражения init относится к той же переменной, которая инициализируется, или к более поздним переменным, которые еще не инициализированы, R7RS сообщает, что это ошибка. В документе также указывается ограничение на использование продолжений, захваченных в выражениях инициализации.

1 голос
/ 09 марта 2020

Комбинатор U

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

Я подозреваю, что есть много информации об этом, но сейчас серьезно сложно найти что-то, что выглядит как * комбинатор (даже сейчас я создание группы компаний под названием «Интеграция по частям» и т. д.)

Вы можете, как вы говорите, сделать это с помощью комбинатора Y, но я не хотел этого делать, потому что Y - это то, что я Я понимаю, что могу понять по несколько часов за один раз, а потом мне нужно все это проработать снова. Но оказывается, что вы можете использовать нечто гораздо более простое: комбинатор U. Кажется, что это еще труднее найти, чем Y, но вот цитата об этом:

В теории языков программирования U комбинатор, U, является математической функцией, которая применяет его аргумент к своему аргументу; то есть U (f) = f (f) или, что то же самое, U = λf. f (f).

Самостоятельное применение позволяет моделировать рекурсию в λ-исчислении, что означает, что U-комбинатор обеспечивает универсальные вычисления. (Комбинатор U на самом деле более примитивен, чем более известный комбинатор Y с фиксированной точкой.)

Выражение U (U), читаемое как U of U, является самой маленькой не завершающей программой, [.. .].

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

Предпосылки

Все следующие примеры кода находятся в Racket. Макросы, безусловно, являются специфичными для Racket c. Чтобы заставить работать макросы, вам понадобится syntax-parse через:

(require (for-syntax syntax/parse))

Однако обратите внимание, что мое использование syntax-parse крайне наивно: я на самом деле просто незамороженный пещерный пещерный человек, притворяющийся, что понимаю Ракетта система макросов.

Также обратите внимание, что я не безжалостно превратил все в λ: в этом коде есть let s, использование нескольких значений, включая let-values, (define (f ...) ...) и т. д.

Две версии U

Первая версия U очевидна:

(define (U f)
  (f f))

Но это столкнется с некоторыми проблемами с языком аппликативного порядка, которым по умолчанию является Racket. , Чтобы избежать этого, мы можем сделать предположение, что (f f) будет функцией, и обернуть эту форму в другую функцию, чтобы отложить ее оценку до тех пор, пока она не понадобится: это стандартный трюк, который вы должны сделать для Y в аппликативном язык заказа также. Я буду использовать аппликативный порядок U только тогда, когда мне нужно, поэтому я дам ему другое имя:

(define (U/ao f)
  (λ args (apply (f f) args)))

Обратите внимание, что я допускаю более одного аргумента вместо выполнения вещь чисто λ-исчисления.

Использование U для построения рекурсивных функций

Для этого мы делаем аналогичный трюк, который вы делаете с Y: напишите функцию, которая, если дана функция в качестве аргумента, который имеет дело с рекурсивными случаями, вернет рекурсивную функцию. И, очевидно, я буду использовать функцию Фибоначчи в качестве канонической рекурсивной функции.

Итак, рассмотрим эту вещь:

(define fibber
  (λ (f)
    (λ (n)
      (if (<= n 2)
          1
          (+ ((U f) (- n 1))
             ((U f) (- n 2)))))))

Это функция, которая, учитывая другую функцию, U из который вычисляет меньшие числа Фибоначчи, вернет функцию, которая вычислит число Фибоначчи для n.

Другими словами, U этой функции - функция Фибоначчи !

И мы можем проверить это:

> (define fibonacci (U fibber))
> (fibonacci 10)
55

Так что это очень приятно.

Завершение U в макрос

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

(define fibber/broken
  (λ (f)
    (let ([fib (U f)])
      (λ (n)
        (if (<= n 2)
            1
            (+ (fib (- n 1))
               (fib (- n 2))))))))

Не пытайтесь вычислить U этого : оно будет бесконечно повторяться, потому что (U fibber/broken) -> (fibber/broken fibber/broken) и это включает в себя вычисления (U fibber/broken), и мы обречены.

Вместо этого мы можем использовать U/ao:

(define fibber
  (λ (f)
    (let ([fib (U/ao f)])
      (λ (n)
        (if (<= n 2)
            1
            (+ (fib (- n 1))
               (fib (- n 2))))))))

И все в порядке ((U fibber) 10) это 55 (и заканчивается!).

И это действительно все, что вам нужно для написания макроса:

(define-syntax (with-recursive-binding stx)
  (syntax-parse stx
    [(_ (name:id value:expr) form ...+)
     #'(let ([name (U (λ (f)
                        (let ([name (U/ao f)])
                          value)))])
         form ...)]))

И это прекрасно работает:

(with-recursive-binding (fib (λ (n)
                               (if (<= n 2)
                                   1
                                   (+ (fib (- n 1))
                                      (fib (- n 2))))))
  (fib 10))

Предупреждение о привязках

Одна довольно очевидная вещь здесь состоит в том, что есть две привязки, построенные этим макросом: внешняя и внутренняя с тем же именем. И они не связаны с одной и той же функцией в смысле eq?:

(with-recursive-binding (ts (λ (it)
                              (eq? ts it)))
  (ts ts))

является #f. Это имеет значение только на языке, где привязки могут быть видоизменены: язык с присваиванием другими словами И внешние, и внутренние привязки, если они не были мутированы, относятся к функциям, которые идентичны функциям : они вычисляют одинаковые значения для всех значений своих аргументов. На самом деле, трудно понять, для каких целей eq? будет использоваться в языке без присваивания.

Это предостережение будет применяться и ниже.

Две версии U для многих функций

Очевидное обобщение U, U * для многих функций состоит в том, что U * (f1, ..., fn) является кортежем (f1 (f1, ..., fn), f2 (f1, ...) , фн), ...). И хороший способ выразить это в Racket - это использовать несколько значений:

(define (U* . fs)
  (apply values (map (λ (f)
                       (apply f fs))
                     fs)))

И нам также понадобится аппликативный порядок:

(define (U*/ao . fs)
  (apply values (map (λ (f)
                       (λ args (apply (apply f fs) args)))
                     fs)))

Обратите внимание, что U * является истинное обобщение U: (U f) и (U* f) одинаковы.

Использование U * для построения взаимно-рекурсивных функций

Я буду работать с тривиальной парой функций:

  • объект является цифрой c деревом , если это минусы и его автомобиль и cdr являются цифрами c объектов;
  • объект является цифра c объект , если это число или дерево цифр c.

Таким образом, мы можем определить функции 'maker' (с помощью '-er' 'соглашение: функция, которая делает x значением x er или, если x содержит дефисы, x - э-э) которые будут выполнять подходящие функции:

(define numeric-tree-er
  (λ (nter noer)
    (λ (o)
      (let-values ([(nt? no?) (U* nter noer)])
        (and (cons? o)
             (no? (car o))
             (no? (cdr o)))))))

(define numeric-object-er
  (λ (nter noer)
    (λ (o)
      (let-values ([(nt? no?) (U* nter noer)])
        (cond
          [(number? o) #t]
          [(cons? o) (nt? o)]
          [else #f])))))

Обратите внимание, что для обоих из них я немного повысил вызов до U*, просто чтобы сделать вызов с соответствующим значением U* меньше непрозрачный.

И это работает:

(define-values (numeric-tree? numeric-object?)
  (U* numeric-tree-er numeric-object-er))

А теперь:

> (numeric-tree? 1)
#f
> (numeric-object? 1)
#t
> (numeric-tree? '(1 . 2))
#t
> (numeric-tree? '(1 2 . (3 4)))
#f

Упаковка U * в макрос

Та же проблема, что и раньше, возникает, когда мы поднимаем внутренний вызов U* с тем же результатом: нам нужно использовать U*/ao. Кроме того, макрос становится значительно более волосатым, и я немного удивлен, что так легко все понял. С концептуальной точки зрения это не сложно: для меня просто не очевидно, что сопоставление с образцом работает.

(define-syntax (with-recursive-bindings stx)
  (syntax-parse stx
    [(_ ((name:id value:expr) ...) form ...+)
     #:fail-when (check-duplicate-identifier (syntax->list #'(name ...)))
     "duplicate variable name"
     (with-syntax ([(argname ...) (generate-temporaries #'(name ...))])
       #'(let-values
             ([(name ...) (U* (λ (argname ...)
                                (let-values ([(name ...)
                                              (U*/ao argname ...)])
                                  value)) ...)])
           form ...))]))

И теперь, в потоке искр, мы можем написать:

(with-recursive-bindings ((numeric-tree?
                           (λ (o)
                             (and (cons? o)
                                  (numeric-object? (car o))
                                  (numeric-object? (cdr o)))))
                          (numeric-object?
                           (λ (o)
                             (cond [(number? o) #t]
                                   [(cons? o) (numeric-tree? o)]
                                   [else #f]))))
  (numeric-tree? '(1 2 3 (4 (5 . 6) . 7) . 8)))

и get #t.


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

...