击败Racket的隔绝编译保证

Defeating Racket’s separate compilation guarantee一文有感。

什么是隔绝编译保证

Racket支持独部(module)系统,代码可以编译成独部,并供其他代码使用。独部可以是预编译后的二进制代码,但是对于使用者而言,预编译的和源码形式的独部在使用上看不出差别,使用者无法判断所用独部是来自于源码,还是来自于预编译的二进制。

Racket的代码有编译阶段和运行阶段之分,编译阶段和运行阶段之间是隔离的,只有有限的沟通手段,主要是通过语句标的进行沟通(虽有跨编译阶段和运行阶段的独部共享机制,但是不建议作为普通使用)。但是在编译阶段和运行阶段,都可以求访其他独部中的代码以重用之。重点在于,虽然求访的是同一个独部,但是编译阶段和运行阶段对独部的例现化是分开的,运行阶段求访的独部所获取的状态在编译阶段不可见,反之亦然,除非把状态转化为语句标的,才可以在两者之间传递。

编译阶段和运行阶段对独部状态有着不同的处理方式,运行阶段求访独部之后,会保留独部的状态。但是编译阶段求访独部之后,会抛弃能够抛弃的状态,具有外部效果的状态(比如IO操作)是没有办法抛弃的。

每个独部都有编译和运行两个阶段,一个独部在编译阶段又可求访另一个独部的编译阶段,其实就涉及了三个个阶段:

  • 发起求访的独部的运行阶段
  • 发起求访的独部的编译阶段和被求访的独部的运行阶段是重叠的
  • 被求访的独部的编译阶段

我们把上面的每个阶段用一个数值表示,每个数值称为位阶,以0未初始位阶。上面共有三个位阶,从上到下逐次升高,分别未0, 1, 2。

另外还可以在运行阶段求访另一个独部的编译阶段,这样就有下下面三个位阶:

  • 被求访的位阶的运行阶段
  • 被求访位阶的编译阶段和发起求访的独部的运行阶段
  • 发起求访的独部的编译阶段

上面三个位阶从上到下分别是 -1, 0, 1。

之间使用racket来运行某个racket文件,会既编译又执行。如果只是使用raco make的运行某个racket文件,则只会执行编译过程,来生成二进制代码。

有了隔绝编译保证,独部例现中状态就互不影响。Racket就可以采用任意顺序来求访独部。

如何打破隔绝编译保证

完全的隔离是不行的,还得留有一些脱漏。

其中一个脱漏是志记系统。Racket的志记手段允许从一处发消息给另一处,这个消息可以携带任意量值。志记系统是跨位阶的,可以从某位阶发送消息给另外一个位阶上的接收者。

志记系统有其限制,比如无法直接在racket诠译的的或者raco make编译的代码中运行。

另个一脱漏跟垃圾收集器有关,垃圾收集器天然是全局的。更详细的说明请看文中。

hash-set

hash这种数据结构似乎具有外部性:

(module m racket
	(define env (make-hash))
	(provide env))

(module p racket
	(require (for-syntax 'm))
	(begin-for-syntax
	  (hash-set! env 'hi 'there)
	  )
	(provide (for-syntax env)))


(module p racket
	(require 'm)
	(hash-set! env 'hi 'there))
	(provide (all-from-out 'm)))

(module q racket
	(require (for-syntax 'm))
	(begin-for-syntax
	  (hash-set! env 'oh 'yes))
	(provide (for-syntax env)))

(module q racket
	(require 'm)
	(hash-set! env 'oh 'yes)
	(provide (all-from-out 'm)))

 (begin-for-syntax
   (displayln env))

参考