Как я могу сериализовать функции во время выполнения в Clojure?

Есть ли способ сериализовать функции во время выполнения в Clojure? Я хотел бы иметь возможность отправлять функции без сохранения состояния (но не чистые) по сети в сериализованном формате (вероятно, edn, но я открыт ко всему).


Например...

Если я запускаю prn-str для функции, я не получаю того, что ожидал/хотел.

user=> (def fn1 (fn [x] (* x 2)))
#'user/fn1
user=> (def data {:test 1 :key "value"})
#'user/data
user=> (defn fn2 [x] (* x 2))
#'user/fn2
user=> (prn-str fn1)
"#object[user$fn1 0x28b9c6e2 \"user$fn1@28b9c6e2\"]\n"
user=> (prn-str data)
"{:test 1, :key \"value\"}\n"
user=> (prn-str fn2)
"#object[user$fn2 0x206c48f5 \"user$fn2@206c48f5\"]\n"
user=> 

Я бы хотел/ожидал что-то вроде этого:

user=> (prn-str fn2)
"(fn [x] (* x 2))\n"

или, может быть,

user=> (prn-str fn2)
"(defn fn2 [x] (* x 2))\n"

person Community    schedule 03.08.2016    source источник
comment
В репл. (source fn2). Покопавшись в паре уровней, скопируйте, чтобы получить исходник в строковой версии, можно увидеть (println (clojure.repl/source-fn 'clojure.repl/source-fn)). На первый взгляд это кажется довольно непрозрачным, но, возможно, его можно изменить, чтобы получить сериализуемую версию функции. Я предполагаю, что в самом общем случае это не сработает.   -  person Shannon Severance    schedule 04.08.2016


Ответы (6)


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

Мы должны иметь возможность сериализовать функцию в байтовый массив и отправить его по сети. Я подозреваю, что вам нужно будет получить объект java.lang.Class функции, а затем передать его через java.lang.instrument.ClassFileTransformer, чтобы получить байты. Получив их, вы можете передать их дружественному java. .lang.ClassLoader на удаленном jvm.

person CurtainDog    schedule 08.08.2016

Вам нужно будет использовать quote или ' для предотвращения оценки и eval для принудительной оценки:

(def fn1 '(fn [x] (* x 2)))
(prn-str fn1) ;;=> "(fn [x] (* x 2))\n"
((eval fn1) 1) ;;=> 2
person Michiel Borkent    schedule 03.08.2016
comment
Но что, если у вас уже есть функция, а не ее исходный код, и вы хотите сериализовать саму функцию? Я почти уверен, что Datomic поддерживает это; как вы можете сделать это в Clojure? - person Sam Estep; 04.08.2016
comment
@SamEstep После компиляции функции у вас нет доступа к исходному коду, только к объекту функции. Однако переменные могут иметь метаданные, описывающие, где можно найти источник. Вот как работает source. - person Michiel Borkent; 04.08.2016
comment
Я полностью осознаю это. Это не то, о чем я спрашивал; Я специально сказал, что спрашивал о случае, когда у вас нет доступа к исходному коду. - person Sam Estep; 04.08.2016

Flambo, оболочка Clojure для Spark, использует библиотеку serializable-fn для сериализации функций (которые Spark требует ). Sparkling, еще одна оболочка для Spark, использует собственные функции Clojure через этот абстрактный класс Java, реализующий интерфейс Java Serializable.

person Elango    schedule 06.08.2016
comment
Обратите внимание, что сериализация функций Clojure в Sparkling на самом деле сериализует два символа: пространство имен и имя fn. При их десериализации требуется пространство имен и возвращается ссылка на fn. То есть тело fn вообще не сериализуется. Ожидается, что код Clojure будет запечен в uberjar и запущен в кластере (поэтому весь код уже существует на каждом воркере). Динамическое определение функций Clojure и их передача операции Spark не поддерживаются. - person Blake Miller; 09.08.2016

У вас есть два основных варианта:

  • передать исходный код (s-выражения хранятся как данные clojure)
  • передавать файлы jar и загружать их с другой стороны.

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

(domain-functions '[(defn foo [x] x)
                    (defn bar [y] (inc y)]

затем вы можете сохранить это в базе данных, и каждый клиент может передать ее read, и тогда все они будут иметь одинаковые функции.

Второй вариант зависит от того факта, что каждый раз, когда вы определяете функцию, она создает файл класса в каталоге /target, а затем загружает его. Затем вы можете синхронизировать этот каталог и загрузить их с другой стороны. Этот подход, конечно, совершенно сумасшедший, хотя люди здесь делают сумасшедшие вещи. Я рекомендую первый подход


И личное примечание:

Я делаю это сейчас с datomic, и я перенял практику помещения git-хэша в имя функции с помощью макроса, поэтому я абсолютно точно знаю, что когда я вызываю функцию, я получаю ту же функцию, которую я вижу в редакторе. Это обеспечивает душевное спокойствие при запуске множества экземпляров, которые все извлекают из одной и той же БД.

person Arthur Ulfeldt    schedule 03.08.2016
comment
Из того, что я слышал, ваш второй подход в основном заключается в том, как работает Hadoop. Но я согласен, что это звучит безумно. - person ; 04.08.2016

Вы можете использовать clojure.repl/source.

(with-out-str (source filter))

чтобы получить строку или

(read-string (with-out-str (source filter)))

чтобы получить список clojure.

person skrat    schedule 03.08.2016

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

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

person Timothy Baldridge    schedule 04.08.2016
comment
Но это ЛИСП. Что случилось с обещанием, что код — это данные, данные — это код? - person ; 04.08.2016
comment
Много раз это обещание интерпретируется просто как означающее, что язык является гомоиконичным и поддерживаются макросы & eval. Реализации не часто интерпретируют это как означающее, что мы предоставляем чрезвычайно динамичный интроспективный доступ к механизмам компилятора, вероятно, потому, что предоставление этих функций обычно снижает производительность. - person amoe; 04.08.2016
comment
Конечно, @BlueJ774, но вы не можете просто взять фрагмент данных, отправить его куда-нибудь и ожидать, что он запустится. Есть такие вещи, как локальное состояние, версии виртуальных машин, языковые версии и т. д., которые могут вызывать проблемы. Код также непрозрачен после компиляции, поэтому он является очень плохим методом коммуникации. Сети предназначены для передачи данных, и мы должны использовать их для передачи данных, а не кода. - person Timothy Baldridge; 04.08.2016