def search(&blk)
if blk.arity == 1
blk.call(self)
else
self.instance_eval(&blk)
end
end
# Then the user can decide what he want to use:
Foo.search { bar }
Foo.search { |s| s.bar }
Yeah, but the whole point is really repeating local variables vs capturing self:
# This wouldn't work because it binds the block to Foo, and the @bar
# would lookup inside Foo instead of the proper context:
Foo.search { bar == @bar }
# This works fine though:
Foo.search { |s| s.bar == @bar }
Besides, if you're setting accessor, it can get a little weird:
# Explicit and nice:
Foo.search { |s| s.foo = 123 }
# This actually sets a local variable:
Foo.search { foo = 123 }
# In order to fix it, we're back to the explicit one:
Foo.search { self.foo = 123 }
There is no silver bullet here, and I believe the best way is to provide both techniques and let the user decide.
For the PHP version: Why is the search method static? You are forcing whatever class you are using to be tightly coupled. Dependency injection can be done in PHP, too.
Also, please make sure the search method returns an iterator, not an array. Just because PHP allows you to shoot yourself in the foot, doesn't mean you should.
Python tip: instead of making the user contstruct a list literal, just use (star)args in the function definition. Python will automatically pack up any number of positional arguments into a list for you!
def search(*args) : ... # args is a sequence
search(this, that, the_other_thing)
Edit: found the HN FAQ entry on formatting comments, finally.
Just to clarify for anyone who isn't familiar with Python: it's asterisk followed by "args". The problem here is that HN comments system interprets asterisk as italic formatting ;)
Python 3 lets you have named keyword arguments too. Anything after the star-args is a named keyword argument, and you can have a simple star if you do not want the argument list:
def search(foo, bar, *, baz):
Then, foo and bar would be named arguments, and baz would need to be passed by keyword.
Not using 'var' (or LINQ, for that matter) in C# seems an odd choice. 'Separate steps for creating the request and performing the search' seems more like an advantage (for Java and C#) to me than a disadvantage, since you can easily eliminate the overhead with a helper function and the separation of a request and a search means that you can reuse a single request instance if you see the need.
I also consider statements like 'search.amount.between "10.00", "20.00"' or 'Amount.Between(10.00M, 20.00M)' to be vastly inferior to the native alternatives: for example, in C#, you could write that predicate as '(amount) => (amount > 10.00M && amount < 20.00M)' and in Python, it would become the even shorter 'lambda amount : 10 > amount > 20'. In each case the native way of expressing the logic requires no special knowledge of the 'fluent' API.
We tend to use var when the return type is obvious, but we wanted to be explicit in the examples. Unfortunately, LINQ is not an option for us as we support .NET 2.0.
Your C# and Python predicate examples are quite readable, but in this case they don't work for us. The return type of each of these statements would be true or false, whereas we are building XML requests to be sent to a server to perform the searches. The call to Amount.Between(...) builds an XML node behind the scenes representing the search data.
You mean you support C# 2.0, since var, LINQ, et al run on .Net 2.0 :-P
Doing a full blown Linq Provider would be pretty cool, but would also be very very very time consuming to get right. Expression trees are pretty cool, but you're still dealing with the AST so it gets crazy. There needs to be a Linq Provider framework / library to work at a slightly higher abstraction level.
Ignoring the .NET version problem, not using LINQ probably makes plenty of sense, given that implementing a LINQ solution would be much much more complicated to implement than what you've done. Breaking apart expressions to get to your XML requests would be hard. I also think the proposal above where each criteria is expressed as an individual lamdba is the worst of both worlds in terms of ease of implementation and readability.
Fluent interfaces are cool, but they throw away the semantics of the programming language like meaningful return values.
The DSL in the article is a good example of this problem:
OrderId.StartsWith("a2d")......
So the StartsWith() returns a TransactionSearch, instead of a bool as you would expect. Once you do this often enough, you really get objects full of state-information, rather than methods with return values.
An alternative to doing this is using Expression Trees, but somewhat harder (and not without faults)
It would look like this:
Two benefits come to mind:
1. StartsWith() can return a bool as you would expect.
2. You can use StartsWith() on strings, like OrderId
--- I am not sure how you pulled this off in the DSL example (is OrderId a custom type, since you can't use Extension methods there?)
The biggest drawback of course is:
1. Harder - You will need to parse the Expression Tree.
def search(**kwargs):
for arg, value in kwargs.items():
action = arg.split('__')
attr = getattr(self, action[0])
if len(action) == 2:
method = getattr(attr, action[1])
method(value)
etc.
.....
EDIT: removed unneeded quotes, thanks for correction, postfuturist
When we began building our Python library, we looked at both django[1] and sqlalchemy[2] for inspiration in designing our search DSL. Your example seems to closely match the django style. We preferred the sqlalchemy style, but both are solid choices.
new Search().equal(Search.NAME,"Joe")
.between(Search.AGE,18,25)
.go();
Just as concise with less voodoo. And it's a single statement.
With the existing method, you can still ditch the empty parens by using public final fields. You just have to pre-populate them all with respective grammar objects.
Your syntax is definitely simpler. One reason we did it the way we did was to get compiler checking for operators. We've tried to follow idioms in different client libraries, and for the statically compiled languages, we've tried to make it so if it compiles, it will work.
Since the field comes first (orderId().startsWith()), if you try to call an operator on a field that does not support it, it will not compile.
I really enjoy reading these articles comparing different implementations in different languages. I wish we had more of that on HN.
Concerning the article, call me grumpy if you want, I like better when there is no mass overloading even thought it's more verbose. From my maintenance experience working on various project, I always cry when after 3 hours of searching I find that "+" is overloaded and that's where the bug was hidden.
Also, for this particular implementation, we could simply use a builder.
Even thought it's more verbose, we clearly see that we configure the builder as we want, and then, we create the search object. A good side effect of that is that we get an immutable Search object. Also, we could use the "fluent" interface on the builder.
Another verbose approach might be using lots of Objects..
Overloading basic operators is the road to hell. Yes, you can do wonderful and elegant things but when it goes wrong your whole mechanism for analysing what you look it is screwed up because the more experience you have in a language the more you tend to be on autopilot for stuff like that.
'test your assumptions' suddenly expands to include checking if basic language operators still do what you expect them to do.
and
v2 = vec_add(v1,v3);
really isn't that much better than
v2 = v1 + v3;
I know this is not a popular opinion but that's how I've always felt about it and those that complain about how difficult languages like c++ care usually painted themselves in a corner by excessive use of such features.
I used to vertically align things into columns in Ruby. It's easy to maintain when you're editing individual lines, but it can easily be messed up with a find-and-replace in file. At that point, it becomes harder to read with some lines off, and a pain to maintain the alignment.
I've grown to dislike that habit in all languages. It's too cute and it doesn't scale with code changes. It's easier for me to read code that's formatted in a standard way, even if that means conceptual columns don't line up.
I'm confused in that none of the examples look like a DSL to me. They look like a library API - implemented in five different languages but I can hardly classify any of them as a DSL. Am I really that obtuse concerning this question?
collection = Braintree::Transaction.search do
order_id /^a2d/
customer_website /\.com$/
billing_first_name "John"
status :authorized, :settled
amount 10..20
end
collection.each do
puts id
end
It might be more difficult not having the specific methods (in, starts_with_ ends_with, and so on), but I think it's quite readable. If we don't want the regexps, we could go for a MySQL style '%.com' and 'a2d%' or something similar.
Why dont they just take a string as the argument to search() and parse it? They could use a very SQL like Syntax:
$collection = Braintree_Transaction::search("
orderId LIKE 'a2d%'
AND customerWebsite LIKE '%.com'
AND billingFirstName='John'
AND status() IN ('AUTHORIZED','SETTLED')
AND amount>=10
AND amount<=20.00)
");
One of the big reasons to build the DSL in the first place is to let the host language's syntax help you spot problems in your query. Syntax errors can be spotted from editor-provided syntax highlighting directly or slightly later when the code is read by the interpreter -- much sooner than when your runtime library finally decides to look at it (at search time).
Yeah, i've never done any research into DSL's, but I always thought they would have some sort of custom syntax. Since DSL's seem quite the hype ATM, I'm a bit dissapointed by these examples. Sure, nice coding and all that (even though I would have used LINQ myself, but then again I'm not restricted to C# 2.0), but I fail to to see where the L of DSL comes into play.
If this is the latest hype I didn't have time for to research, I'm not missing out.
There are two types of DSLs, internal and external. External ones are the custom syntax you talk of, internal are DSLs that use features or quirks of a host language to appear different.
XML or SQL are external DSLs, much of the cool Ruby stuff like Rake and Rails are internal DSLs. The examples presented are all internal (presumably for a good reason). There are advantages to both approaches. Read Martin Fowler's discussion at http://martinfowler.com/dslwip/