Closed alex-lew closed 5 years ago
It is annoying that source files have to start with
(:refer-clojure :exclude [map replicate apply])
One possible solution: the gen
macro itself could insert a (let [map metaprob.eager/map, replicate metaprob.eager/replicate, apply (with-meta apply {:apply? true})] ...)
around the body of a function. Outside of generative functions, there's no need for a special special map
or replicate
, so users could just use the Clojure versions.
@zane @joshuathayer Curious to hear what you all think of this possibility.
@alex-lew If the best practice is to replace clojure.core/map
with metaprob.eager/map
I would prefer that it happen explicitly, in the ns
form, rather than implicitly via the gen
macro. Otherwise it will be too confusing for people coming to Metaprob from Clojure.
@alex-lew If the goal is to provide an eager map
could we encourage users to use clojure.core/mapv
instead? No :refer
needed! One thing to note about clojure.core/mapv
is that the output type is always a vector, regardless of what the input type was.
@alex-lew Another thing to note is that in ClojureScript, :refer :all
is not available. We're going to need to recommend users do one of the following:
:refer
every "core" Metaprob function individually.(:require [metaprob.math :as math])
(math/exp 10 2)
I pretty strongly prefer the second option. It's the Clojure community best-practice [1] [2], and referring each symbol individually would result in ginormous ns
boilerplate.
@zane thanks for this! some initial responses--
Within a generative function, lazy versions of map
, map-indexed
, reduce
, repeatedly
, etc. are dangerous for subtle reasons. I don't think we want Metaprob users to have to reason about when it's ok or not ok to use laziness.
It's interesting that mapv
is eager, but I'm not sure that's a solution -- because there's no analogue for map-indexed
, reduce
, etc.
Can you say more about what might be confusing to Clojure programmers about replacing clojure.core/map
with metaprob.eager/map
inside of gen
bodies? I am thinking that metaprob.eager/map
would be implemented something like this:
(defn map [& args]
(doall (apply clojure.core/map args)))
so the only way for it to bite you would be if you were relying on laziness inside your generative function (which we want to avoid).
I do agree that hackily replacing one function with another in a macro is not a particularly clean implementation strategy, though, and would love to find a satisfying alternative.
Re: refer
, I could be convinced, but am not yet. Looking at core.logic and anglican, two other Clojure libraries that are trying to be "languages," I see that both encourage users to :use
them, exposing all symbols with no qualifiers. In core.logic
, e.g., we wouldn't write
(logic/run [q]
(logic/fresh [x]
(logic/== x q)))
but rather
(run [q]
(fresh [x]
(== x q)))
I can imagine creating a namespace that just exposed names from all the other namespaces, and importing it as something short like m
or mp
in Clojurescript code -- then you'd use, e.g., mp/trace-value
and mp/infer-and-score
and mp/importance-resampling
. But I don't like the idea of trace/trace-value
, gen-func/infer-and-score
, and inference/importance-resampling
: remembering what function comes from where is a big burden to place on users.
I can imagine creating a namespace that just exposed names from all the other namespaces, and importing it as something short like
m
ormp
in Clojurescript code -- remembering what function comes from where is a big burden to place on users.
I was heading there, and agree!
Great :) Do you know if there's a way to have gen
(the macro) exposed in the same namespace?
So people could do
(ns my.project
(:require [metaprob.core :as m :refer [gen]]))
Not sure how to re-expose a macro that's implemented somewhere else.
That syntax should work as-is, if I remember, and is how I would do it! 🙂 It would be a little different in ClojureScript (you'd probably use :refer-macros
).
Ah, yes, that syntax should work, but the part I was wondering about is how metaprob.core
(our giant namespace) would expose a gen
macro that was actually defined in metaprob.generative-functions
:-) (since it couldn't just do (def gen metaprob.generative-functions/gen)
.)
But I just found https://github.com/ztellman/potemkin, which has an import-vars
feature that seems to exist for just this purpose. (I guess there is the question of whether potemkin will work in ClojureScript.)
Oh, sorry. I misunderstood!
Yes, I had imagined that we would use Potemkin. But now that I'm looking at it I'm not sure it supports ClojureScript. Will look into it further.
You're right that it's not uncommon to refer a few core functions macros from libraries that implement embedded language (like core.logic
). gen
, letgen
, and perhaps infer
seem like a prime candidates for this. But in my experience referring to functions via aliases is pretty strongly preferred among Clojure programmers. I would draw the line before referring something like replicate
, personally.
I think there are two questions:
flip
and gaussian
and uniform
? trace operations like trace-value
?)map
, reduce
, replicate
, etc.In the second, the problem is that people will likely, out of habit, call the Clojure versions of these functions inside a gen
form, which will cause hard-to-debug failures -- running infer-and-score
might return an empty trace, for instance. How can we effectively prevent this?
So, within a generative function, lazy versions of
map
,map-indexed
,reduce
,repeatedly
, etc. are dangerous for subtle reasons.
I understand, but I don't think rebinding map
within gen
bodies is the solution. Swapping out the function they intended to use will confuse users who have prior experience with Clojure. They might be surprised to find a different function than the one they intended to use in stack traces, for example.
If we want to take a hard line there it would be better to force the programmer to fix the issue rather than try to fix the problem for them reactively behind the scenes. We could have gen
error out if it finds symbols that refer to known lazy functions in its body for example.
Hmm, I see. What about inserting a doall
in front of all calls to lazy functions? At least within the generated inference code (i.e., this wouldn't take effect when running the function as an ordinary Clojure function).
I think there's maybe a semantic issue here: if you run
(take 1 (take 1000 (repeatedly (fn [] (t (gensym) flip [0.5])))))
how many flips should be in the trace?
That's certainly an option! I'm wary of that route because I'm worried that it will cause a divergence between what experienced Clojure programmers think is happening ("This expression will return a lazy sequence!") and what is actually happening ("This expression will return a fully realized list.").
We also can't anticipate the full set of lazy functions that people might use. What if they get used to the behavior of implicitly doall
-wrapped functions from clojure.core
but then later get bitten by some lazy function that they've written themselves (or that they got from a library)? Better for them to get an error (or warning?) from their first naive use of clojure.core/map
and thereafter know to be wary of laziness.
@alex-lew and I discussed this a bit more on Slack, and at this stage my preferred way to protect users from the unintended consequences of laziness+tracing are, in order:
clojure.core
functions in a special namespace and encouraging users to use them via an alias. (e.g. (:require [metaprob.tracesafe :as tracesafe]
tracesafe/map
):refer-clojure :only […]
and :refer […]
.map-random
).gen
Implicitly wraps lazy functions in clojure.core
with doall
.gen
implicitly rebinds lazy functions from clojure.core
with eager versions.I think I'd be happy merging this, if @zane and @joshuathayer think we can go ahead. Things to resolve in the future:
make-constrained-generator
?gen
? (what Zane and I were discussing above).
This branch contains a new implementation of Metaprob.
Breakdown of the new code
The Metaprob implementation now consists of the following parts:
Utilities (written in Clojure)
metaprob.code-handlers
implements helper functions for dealing with Metaprob S-expressionsmetaprob.trace
implements the trace datatype and a number of operations on tracesCore language
metaprob.generative-functions
implements Metaprob's core abstraction: the generative function. It provides a general constructormake-generative-function
for generative functions, and two additional constructs that desugar tomake-generative-function
calls: thegen
macro, and themake-primitive
helper.Libraries
metaprob.prelude
exposes useful math functions likelog
andexp
, as well as eager, generative-function versions of higher-order functions likemap
andreplicate
.metaprob.distributions
implements several common distributions as generative functions.metaprob.inference
implements the Metaprob standard inference library.Syntactic transformations
metaprob.expander
exposes themp-expand
helper function, which authors of Metaprob macros / static analyses can call. It desugars any generative function source code into a small core language consisting only of variables, literals, function calls,quote
forms,if
expressions, andgen
expressions.metaprob.autotrace
implements anautotrace
macro that relies onmp-expand
.(autotrace (gen [] ...))
creates an autotraced generative function.Very quick overview of language changes
Metaprob source files will typically begin with something like
In their project, Clojure code can be used as usual. To define a generative function, users can:
Call
make-generative-function
manually, passing in two arguments:run-in-clojure
, a Clojure function that accepts arguments and returns a valuemake-constrained-generator
, a Clojure function that accepts an observation trace and returns a constrained generator (see below for more details).{:support [true false]}
.Call
make-primitive
, passing in a sampler(fn [& args] ...)
and a scorer(fn [val args] ...)
. The scorer returns the log probability mass or density ofval
under the distribution on return values induced by(apply sampler args)
.Use the
gen
macro:gen
macro is(gen [& args] body...)
. This is a generative function in which no choices are traced.gen
function (e.g., for recursive calls) using either(gen my-name [& args] body...)
or(gen {:name my-name} [& args] body...)
.gen
and preceding the argument list. These annotations are stored as part of the resulting generative function object's metadata, accessible via Clojure'smeta
function.gen
body, traced random calls can be made in three ways:(at addr other-generative-function arg1 arg2...)
,(apply-at addr other-generative-function [arg1 arg2...])
, and(let-traced [x (some-generative-function arg1 arg2...)] ...)
.A generative function can be run like an ordinary Clojure function, but also supports another operation,
make-constrained-generator
, which accepts an observation trace and returns a constrained generator. A constrained generator is a generative function with the following properties:[value trace score]
, such thattrace
is a complete trace of the original generative function from which it was derived,value
is the corresponding return value, andscore
is a log importance weight.infer-and-score
is implemented in terms ofmake-constrained-generator
.The examples in the
metaprob.examples
namespaces have been updated to use the new syntax, as have all the tests. They pass!However, we should decide:
.md
files (documentation) that are out of date.