4 ms·
I was the one at Adtuitive who chose Cascading.JRuby. It was a pre-existing DSL on top of Cascading. It needed some work, but it was a pretty nice concise way t
by StefanKarpinski 13y ago
I was the one at Adtuitive who chose Cascading.JRuby. It was a pre-existing DSL on top of Cascading. It needed some work, but it was a pretty nice concise way to generate Cascading jobs. However, with half a dozen to a dozen people hacking features into it over time at Etsy, and with no real design coordination, things got pretty out of hand. There were a couple of fundamental problems:
1. Type system mismatch. Ruby is dynamic and not type checked; Java is static and type checked; Cascading is somewhere in between and unfortunately doesn't seem to use Java's type system as well as it could.
2. User-defined functions in strings. For some reason Cascading lets you write user-defined functions as strings and compiles them dynamically during job execution. This was the way to write user-defined code in Cascading.JRuby.
Cascading requires a compilation step, yet since you're writing Ruby code, you get get none of the benefits of static type checking. It was standard to discover a type issue only after kicking off a job on, oh, 10 EC2 machines, only to have it fail because of a type mismatch. And user code embedded in strings would regularly fail to compile – which you again wouldn't discover until after your job was running.
Each of these were bad individually, together, they were a fucking nightmare. The interaction between the code in strings and the type system was the worst of all possible worlds. No type checking, yet incredibly brittle, finicky and incomprehensible type errors at run time. I will never forget when one of my friends at Etsy was learning Cascading.JRuby and he couldn't get a type cast to work. I happened to know what would work: a triple cast. You had to cast the value to the type you wanted, not once, not twice, but THREE times.
Scalding fixes both problems since it is statically typed and lets you write real user-defined functions instead of stuffing them in strings. To me, the main moral is never, ever design an API that involves writing code in strings. It's just bound to be a disaster. Also, if you're going to have a compilation step anyway, you might as well get some static checking for obvious problems like type errors as part of the bargain.
- mcfunley 13y agoOne other unforced error with c.j was having almost every function take a dictionary of args instead of a sane argument list. That left "grep the codebase" and "read the entire function definition and god help you if it passes on the argument dict" as the two horrible options for figuring out how to even call most of the functions. But we probably could have erected a blast shield around that mess if only we could have written functions and aggregators in ruby.
- StefanKarpinski 13y agoYeah, that's an unfortunate part of Ruby's design and culture though and not really specific to c.j. Since Ruby doesn't have real keyword arguments, you don't get any checking (even at run time) that the things you passed in aren't completely wrong (and thus silently ignored). We could have fixed that bit with some API redesign, so it's not nearly as dire a problem. The other two things couldn't really be fixed without changing languages entirely.
- gfodor 13y agoYeah that right there was our biggest error I think. We should have had a "compilation pass" that used JRuby's ruby parser to do program transformation on the original source, extracting the inline operator code and doing "something" with it to generate fast JVM bytecode (probably using something like Mirah) and then transforming the call site into something that would use this. It would have been a few months of nightmarish debugging but would have also provided the leverage we needed to do data flow-level type checking/annotation as well, in addition to not having to write our UDFs in another language. I think the thing to remember with the c.j stuff is at the time Cascading's Java API was pretty much state of the art, and we had to shore up a lot of what was in c.j in order to get to a viable DSL. And at that point, it was a huge leap forward from a readability standpoint. (Cascading's API had the same shortcomings as our c.j API wrt types etc, fwiw)
- StefanKarpinski 13y agoAt that point you're just implementing a crappy statically type language inside of a Ruby DSL. Using a real statically typed JVM language that's more expressive than Java – i.e. Scala – would be much better. So, that's basically Scalding. Of course, Scalding wasn't even close to existing when we chose Cascading.JRuby, so yeah.
- gfodor 13y agoYeah I'm just talking counterfactually. At the time there were no solid cascading DSLs whatsoever, cascalog wasn't even around for another 2 years or so iirc. Cascading itself was the new hotness vs writing raw map reduce, so c.j was kind of on the edge of what was out there. The decision to keep UDFs outside of Ruby was made at the outset (out of concern for performance, and being icing on the cake, etc.) and not really revisited.