【问题标题】:How can I serialize functions at runtime in Clojure?如何在 Clojure 运行时序列化函数?
【发布时间】:2016-12-09 18:53:24
【问题描述】:

有没有办法在 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"

【问题讨论】:

  • 在repl中。 (source fn2)。挖掘几个级别,复制以获取字符串版本的源代码可以看到(println (clojure.repl/source-fn 'clojure.repl/source-fn))。乍一看,它似乎很不透明,但也许可以修改为给定一个可序列化的 func 版本。我的猜测是在最一般的情况下是行不通的。

标签: function serialization clojure edn


【解决方案1】:

您必须使用quote' 来阻止评估,并使用eval 来强制评估:

(def fn1 '(fn [x] (* x 2)))
(prn-str fn1) ;;=> "(fn [x] (* x 2))\n"
((eval fn1) 1) ;;=> 2

【讨论】:

  • 但是如果你已经有一个函数,而不是它的源代码,你想序列化函数本身呢?我很确定 Datomic 支持这一点。如何在 Clojure 中做到这一点?
  • @SamEstep 函数编译完成后,您无法访问源代码,只能访问函数对象。但是,var 可以具有描述可以在何处找到源的元数据。这就是source 的工作原理。
  • 我完全清楚这一点。那不是我要问的;我特别说我问的是你没有访问源代码的情况。
【解决方案2】:

Flambo 是 Spark 的 Clojure 包装器,它使用 serializable-fn 库来序列化函数(Spark 需要)。 Sparking 是 Spark 的另一个包装器,它通过实现 Java 接口 Serializable 的 this Java abstract class 使用本机 Clojure 函数。

【讨论】:

  • 请注意,Sparkling 对 Clojure 函数的“序列化”实际上是在序列化两个符号:命名空间和 fn 的名称。在反序列化它们时,它需要命名空间并返回对 fn 的引用。也就是说,fn 的主体根本没有被序列化。 Clojure 代码预计将被烘焙到一个 uberjar 中并在集群上启动(因此所有代码都已经存在于每个 worker 上)。不支持动态定义 Clojure 函数并将它们传递给 Spark 操作。
【解决方案3】:

你基本上有两种选择:

  • 传递源代码(存储为 clojure 数据的 s 表达式)
  • 传递 jar 文件并将它们加载到另一端。

对于第一个选项,您在编译函数时保存源代码(几乎总是在定义函数时),然后将相同的源表达式传递给另一台计算机并让它编译相同事物。所以首先你可以制作一个表达式向量:

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

然后您可以将其存储到数据库中,每个客户端都可以将其传递给read,然后它们将具有相同的功能。

第二个选项取决于这样一个事实,即每次定义一个函数时,它都会在 /target 目录中生成一个类文件,然后加载它。然后,您可以同步此目录并将它们加载到另一端。这种方法当然是完全疯狂的,尽管人们在这里做疯狂的事情。我推荐第一种方法


作为个人说明:

我现在正在使用 datomic 执行此操作,并且我采用了使用宏将 git-hash 放入函数名称的做法,所以我绝对知道当我调用一个函数时,我得到的结果是一样的我在编辑器中看到的功能。当运行所有从同一个数据库中提取的许多实例时,这让您高枕无忧。

【讨论】:

  • 据我所知,您的第二种方法基本上是 Hadoop 的工作原理。但我同意这听起来很疯狂。
【解决方案4】:

在某些时候它不再是 Clojure,因此我们可以任意往返于源代码和机器指令并返回的期望有点偏离。

我们应该能够将函数序列化为字节数组并通过网络发送。我怀疑您需要获取函数的 java.lang.Class 对象,然后通过 java.lang.instrument.ClassFileTransformer 传递它以获取字节。一旦你有了这些,你可以将它们传递给远程 jvm 上的友好 java.lang.ClassLoader

【讨论】:

    【解决方案5】:

    你可以使用clojure.repl/source

    (with-out-str (source filter))
    

    获取字符串,或

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

    获取 clojure 列表。

    【讨论】:

      【解决方案6】:

      确实没有什么好的方法,出于充分的理由,简单地将一个函数传送到另一台计算机或将其存储在数据库中可能会导致很多问题,其中最重要的是该函数可能需要其他函数不是在另一端。

      一个更好的主意是坚持数据。不要编写函数,而是将函数的名称写为事件,然后甚至可以稍后通过读取数据的任何内容进行翻译。坚持数据,这是惯用的方式。

      【讨论】:

      • 但这是 LISP。 “代码就是数据,数据就是代码”的承诺发生了什么?
      • 很多时候,这个承诺被解释为仅仅意味着该语言是同音字的,并且支持宏和eval。实现通常不会将其解释为“我们提供对编译器机制的极其动态的内省访问”,可能是因为提供这些功能通常是性能杀手。
      • 当然@BlueJ774,但您不能只获取数据片段并将其发送到某处并期望它运行。诸如本地状态、VM 版本、语言版本等都可能导致问题。代码在编译后也是不透明的,因此它是一种非常糟糕的通信方法。网络旨在传输数据,我们应该将它们用于传输数据,而不是代码。
      猜你喜欢
      • 1970-01-01
      • 2010-12-14
      • 1970-01-01
      • 2023-03-26
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-04-23
      • 1970-01-01
      相关资源
      最近更新 更多