简体   繁体   中英

Scala programmer - “should there be one obvious way to do it” or “more than one way to do it”?

I have been doing Java for a long time and started Scala about 6 months ago. I love the language. One thing that I discovered is that there are multiple ways to do things. I don't know if it is because of the nature of the language or because it's still young and evolving and idioms and best practices haven't emerged.

What surprises me is I have always been a python over perl person where:

"There should be one-- and preferably only one --obvious way to do it."

makes more sense to me than

"There's more than one way to do it" .

I'm interested to know where you think Scala fit on this scale and why?


Quotes: Python PEP20 and Perl quote

My experience with Scala is that there is more than one way to do things, but relatively few "best" ways to do things. The library has very few gratuitous options for doing things different ways; when it does so, it's usually to enable extra expressiveness or compactness or efficiency in a subset of cases.

For example, if you want to sum an array of integers, you could be completely procedural, which would generate code that was as fast as possible, at the expense of being a little clunky--but if this is really time-critical code, this is the unique best way to solve the problem:

val array = Array(1,2,3,4,5)
var sum = 0
var index = 0
while (index < array.length) {
  sum += array(index)
  index += 1
}
sum

There are equally general functional approaches that are slower when working with primitives (this may change with @specialized in 2.8) but leave you with less tedium:

var sum = 0
Array(1,2,3,4,5).foreach(x => sum += x)
sum

And then there are slightly less general functional constructs that are designed for just this sort of problem (since it comes up a lot) and this is "best" if you want clean, compact code:

Array(1,2,3,4,5).reduceLeft( _ + _ )

And sometimes there are very non-general constructs to do exactly what you want; in Scala 2.8, for example:

Array(1,2,3,4,5).sum

So you get a continuum of choices with tradeoffs in general power, compactness, clarity, and speed. If you can assume that the code needs only to be accessible to people who are quite familiar with Scala, and you know whether you need the absolute best possible performance or not, the good choices are usually rather limited. But, because of the expressive power of the language and library, there are usually many possible if suboptimal ways to do things.

Scala is a multi-paradigm language encompassing many different philosophies and design approaches . These include

  • Object-orientation
  • Functional programming
  • Java-specific approaches
  • Language oriented programming (extending the syntax, metaprogramming)

This allows the programmer to solve problems from many different points of view and therefore in many ways. As you suspected, this is in the very nature of the language (like in other general-purpose/multi paradigm languages).

Which one is used largely depends on the programmer, though I can give some general rules/thoughts:

  • Scala is - after all - a functional language . I therefore consider it a good practice to follow a functional style of programming (which will also lead to very concise and elegant solutions)

  • Be consistent! After all, there should be one way for you , but you can choose which one . Avoid mixing side-effects and pure functions, list-comprehensions and library functions (map, filter), or Java- vs. functional thinking. Instead, use the different paradigms orthogonally where they serve best.

  • Use the wonderful features Scala provides you with (and not just use it as Java with nice syntax): Pattern-matching, continuations, generics and an extremely powerful type system, higher-order functions.

Scala is definitely a more-than-one-way language. In particular, because it is a language highly orthogonal, with as few exceptions as possible, and providing as much as possible through libraries instead of the language itself.

Being orthogonal means language features aren't interdependent. Instead, they can be freely combined to achieve any purpose. This gives the language more expressiveness -- and makes the language more simple --, but it definitely doesn't "guide" the programmer, by either forcing a preferred way, or by disallowing combinations of dubious value.

Few exceptions is mostly a consequence of high orthogonality. Things in Scala aren't disallowed because they don't make sense (in the designer's perspective) or are suboptimal.

And, finally, the emphasis on libraries. Scala doesn't have lists or maps. And arrays are only specified because interoperability with Java requires it to do so. In this sense, it is much like C, as opposed to BASIC. In C, pretty much everything you do except math was done through libraries, written in either C itself or assembler. In BASIC, even commands like opening files or displaying text on the screen are part of the language itself.

There are two practical consequences of this. First, there's a tendency of having a multitude of libraries whose responsibilities overlap. That, of course, is true for any language with a strong library ecosystem, but, in Scala, these libraries look like part of the language itself;

The other practical consequence is that the base libraries require a certain flexibility, which creates some redundancy between them.

For instance, List have an append operator (on Scala 2.8), even though that's suboptimal for lists. It has it because a List is a Seq , and this is a general operation one may want to do with them.

As a final note, the more-than-one-way characteristic of the language can be seen in the very union of functional and object oriented programming.

The most obvious place where scala offers to completely different ways of achieving the same thing is in the area of for comprehensions:

val ls = List(1, 2, 3, 4)
for (l <- ls) println(l) //for comprehension
ls.foreach(println(_))   //underlying form 

This extends to flatMap , filter etc. My personal choice is to stick with the method calls, although in the more complicated examples like:

for (i <- 1 to 10;
     j <- 1 to 20 if (j % 2 == 0)) ...

I can see these are simpler and more intuitive in the for form. For what it's worth, I value the ability to choose your style because people from different backgrounds prefer different things.

The addition of the control constructs to 2.8 like Exceptions is a case in point: coming from a Java background I didn't get at first the power behind these abstractions but if I had started off in functional-land then I would have probably understood straight away.

My main bugbear in Scala is the lack of sensible predicate comprehensions on the option class. Even though for some predicate p :

opt.isEmptyOr( p ) //does not exist
opt.forall( p )

Are equivalent, I don't like the fact that the former does not exist from a readability perspective. Likewise:

opt.notEmptyAnd( p ) //does not exist       
opt.map(p).getOrElse(false)

In my opinion the 2nd form is unnecessarily unreadable! Duplication to achieve readability is surely a positive!

That whole notion is so vague as to be pretty much meaningless. But I suspect that if you were working with a language in which there truly was only one way to do any given thing, you'd be wailing and gnashing your teeth at least 50% of the time.

My (vague) feeling is that in any decent general-purpose programming language, there will always be many ways to accomplish most things.

Disclaimer: I have no idea what Scala authors think about this, I'm just expressing my opinion as a scala (and python and ruby) developer.

Going through the Scala API, you don't see many ways of doing the same thing. For instance, Mutable and Immutable versions of objects have the same interface. Things like this give Scala some coherence.

Being an hybrid language there are usually two ways of solving a problem: the functional and procedural way (foreach vs for). Of course if you wanted to use the procedural way, you would just be using Java.

If you look at functional languages like Lisp and Haskell, there is usually one way to do it for each approach you take (since you can solve a problem in recursive or list-comprehension, for example).

Finally, I believe that any code should be obvious to read. And obvious to write. You can either be very flexible (perl/ruby) or strict (python). Scala is flexible allright (think the syntax of using the "." and parenthesis in method calls, but you can make it as strict as you want. Personally, I like things strict, so I'll write my own code, what I think it's the obvious way.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM