A little side note before I get started - pivoting from my last post on ski mountaineering racing to this post on advanced Cascalog patterns has made me realize that I'm a full-fledged connoisseur of the esoteric. I'm embracing it! This is the first in a series of posts on hardcore Cascalog. If you're stoked, leave me a comment telling me what you want to learn more about and we'll go from there.
While Cascalog's primitives and operations can get you pretty far down the Big Data yellow brick road, advanced users will need to go beyond the basics and generate queries and predicates dynamically. Because Cascalog queries are just Clojure data structures, you can abstract out patterns by writing functions that return custom Cascalog queries. Here's a contrived example:
(defn perform-on-all [src f]
(<- [?result]
(src ?x)
(f ?x :> ?result)))
This function takes a source (of 1-tuples) and a function, and returns a new source with that function applied to all tuples. (In vanilla Clojure, this is map
). It works because the number of predicates in the query doesn't depend on any of the function arguments. It's always just that one predicate. But what if you wanted to generate more predicates based on the function's arguments?
The cascalog.logic.ops namespace defines a bunch of higher order functions that require this trick. This post is going to walk through the implementation of one of those functions - cascalog.logic.ops/juxt
. Cascalog's juxt
works like Clojure's juxt, but can accept any Cascalog operation as an argument, not just Clojure functions. As we'll see, juxt
needs to generate one predicate for every argument, and should support any number of arguments. Let's look at Clojure's juxt
to see how to begin.
Juxt!
In vanilla Clojure, you use juxt
to apply multiple operations to the same input variables:
(let [compound-fn (juxt inc square dec)]
(compound-fn 10))
;;=> [11 100 9]
juxt
returns a function that generates a vector with an entry for each of the functions passed to juxt
. ClojureDocs has some nice examples.
What's the Cascalog equivalent of juxt
? This pattern, applying a bunch of operations to the same input, shows up all over. Here's a contrived example - for each of a collection of numbers, calculate the increment, the decrement and the square.
(defn square [x] (* x x))
(let [src [[1] [2] [3]]]
(??<- [?incs ?squares ?decs]
(src ?x)
(inc ?x :> ?incs)
(square ?x :> ?squares)
(dec ?x :> ?decs)))
;;=> ([2 1 0] [3 4 1] [4 9 2])
The query is similar to this Clojure code block:
(let [src [1 2 3]
step-1 (map inc src)
step-2 (map square src)
step-3 (map dec src)]
(map vector step-1 step-2 step-3))
;;=> ([2 1 0] [3 4 1] [4 9 2])
Here's the same block, using juxt
and killing the let
binding:
(map (juxt dec square inc) [1 2 3])
;;=> ([2 1 0] [3 4 1] [4 9 2])
Clojure's juxt
allowed us to clean up the boilerplate of passing src
into each of those functions. What we want in Cascalog is a function, call it juxt*
, that'll let us avoid the repetition that showed up in the example query above. Here's a new query written in wishful thinking style:
(let [src [[1] [2] [3]]]
(??<- [?incs ?squares ?decs]
(src ?x)
((juxt* inc square dec) ?x :> ?incs ?squares ?decs)))
How can we implement juxt*
? One option would be to write a function that takes functions returns a predicate macro. (See the end of the post for a short refresher on predicate macros.) It's easy for a few of the arities, but things quickly devolve.
(defn juxt*
([f]
(<- [!x :> !a]
(f !x :> !a)))
([f g]
(<- [!x :> !a !b]
(f !x :> !a)
(g !x :> !b)))
([f g h]
(<- [!x :> !a !b !c]
(f !x :> !a)
(g !x :> !b)
(h !x :> !c)))
,,,, ;; madness!
)
The problem here is that you need one predicate for each of the input functions. To match Clojure's juxt
, juxt*
needs to be able to handle as many functions as you throw at it. If <-
were a function instead of a macro, we could just apply
it to the predicates. "Oh my god," I hear you sigh. "Could a function version of <-
exist?" Boom, from stage left, enter cascalog.api/construct
.
construct
The <-
macro is a thin wrapper around the cascalog.api/construct
function. All <-
does is
- convert symbols beginning with
?
or!
into strings (since Cascalog variables are represented by strings, not symbols), - allow you to use lists instead of vectors for the predicates in your queries,
- that's it. There's nothing else. Isn't it so simple?
The following queries are all identical. Here's a Cascalog query written in the style I use in my examples:
(def src [[1] [2] [3]])
(<- [?x ?square]
(src ?x)
(odd? ?x)
(* ?x ?x :> ?square))
Because <-
converts logic variables to strings, doing that conversion ourselves is a no-op:
(<- ["?x" "?square"]
(src "?x")
(odd? "?x")
(* "?x" "?x" :> "?square"))
Using vectors instead of lists is fine too. I think lists look prettier, but if <-
were a function, Clojure would treat those lists as function applications. In fact, the lists are little special collections of operations and variables.
(<- ["?x" "?square"]
[src "?x"]
[odd? "?x"]
[* "?x" "?x" :> "?square"])
And finally, the same query using construct
, with all macro sugar removed.
(let [outputs ["?x" "?square"]
predicates [[src "?x"]
[odd? "?x"]
[* "?x" "?x" :> "?square"]]]
(construct outputs predicates))
construct
takes two arguments - the first is a a sequence of output variables, and the second is a sequence of predicates. construct
solves juxt*
's problem. Because construct
is a function, not a macro, the juxt*
function can take any number of input functions and generate a predicate for each, then pass all those predicates into construct
.
Defining Juxt
First, I'll show the final definition of juxt*
, then I'll go through it line by line. Here she blows:
(require '[cascalog.logic.vars :as v])
(defn juxt*
"Accepts any number of predicate operations and returns a new
predicate that is the juxtaposition of those ops."
[& ops]
(let [outvars (v/gen-nullable-vars (count ops))]
(construct
["!input" :>> outvars]
(map (fn [o v] [o "!input" :> v])
ops
outvars))))
The first thing this juxt*
implementation does is generate a randomly-named logic variable for each supplied operation using v/gen-nullable-vars
. This list is bound to outvars
.If you look at the first query example:
(let [src [[1] [2] [3]]]
(<- [?incs ?squares ?decs]
(src ?x)
(inc ?x :> ?incs)
(square ?x :> ?squares)
(dec ?x :> ?decs)))
Every operation that acts on ?x
has a distinct output. Calling (juxt* inc square dec)
generates three logic variables internally and binds them to outvars
.
Next, construct
's two arguments are declared inline. This version of juxt*
only allows a single input variable to each function, so we can just make up a variable name. Let's call it !input
. (I'll go over how to extend juxt
to multiple inputs in a future post.)
The outputs are the outvars
generated before. As discussed in the Predicate Operators section of the docs, :>>
allows a predicate to use a vector of logic variables as its output. The same rule applies to predicate macro signatures.
All that's left to create are the predicates. Predicates are vectors of the form [<operation> !input :> <output-variable>]
. (The grammar's more complicated, but this will do for now.) Because predicates are just vectors, and construct
needs a sequence of predicates, we can generate that sequence by mapping across the supplied ops:
(map (fn [o v] [o "!input" :> v])
ops
outvars)
Mapping across op
and outvars
at the same time pairs each operation up with one of the fresh logic variables. And, just as we wanted, for each pair the anonymous function we're mapping outputs [o "!input" :> v]
.
With this new definition, the wishful thinking example from above compiles and runs!
(let [src [[1] [2] [3]]]
(??<- [?incs ?squares ?decs]
(src ?x)
((juxt* inc square dec) ?x :> ?incs ?squares ?decs)))
;;=> ([2 1 0] [3 4 1] [4 9 2])
So, there you have it. Dynamic query generation. Wasn't that easy?
In the next post, I'll show how to extend juxt*
to handle multiple input arguments. If you want a head start, check out Cascalog's definition of juxt. The definition uses destructuring in the generated predicate macro, which, like the recipe for Coca Cola, is a feature that probably only a couple of living souls know about. If you know what I'm talking about, explain the feature in the comments and I'll come up with some sort of prize.
As I said in the beginning, I'm planning on writing more of these, and would love feedback on what parts of Cascalog you all want to hear more about. Leave me a comment below and I'll see what I can do.
To wrap things up, here's the promised primer on predicate macros.
Predicate Macros :)
Even though "predicate macro" has the word "macro" in it, you shouldn't be scared. (The smiley in the heading is a friendly smile, not a creepy one.) A predicate macro is just Cascalog's way of grouping together a bunch of operations into one new black-box operation. Predicate macros are how you declare "functions" in Cascalog's logic land.
For example, if you wanted to write a function that calculated the average value of some input variable, you'd probably want to reuse the efficient sum
and div
operations Cascalog provides in cascalog.logic.ops
. Because sum
is an aggregator, you can't compose these with comp
like you would normal Clojure functions. Predicate macros make this composition easy:
(def avg
"Predicate operation that produces the average value of the
supplied input variable.
"
(<- [!v :> !avg]
(count !c)
(sum !v :> !s)
(div !s !c :> !avg)))
A predicate macro looks exactly like a normal query, except predicate macros allow input variables. You can tell that a query is a predicate macro because it'll have a :>
or :>>
in the argument vector separating input variables from output variables. Predicate macros like avg
can be used in other queries like any operation:
(let [src [[1] [2]]]
(<- [?avg]
(src ?x)
(avg ?x :> ?avg)))
;;=> ([1.5])
Go forth and macro, my children.
Comments
comments powered by Disqus