Sponsored Link •
|
Summary
Last week I released a new version of ScalaTest (0.9.5) that includes a "matchers DSL" for writing more expressive assertions in tests. In this post I show differences between ScalaTest matchers and those in Ruby's RSpec tool, and discuss some of the general differences in DSL creation in Ruby and Scala.
Advertisement
|
Dynamic languages such as Ruby and Groovy have a reputation for enabling "internal" domain specific language (DSL) creation, but internal DSLs are not only a feature of dynamic languages. Although Scala is statically typed, its flexible syntax is quite accommodating to internal DSLs. However, the different languages place different constraints on DSL design.
One feature of Ruby that helps in DSL creation, for example, is that you can leave off parentheses when you invoke a method. For example, given a string in Ruby:
>> s = "hello" => "hello"
You can determine whether it contains a substring like this:
>> s.include?("el") => true
Or, alternatively, by leaving off the parentheses, like this:
>> s.include? "el" => true
Scala does not let you leave off the parentheses in the same way:
scala> val s = "hello" s: java.lang.String = hello scala> s.contains("el") res5: Boolean = true scala> s.contains "el":1: error: ';' expected but string literal found. s.contains "el" ^
However, Scala supports an "operator notation," which allows you to leave off both the dot and the parentheses:
scala> s contains "el" res6: Boolean = true
By contrast, Ruby does not support this kind of operator notation:
>> s include? "el" (irb):21: warning: parenthesize argument(s) for future version NoMethodError: undefined method `include?' for main:Object from (irb):21
Another feature of Ruby that facilitates DSL creation is its open classes, which among other things allows you to add new methods to existing classes. For example, class String
in Ruby has no method named should
:
>> "".should NoMethodError: undefined method `should' for "":String from (irb):1
Nevertheless, here's how you could, using open classes, add a method named should
to class String
in Ruby:
>> class String >> def should >> "should was invoked!" >> end >> end => nil
Now you can invoke should
on Ruby String
:
>> puts "".should should was invoked! => nil
Scala, being statically typed, doesn't support open classes. The methods supported by a class are fixed at compile time. However, Scala's implicit conversion feature provides much the same benefit, allowing you to write code in which it appears you are invoking new methods on existing classes. For example, because Scala's string is java.lang.String
, you can't invoke should
on it:
scala> "".should:5: error: value should is not a member of java.lang.String "".should ^
Nevertheless, you can define an implicit conversion from String
to a type that does have a should
method. The Scala compiler will apply the implicit conversion to solve a type error. Here's how you could define the implicit conversion:
scala> class ShouldWrapper(s: String) { | def should = "should was invoked on " + s | } defined class ShouldWrapper scala> implicit def convert(s: String) = new ShouldWrapper(s) convert: (String)ShouldWrapper
Given this implicit conversion, you can now write code that appears to invoke should on a string:
scala> "howdy".should res10: java.lang.String = should was invoked on howdy
Behind the scenes, the Scala compiler will implicitly convert the String
to a ShouldWrapper
, and then invoke should
on the ShouldWrapper
, like this:
scala> convert("howdy").should res11: java.lang.String = should was invoked on howdy
Ruby's RSpec tool includes a matchers DSL, that allows you to write assertions in tests that look like this:
result.should be_true # this is RSpec result.should_not be_nil num.should eql(5) map.should_not have_key("a")
One thing to note is that Ruby's convention of separating words with underscores helps make these expressions read more like English. Between each word is either a space, underscore, or dot. In Scala, you could use operator notation to get rid of the dot, yielding expressions like:
result should be_true // Could do this in Scala result should_not be_null num should eql(5) map should_not have_key("a")
The problem is that this use of the underscore is not idiomatic in Scala. Like Java, Scala style suggests using camel case, which would yield expressions like:
result should beTrue // Could do this in Scala result shouldNot beNull num should eql(5) map shouldNot haveKey("a")
This works, but is not quite as satisfying, because the words do not separate as nicely in camel case compared to underscores. When designing a matchers DSL for ScalaTest, I decided to try and see how far I could go with operator notation. The corresponding expressions in ScalaTest are:
result should be (true) // This is ScalaTest result should not be (null) num should equal (5) map should not contain key ("a")
The parentheses on the rightmost value are not always required, but the rule is subtle, so I recommend you always use them. The parentheses also serve to emphasize what is usually the expected value. Here's how one of these expressions will be rewritten by the Scala compiler, when it desugars the operator notation back into normal method call notation during compilation:
result.should(not).be(null)
The should
method is invoked on result
(via an implicit conversion), passing in the object referred to by a variable named not
. Then be
is invoked on that return value, passing in null
. In other words, in this expression, operator notation is used twice in a row.
When designing an internal DSL, you don't have as much freedom as when you design an external DSL—i.e., a new language from scratch. With an internal DSL you need to work within the confines of the host language, and so will your users. In RSpec's matchers, for example, users need to keep track of where to put dots, underscores, and spaces. Similarly, in ScalaTest matchers, users need to keep track of where to put parentheses. In both cases, the syntax is nevertheless quite easy to learn, and the resulting code is quite readable.
RSpec:
http://rspec.info/
ScalaTest
http://www.artima.com/scalatest
Programming in Scala
http://www.artima.com/shop/programming_in_scala
Have an opinion? Readers have already posted 6 comments about this weblog entry. Why not add yours?
If you'd like to be notified whenever Bill Venners adds a new entry to his weblog, subscribe to his RSS feed.
Bill Venners is president of Artima, Inc., publisher of Artima Developer (www.artima.com). He is author of the book, Inside the Java Virtual Machine, a programmer-oriented survey of the Java platform's architecture and internals. His popular columns in JavaWorld magazine covered Java internals, object-oriented design, and Jini. Active in the Jini Community since its inception, Bill led the Jini Community's ServiceUI project, whose ServiceUI API became the de facto standard way to associate user interfaces to Jini services. Bill is also the lead developer and designer of ScalaTest, an open source testing tool for Scala and Java developers, and coauthor with Martin Odersky and Lex Spoon of the book, Programming in Scala. |
Sponsored Links
|