A quick tips when creating block DSLs in Ruby:<p><pre><code> 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 }</code></pre>
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.<p>Better:<p><pre><code> $bt = Braintree_Transaction_Search::someFactory();
$collection = $bt->search(array(
$bt->orderId()->startsWith('a2d')
$bt->customerWebsite()->endsWith('.com'),
$bt->billingFirstName()->is('John'),
$bt->status()->in(array(
Braintree_Transaction::AUTHORIZED,
Braintree_Transaction::SETTLED
)),
$bt->amount()->between('10.00', '20.00')
));
</code></pre>
If you're really up to it, there is no reason you couldn't use an even more fluent interface in PHP...<p><pre><code> $collection = $bt->where()
->orderId()->startsWith('a2d')
->and()->customerWebsite()->endsWith('.com')
->and()->billingFirstName()->is('John')
->search();
</code></pre>
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.
In Lisp I would write a macro that could be used like this:<p><pre><code> (for-each (transaction)
:where
((order :starts-with "a2d")
(customer-website :ends-with ".com")
(billing-first-name :equals "John")
(status :one-of '(:authorized :settled))
(amount :between '("10.00" "20.00")))
:do
(print (id transaction)))</code></pre>
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!<p><pre><code> def search(*args) : ... # args is a sequence
search(this, that, the_other_thing)
</code></pre>
Edit: found the HN FAQ entry on formatting comments, finally.
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.<p>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.
Fluent interfaces are cool, but they throw away the semantics of the programming language like meaningful return values.<p>The DSL in the article is a good example of this problem:<p><pre><code> OrderId.StartsWith("a2d")......
</code></pre>
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.<p>An alternative to doing this is using Expression Trees, but somewhat harder (and not without faults)
It would look like this:<p><pre><code> search.Where(s => s.Order.Id.StartsWith('a2d') &&
s.Customer.Website.EndsWith('.com') && ...)
</code></pre>
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?)<p>The biggest drawback of course is:
1. Harder - You will need to parse the Expression Tree.
You can make it without any 'weaknesses' in Python:<p><pre><code> collection = Transaction.search(
order_id__starts_with='a2d',
customer_website__ends_with='.com',
billing__first_name__exact='John',
status__in=[
Transaction.Status.Authorized,
Transaction.Status.Settled
],
amount__between=("10.00", "20.00")
)
</code></pre>
the implementation could look like this:<p><pre><code> 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.
.....
</code></pre>
EDIT: removed unneeded quotes, thanks for correction, postfuturist
In Java, I would do this:<p><pre><code> new Search().equal(Search.NAME,"Joe")
.between(Search.AGE,18,25)
.go();
</code></pre>
Just as concise with less voodoo. And it's a single statement.<p>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.
I really enjoy reading these articles comparing different implementations in different languages. I wish we had more of that on HN.<p>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.<p>Also, for this particular implementation, we could simply use a builder.<p>SearchBuilder sb;<p>sb.equal(Search.Name, "..");
sb.equals(Search.AGE, ..);<p>Search s = new Search(sb);<p>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.<p>Another verbose approach might be using lots of Objects..<p>Search s;
s.add_contraint(SearchEqual(Search.Name, "bob"));<p>This way, it make it easier to add constraint without modifying the Search class.
Let's clean up the Ruby example a bit, shall we?<p><pre><code> collection = Braintree::Transaction.search do
order_id.begins_with? "a2d"
customer_website.ends_with? ".com"
billing_first_name == "John"
status.in? Status::Authorized, Status::Settled
amount.between? "10.00", "20.00"
end
collection.each do |transaction|
puts transaction.id
end</code></pre>
In the Ruby code, you include<p><pre><code> search.status.in(
Braintree::Transaction::Status::Authorized,
Braintree::Transaction::Status::Settled
)
</code></pre>
, but wouldn’t it be better to allow just<p><pre><code> search.status.in(:authorized, :settled)
</code></pre>
? (And I agree with judofyr’s comment that “search.” shouldn’t be necessary on every line.)
You can also handle a variable number of positional arguments in PHP instead of making the client send in a literal array.<p><pre><code> function search() { $arg_list = func_get_args(); ... }</code></pre>
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?
What about doing it like this in Ruby:<p><pre><code> 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
</code></pre>
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:<p><pre><code> $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)
");</code></pre>