Идиоматический Clojure эквивалент этого кода Python? - PullRequest
3 голосов
/ 04 января 2012

Я написал простую основанную на стеке виртуальную машину на Python, и теперь я пытаюсь переписать ее на Clojure, что оказывается непросто, поскольку у меня нет большого опыта работы с Lisp. Этот фрагмент Python обрабатывает байт-код, который представляется в виде списка кортежей, например:

[("label", "entry"),
 ("load", 0),
 ("load", 1),
 ("add",),
 ("store", 0)]

Или в Clojure:

[[:label :entry]
 [:load 0]
 [:load 1]
 [:add]
 [:store 0]]

Когда объект Functionзагружает байт-код, каждый кортеж «label» обрабатывается специально, чтобы пометить эту позицию, в то время как каждый другой кортеж остается в конечном байт-коде.Я бы предположил, что эквивалент этой функции Clojure будет включать в себя сгиб, но я не уверен, как это сделать элегантным или идиоматическим способом.Есть идеи?

Ответы [ 4 ]

10 голосов
/ 04 января 2012

Читая этот фрагмент Python, похоже, что вы хотите, чтобы конечный результат выглядел следующим образом:

{:code [[:load 0]
        [:load 1]
        [:add]
        [:store 0]]
 :labels {:entry 0}}

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

(defn load [asm]
  (reduce (fn [{:keys [code labels]} [op arg1 & args :as instruction]]
            (if (= :label op)
              {:code code
               :labels (assoc labels arg1 (count code))}
              {:code (conj code instruction)
               :labels labels}))
          {:code [], :labels {}},
          asm))

Edit

Эта версия поддерживает аргумент name,и упрощает этап сокращения, не повторяя элементы, которые не меняются.

(defn load [name asm]
  (reduce (fn [program [op arg1 :as instruction]]
            (if (= :label op)
              (assoc-in program [:labels arg1] (count (:code program)))
              (update-in program [:code] conj instruction)))
          {:code [], :labels {}, :name name},
          asm))
3 голосов
/ 08 января 2012

Я не могу гарантировать, что это идиоматическая Clojure, но это функциональная версия вашего кода на Python, которая должна, по крайней мере, приблизить вас.

(def prog [
 [:label :entry]
 [:load 0]
 [:load 1]
 [:add]
 [:store 0]])

(defn parse [stats]
    (let [
        f (fn [[out-stats labels pc] stat]
            (if (= :label (first stat))
                [out-stats (conj labels [(second stat) pc]) pc]
                [(conj out-stats stat) labels (+ 1 pc)]))
        init [[] {} 0]
        ]
        (reduce f init stats)))

(println (parse prog))

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

В нашем случае мы будем использовать трехпараметрическую версию Reduce - это позволяет нам указать начальное значение аккумулятора. Мы должны сделать это, потому что мы собираемся отслеживать много состояний, когда мы перебираем коллекцию байт-кодов, а версия с двумя параметрами в значительной степени требует, чтобы ваш аккумулятор был похож на элементы в списке. (ср. (reduce + [1 2 3 4]))

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

  • Выходные операторы (self.code)
  • Отображение метки (self.labels)
  • Счетчик программ (pc)

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

Этот предыдущий бит является наиболее важной частью.

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

  • Добавить новое отображение метки
  • Добавить новый вывод программы и увеличить счетчик программы

А теперь вывод:

[[[:load 0] [:load 1] [:add] [:store 0]] 
 {:entry 0}
 4]

Это 3-элементный вектор. Первым элементом является программа. Второй элемент - это отображение меток. Третий элемент - это следующее значение ПК. Теперь вы можете изменить синтаксический анализ, чтобы получить только два значения; это не лишено смысла. Есть причины, по которым вы, возможно, не захотите этого делать, но это больше проблема дизайна API, чем что-либо еще. Я оставлю это как упражнение читателю.

Я должен также упомянуть, что изначально я пропустил блок let и просто указал именованные значения. Я решил вытащить их, чтобы улучшить читаемость. Опять же, я не знаю, что более идиоматично. Это может быть больше соглашения для каждого проекта.

Наконец, я не знаю, действительно ли в сообществе Clojure возникли монады, но вы также можете создать монаду для разбора байт-кода и определить операции «add-Statement» и «add-label» как значения. в этой монаде. Это значительно увеличило бы сложность настройки, но упростило бы фактический код синтаксического анализа. Фактически, это позволит вашему коду анализа выглядеть довольно процедурно, что может быть, а может и не быть хорошей вещью. (не волнуйтесь, он по-прежнему функционален и не имеет побочных эффектов; монады просто позволяют скрыть подключение.) Если ваш пример Python довольно типичен для данных, которые вам нужно обрабатывать, то монады почти наверняка не требуют дополнительных затрат. С другой стороны, если вам действительно нужно управлять гораздо большим состоянием, чем указано в вашем образце, то монады могут помочь вам сохранить здравый смысл.

1 голос
/ 04 января 2012
(defn make-function [name code]
  (let [[code labels] (reduce (fn [[code labels] inst]
                                (if (= (first inst) :label)
                                  [code (assoc labels (second inst) (count code))]
                                  [(conj code inst) labels]))
                              [[] {}] ;; initial state of code and labels
                              code)]
    {:name name, :code code :labels labels}))

На мой взгляд, оно немного широкое, но не слишком плохо.

0 голосов
/ 11 января 2012

Я собираюсь дать вам общее решение для такого рода проблем.

Большинство циклов можно сделать без особых усилий с прямой линией map , filter или уменьшите , и если ваша структура данных является рекурсивной, то, естественно, цикл будет рекурсивным.

Однако ваш цикл представляет собой цикл другого типа.Ваш цикл накапливает результат - что предполагает использование уменьшение - но цикл также несет локальную переменную ( pc ), так что это не прямое уменьшение.

Это довольно распространенный вид цикла.Если бы это был Ракетка , я бы использовал для / fold1 , но, поскольку это не так, нам придется вставить ваш цикл в уменьшить .

Давайте определим функцию с именем load , которая возвращает две вещи: обработанный код и обработанные метки.Я также буду использовать вспомогательную функцию с именем is-label? .

(defn load [asm]
  (defn is-label? [x] (= (first x) :label))
  {:code <<< CODE GOES HERE >>>

   :labels
   <<< CODE GOES HERE >>>
  })

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

Чтобы получить код, нам просто нужно удалить метки.Это вызов filter .

  {:code (filter (complement is-label?) asm)

   :labels
   <<< CODE GOES HERE >>>
  }

Как правило, Reduce имеет только один аккумулятор, но вашему циклу нужны два: результат и локальная переменная pc .Я упакую эти два в вектор, который будет немедленно деконструирован телом цикла.Два слота вектора будут моими двумя локальными переменными.

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

   (first
    (reduce
     (fn [[result, pc] inst]

        << MORE CODE >>

     [{} 0] asm))

(Обратите внимание, как исходные значения для переменных расположены далеко от их объявления. Если тело длинное, это может быть трудно для чтения. Вот проблема, которую решает Ракет для / fold1 .)

Один раз уменьшить возвращает, я вызываю first чтобы сбросить локальную переменную pc и сохранить только результат.

Заполнение тела циклапрямо вперед.Если инструкция является меткой, assoc в результате, в противном случае увеличьте pc на единицу.В любом случае я создаю вектор, содержащий новые значения для всех локальных переменных.

     (fn [[result, pc] [_ arg :as inst]]
       (if (is-label? inst)
         [(assoc result arg pc) pc]
         [result (inc pc)]))

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

(defn load [asm]
  (defn is-label? [x] (= (first x) :label))
  {:code (filter (complement is-label?) asm)

   :labels
   (first
    (reduce
     (fn [[result, pc] [_ arg :as inst]]
       (if (is-label? inst)
         [(assoc result arg pc) pc]
         [result (inc pc)]))
     [{} 0] asm))})
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...