Skip to content

Instantly share code, notes, and snippets.

@gfredericks
Last active August 7, 2016 15:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save gfredericks/69b42d3ff527bcd48f9a1f60372e173b to your computer and use it in GitHub Desktop.
Save gfredericks/69b42d3ff527bcd48f9a1f60372e173b to your computer and use it in GitHub Desktop.
Independent bindings in gen/let

Independent bindings in gen/let

The Problem

adapted from CLJ-1997

A common usage of gen/let might look something like this:

(gen/let [a gen-a
          b gen-b]
  (f a b))

The crucial characteristic of this code is that the generator for b does not depend on the value a (though in general gen/let allows this, just like clojure.core/let). Because of this independence, the ideal expansion is:

(gen/fmap
  (fn [[a b]] (f a b))
  (gen/tuple gen-a gen-b))

However, because gen/let cannot, in general, tell whether or not the expression for the generator for b depends on a, it needs to fallback to a more general expansion:

(gen/fmap
  (fn [[a b]] (f a b))
  (gen/bind
    gen-a
    (fn [a]
      (gen/tuple (gen/return a) gen-b))))

Using gen/bind greatly reduces shrinking power, and so it's best to avoid it when possible.

A careful user could get around this by using gen/tuple explicitly, e.g.:

(gen/let [[a b] (gen/tuple gen-a gen-b)]
  (f a b))

But this is rather less readable than normal gen/let usage, and so not ideal.

Proposed Solution: :parallel

A :parallel clause allows the user to specify that multiple generators are independent, so the example above becomes:

(gen/let [:parallel
          [a gen-a
           b gen-b]]
  (f a b))

:parallel clauses compile to gen/tuple, and can be intermingled with regular binding clauses:

(gen/let [x gen-x
          :parallel
          [a (gen-a x)
           b (gen-b :foo x)]
          y (gen-y a x)]
  (f a b x y))
  • PROs
    • semantics are explicit, no magic
    • generators and bindings are placed next to each other, unlike with gen/tuple where the user needs to manually match them up by position
  • CONs
    • an extra level of nesting
    • there's probably another CON

Alternatives

Map syntax

I don't think anybody will like this because it looks very foreign but I thought it was an interesting idea since a map suggests the idea of order-independence:

(gen/let {a gen-a
          b gen-b}
  (f a b))

let*

This would simply be an alternative macro that always compiles to gen/tuple. Alex said he didn't like the idea of alternate lets, and I also think it would be less useful for mixing things.

for

test.chuck/for is a macro very similar to gen/let, where :parallel was originally implemented. It could be promoted to test.check proper, and users could be encouraged to use for instead of let when they care about shrinking efficiency.

  • PROs
    • Several other features too (:let, :when)
  • CONs
    • Possibly more confusing (alex thinks so at least)

Don't do anything

  • PROs
    • Keeps everything simple
  • CONs
    • The problem remains
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment