I wrote a blog post trying to answer to the question on subject. Feedbacks
are greatly appreciated!
http://freecella.blogspot.com/2010/03/is-go-suitable-for-building-dsl.html
Thank you,
Andrea
--
Andrea Fazzi @ alcacoop.it
Read my blog at http://freecella.blogspot.com
Follow me on http://twitter.com/remogatto
One of the things Ruby does particularly elegantly is get out of the way when building DSLs and consequently it's a coding style which is prevalent. Whilst it's true that letter count alone isn't a dominant consideration for such things, verbosity of expression does play a significant role in deciding whether similar techniques are idiomatic in any given language and so that's what we really need to be considering.
Based on my experiments to date (and this is by no means a scientific opinion, so take with a large pinch of salt) I'd say that Go is appreciably faster to code in than comparable statically compiled languages, but the verbosity that brings makes internal DSLs less appealing than in Ruby. I've commented before on the lack of method_missing and the fact that explicitly checking for a method's presence is more cumbersome, but setting that aside I think the main reason that Ruby-style DSLs would be tedious to implement in Go is the very different approach taken to parsing the language in the first place.
Go's parser aims to be clean, fast and simple. That means that a certain amount of work has to be undertaken by the programmer. Ruby on the other hand has possibly the most complex and unwieldy parser it's ever been my misfortune to study. However parsing the language is a pig because its syntax is focused on ease of expression for the programmer with no real concessions to computational efficiency or parser clarity.
Whilst I have no desire for Go to become a low-level clone of Ruby I would like to see a little more of its underlying philosophy reflected in the language specification: make programmer happiness a primary goal and try to replicate as many of the elegant idioms of the dynamic world as make sense in a mostly static language.
In terms of practical implementation, the real edge that Ruby has in this regard is that like Lisp it's expression-oriented. Every Ruby statement returns a value and this makes it very easy to be succinct - especially when combined with exception handling for out-of-band. The same thing can be achieved in a more traditional language: Icon famously combined an Algol-derived syntax with out-of-band success/failure and generators to achieve much the same ease of development in a compiled language that many dynamic language enthusiasts have come to expect in Ruby or Python.
It's not entirely unreasonable to foresee Go developing along these lines in the future, but it would at some stage become a markedly different language to the one we're working with today.
Ellie
Eleanor McHugh
Games With Brains
http://feyeleanor.tel
----
raise ArgumentError unless @reality.responds_to? :reason
I agree, which is why I'd say making a DSL within go is a bit silly.
Haskell (also statically-typed) is a great languages for embedded
DSLs, because of its insane degree of syntactic flexibility. e.g. you
can define whatever operators you want, with your choice of
precedence. Without that kind of syntactic flexibility (or something
similar, as in ruby), what you have is a library, not a DSL.
However, this comes at a cost, and that cost isn't just in parser
complexity: it's also in human complexity. Just as the compiler
cannot parse Haskell code without compiling any code that it imports,
neither can a human! So what on the surface seems very, very
programmer-friendly, can easily become very, very programmer-hostile
(and this does happen in practice... I've done it myself!). Being
able to read and parse a file without reading and parsing a whole lot
more files is a *very* nice feature, and I'd be sad to see go lose it!
(True, to know what the functions and methods actually do, you need
to look elsewhere, but at least you know that they are functions and
methods, respectively... and you know which are being called with
which arguments.)
Having used and written a number of DSLs, I'd prefer a regular library
any day of the week. With the one exception (everyone has to have
one, right?), which is that I'd love to be able to overload arithmetic
operators for vector and matrix operations and the like. Which is
effectively a DSL for math and Physics.
David
Very true. Every time I think I've explored the furthest reaches of Ruby's syntactic flexibility, I find yet another bit of code that does something utterly unexpected - and utterly delightful :)
> However, this comes at a cost, and that cost isn't just in parser
> complexity: it's also in human complexity. Just as the compiler
> cannot parse Haskell code without compiling any code that it imports,
> neither can a human! So what on the surface seems very, very
> programmer-friendly, can easily become very, very programmer-hostile
> (and this does happen in practice... I've done it myself!). Being
> able to read and parse a file without reading and parsing a whole lot
> more files is a *very* nice feature, and I'd be sad to see go lose it!
> (True, to know what the functions and methods actually do, you need
> to look elsewhere, but at least you know that they are functions and
> methods, respectively... and you know which are being called with
> which arguments.)
I do love the compilation speed compared to C++ etc., but I don't think that aspect of Go necessarily has to change to be more friendly to DSL design. Ruby gets a ridiculous amount of mileage out of first-class symbols and three very simple syntax rules:
1. Allowing the last argument to a method to be a hash of values
some_method(:a => 13, :z => 'dog')
is similar to Go's variadic parameter lists if they were defined as
map[symbol]interface{}
2. Allowing each method to accept an implicit block (anonymous closure)
def some_method
yield 3, 5
end
some_method do |x, y|
x + y ** 2
end
which is equivalent to
func some_method(block func(x, y int) int {}) int {
block(3, 5)
}
some_method(func(x, y int) int { return x + y ** 2 })
3. braces are optional for function calls except where this would be ambiguous
> Having used and written a number of DSLs, I'd prefer a regular library
> any day of the week. With the one exception (everyone has to have
> one, right?), which is that I'd love to be able to overload arithmetic
> operators for vector and matrix operations and the like. Which is
> effectively a DSL for math and Physics.
I suspect there's an analogue to Greenspun's tenth rule involving ad hoc implementations of Fortran :)
[1] http://github.com/orfjackal/gospec/blob/release/examples/stack_test.go
[2] http://code.google.com/p/specs/wiki/QuickStart
Random thought:
What if there was an operator keyword?
operator (leftOperand[s]) Op (rightOperand[s]) (result) { }
Can of worms, but not having them be functions/methods might save some
problems. (note this isn't operator overloading; it could be, but
that's something else entirely)
In order for it to work, it would either have to be inlineable, or
work with references (no extra syntax) or both. Operand lists > 1
would have to be put in parenthases (maybe) to keep parsing simple.
I'm not entirely sure how much it would benifit in general though.
Otherwise, Go just isn't designed to let you elide syntax wherever
possible, it's designed to be regular, which means it may not be
suited for making up your own internal language if you don't like
being tied down by the syntax.
Thank you all for your feedbacks. I want to cite an interesting (though
anonymous) comment on my blog:
> What about this approach (look at main):
>
> http://pastie.org/pastes/867451
>
> Really, Go has pretty decent literal syntax for most things, so just
> using them w/ some helper functions is the right way for pretty much
> everything, in my experience.
She/he proposed an interesting DSL pattern: a sort of type aliasing
supported by helper functions.
What do you think about this approach?
p.s.
If possible, I'd like to know her/his name in order to give credits.
Thank you,
Hi Steven,
you make a valid point. Indeed, when using varargs it's often necessary to
handle exceptions regarding an incorrect number of arguments. However, I
think that the actual intentions of the OP was merely didascalic without
the claim to give a complete implementation.
Cheers,
On second thought, I wonder if a pure method chaining approach could lead
to an even more readable DSL:
Task("A").DependsOn("B", "C").Do(func() {
// do something
}
The current implementation is:
Task("A", func() {
// do something
}).DependsOn("B", "C")
How does it look?
Cheers,
Hi Steven,
you make a valid point. Indeed, when using varargs it's often necessary to
handle exceptions regarding an incorrect number of arguments. However, I
think that the actual intentions of the OP was merely didascalic without
the claim to give a complete implementation.
Excerpts from Steven's message of dom mar 14 21:01:08 +0100 2010:
On second thought, I wonder if a pure method chaining approach could lead
to an even more readable DSL:
Task("A").DependsOn("B", "C").Do(func() {
// do something
}
The current implementation is:
Task("A", func() {
// do something
}).DependsOn("B", "C")
How does it look?
From the perspective of readability method chaining definitely has advantages, but with Go being both a statement-oriented language and supporting multiple return values there's a natural pressure not to use it extensively.
Rule #1 of writing code: it should play nicely with others.
Rule #1 of writing code: it should play nicely with others.
I assume you're referring to the string arguments passed to DependsOn().
Well, the advantage to use strings rather than task values is that I can
define tasks without worry about order of definition. E.g.
Task("A", func () {}).DependsOn("B")
Task("B", func () {})
The Task() function doesn't run tasks immediately. It's used to set up
tasks attributes and dependencies among tasks. The responsability to
actually invoke tasks in the right order is deferred to a taskmanager
object. If you look at the code of the anonymous poster you'll be aware
that - in her/his implementation - the order of definition does matter.
Hope I answered to your question.