A DSL in 5 Languages: Ruby, Python, PHP, C#, Java
braintreepaymentsolutions.com
braintreepaymentsolutions.com
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 } # 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.Better:
$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')
));
If you're really up to it, there is no reason you couldn't use an even more fluent interface in PHP... $collection = $bt->where()
->orderId()->startsWith('a2d')
->and()->customerWebsite()->endsWith('.com')
->and()->billingFirstName()->is('John')
->search();
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. (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)))No, no, remember: all other languages need a chance to incrementally catch back up to Lisp. :)
(braintree/search
(starts-with :order-id "a2d")
(ends-with :customer-website ".com")
(equals :billing-first-name "John")
(one-of :status [:authorised :settled])
(between :amount 10 20))
The cool thing being that both styles are possible in a lisp. def search(*args) : ... # args is a sequence
search(this, that, the_other_thing)
Edit: found the HN FAQ entry on formatting comments, finally.Edit: Parent comment fixed by postfuturist.
def search(*args): ...Really? Where?
def search(foo, bar, *, baz):
Then, foo and bar would be named arguments, and baz would need to be passed by keyword.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.
Drew Olson (Braintree Dev)
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.
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:
search.Where(s => s.Order.Id.StartsWith('a2d') &&
s.Customer.Website.EndsWith('.com') && ...)
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.
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")
)
the implementation could look like this: 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 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")
)[1]http://docs.djangoproject.com/en/1.2/topics/db/queries/#chai... [2]http://www.sqlalchemy.org/docs/ormtutorial.html#common-filte...
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.
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.
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.
SearchBuilder sb;
sb.equal(Search.Name, ".."); sb.equals(Search.AGE, ..);
Search s = new Search(sb);
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..
Search s; s.add_contraint(SearchEqual(Search.Name, "bob"));
This way, it make it easier to add constraint without modifying the Search class.
'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. 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 amount.within? "10.00".."20.00"
Or somesuch. Ranges are always overlooked. search.status.in(
Braintree::Transaction::Status::Authorized,
Braintree::Transaction::Status::Settled
)
, but wouldn’t it be better to allow just search.status.in(:authorized, :settled)
? (And I agree with judofyr’s comment that “search.” shouldn’t be necessary on every line.) function search() { $arg_list = func_get_args(); ... } 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. $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)
");If this is the latest hype I didn't have time for to research, I'm not missing out.
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/