Serialization Clojure:存储和编译大型派生数据结构

Serialization Clojure:存储和编译大型派生数据结构,serialization,clojure,compilation,tree,flat-file,Serialization,Clojure,Compilation,Tree,Flat File,我有一个很大的数据结构,一棵树,它占用了大约2gb的内存。它包括叶子中的clojure集,以及作为分支的参照。树是通过读取和解析一个大平面文件并将行插入到树中来构建的。然而,这大约需要30秒。是否有一种方法可以构建一次树,将其发送到clj文件,然后将树编译到我的独立jar中,这样我就可以在树中查找值,而无需重新读取大型文本文件?我认为这将删除30秒的树构建,但这也将帮助我部署独立的jar,而不需要文本文件 我的第一次挥杆失败了: (def x (ref {:zebra (ref #{1 2 3

我有一个很大的数据结构,一棵树,它占用了大约2gb的内存。它包括叶子中的clojure集,以及作为分支的参照。树是通过读取和解析一个大平面文件并将行插入到树中来构建的。然而,这大约需要30秒。是否有一种方法可以构建一次树,将其发送到clj文件,然后将树编译到我的独立jar中,这样我就可以在树中查找值,而无需重新读取大型文本文件?我认为这将删除30秒的树构建,但这也将帮助我部署独立的jar,而不需要文本文件

我的第一次挥杆失败了:

(def x (ref {:zebra (ref #{1 2 3 4})}))
#<Ref@6781a7dc: {:zebra #<Ref@709c4f85: #{1 2 3 4}>}>

(def y #<Ref@6781a7dc: {:zebra #<Ref@709c4f85: #{1 2 3 4}>}>)
RuntimeException Unreadable form  clojure.lang.Util.runtimeException (Util.java:219)
(def x(ref{:zebra(ref{1234}))
#
(定义y#)
RuntimeException不可读表单clojure.lang.Util.RuntimeException(Util.java:219)

如果可以将树结构为单个值,而不是引用多个值的树,则可以打印并读取树。因为引用是不可读的,所以如果不进行自己的解析,就无法将整个树视为可读的

通过使树成为一个类型,可以使用为树添加打印和读取函数

以下是使用数据读取器从字符串生成对集合和映射的引用的一个简单示例:

首先为每个EDN标记/类型的内容定义处理程序

user> (defn parse-map-ref [m] (ref (apply hash-map m)))
#'user/parse-map-ref
user> (defn parse-set-ref [s] (ref (set s)))
#'user/parse-set-ref
然后绑定地图数据读取器,将处理程序与文本标记关联:

(def y-as-string 
   "#user/map-ref [:zebra #user/set-ref [1 2 3 4]]")

user> (def y (binding [*data-readers* {'user/set-ref user/parse-set-ref
                                       'user/map-ref user/parse-map-ref}]
              (read-string y-as-string)))

user> y
#<Ref@6d130699: {:zebra #<Ref@7c165ec0: #{1 2 3 4}>}> 
(定义y-as-string
“#用户/地图参考[:zebra#用户/设置参考[1 2 3 4]]”)
用户>(定义y(绑定[*数据读取器*{'user/set ref user/parse set ref
'user/map ref user/parse map ref}]
(将字符串y读为字符串)))
用户>y
# 
这也适用于嵌套较深的树:

(def z-as-string 
  "#user/map-ref [:zebra #user/set-ref [1 2 3 4] 
                  :ox #user/map-ref [:amimal #user/set-ref [42]]]")

user> (def z (binding [*data-readers* {'user/set-ref user/parse-set-ref
                                       'user/map-ref user/parse-map-ref}]
               (read-string z-as-string)))
#'user/z
user> z
#<Ref@2430c1a0: {:ox #<Ref@7cf801ef: {:amimal #<Ref@7e473201: #{42}>}>, 
                 :zebra #<Ref@7424206b: #{1 2 3 4}>}> 
(定义z-as-string
“#用户/地图参考[:zebra#用户/设置参考[1 2 3 4]
:ox#user/map ref[:amimal#user/set ref[42]]”)
user>(def z(绑定[*数据读取器*{'user/set ref user/parse set ref
'user/map ref user/parse map ref}]
(读取字符串z-as-string)))
#'用户/z
用户>z
# 
通过扩展print方法multimethod可以从树中生成字符串,不过如果使用deftype为ref map和ref set定义一个类型,那么打印机就可以知道哪个ref应该生成哪个字符串,这会容易得多


如果通常将它们作为字符串读取太慢,则会有更快的二进制序列化库,如协议缓冲区

这种结构是不变的吗?如果没有,请考虑使用java序列化来持久化结构。反序列化将比每次重建快得多。

由于JVM的大小限制,在编译代码中嵌入如此大的数据可能是不可能的。特别是,任何单一方法的长度不得超过64千磅。以我在下面进一步描述的方式嵌入数据还需要在它将要存在的类文件中包含大量的内容;这似乎不是个好主意

假设您使用的是只读数据结构,您可以构造它一次,然后将其发送到
.clj
/
.edn
(即基于Clojure文字符号的序列化格式),然后将该文件作为“资源”包含在类路径中,以便将其包含在überjar中(在
resources/
中使用默认的Leiningen设置;然后它将被包括在überjar中,除非被
:uberjar排除在
project.clj
中),并在运行时以Clojure阅读器的全速从资源中读取它:

(ns foo.core
  (:require [clojure.java.io :as io]))

(defn get-the-huge-data-structure []
  (let [r   (io/resource "huge.edn")
        rdr (java.io.PushbackReader. (io/reader r))]
    (read r)))

;; if you then do something like this:

(def ds (get-the-huge-data-structure))

;; your app will load the data as soon as this namespace is required;
;; for your :main namespace, this means as soon as the app starts;
;; note that if you use AOT compilation, it'll also be loaded at
;; compile time
你也不能将它添加到überjar中,而是在运行你的应用程序时将它添加到类路径中

可以使用
打印方法(序列化时)和读卡器标记(反序列化时)来处理持久Clojure数据以外的内容。Arthur已经演示了如何使用读卡器标记;要使用
打印方法
,您可以执行以下操作

(defmethod print-method clojure.lang.Ref [x writer]
  (.write writer "#ref ")
  (print-method @x writer))

;; from the REPL, after doing the above:

user=> (pr-str {:foo (ref 1)})
"{:foo #ref 1}"
当然,序列化时只需要定义
print方法
方法;反序列化代码时可以不使用它,但需要适当的数据读取器


暂时不考虑代码大小问题,因为我发现数据嵌入问题很有趣:

假设您的数据结构只包含Clojure本机处理的不可变数据(Clojure持久集合,任意嵌套,加上原子项,如数字、字符串(用于此目的的原子项)、关键字、符号、无引用等),那么您确实可以将其包含在代码中:

(defmacro embed [x]
  x)
然后,生成的字节码将使用类文件中包含的常量和
clojure.lang.RT
类的静态方法(例如
RT.vector
RT.map
)在不读取任何内容的情况下重新创建
x

当然,这就是文字的编译方式,因为上面的宏是一个noop。不过,我们可以让事情变得更有趣:

(ns embed-test.core
  (:require [clojure.java.io :as io])
  (:gen-class))

(defmacro embed-resource [r]
  (let [r   (io/resource r)
        rdr (java.io.PushbackReader. (io/reader r))]
    (read r)))

(defn -main [& args]
  (println (embed-resource "foo.edn")))

这将在编译时读取
foo.edn
,并将结果嵌入到编译后的代码中(包括适当的常量和代码以在类文件中重建数据)。在运行时,将不会执行进一步的读取。

真的,不可能吗?我的意思是,在上面的示例中,如果您更改
#
,我似乎应该能够将树编译到可执行文件中。为什么这不是小事?在clojure中,读卡器类型要么是“可读”要么是“不可读”取决于它们是否可以写入文件并读回。这是读者使用的一个术语,而不是对序列化和解析它们的可能性的意见。理解,谢谢。我现在要做的是将数据存储在文本文件中并读入,不幸的是,这会使我的应用程序从命令行变得无用,需要一个守护程序/服务器。。。我想我不会