What is the accepted/recommended syntax for Scala

2019-03-14 11:08发布

问题:

In Scala I tend to favour writing large chained expressions over many smaller expressions with val assignments. At my company we've sort of evolved a style for this type of code. Here's a totally contrived example (idea is to show an expression with lots of chained calls):

import scala.util.Random
val table = (1 to 10) map { (Random.nextInt(100), _) } toMap

def foo: List[Int] =
  (1 to 100)
    .view
    .map { _ + 3 }
    .filter { _ > 10 }
    .flatMap { table.get }
    .take(3)
    .toList

Daniel Spiewak's Scala Style Guide (pdf), which I generally like, suggests the leading dot notation in the chained method calls may be bad (see doc: Method Invocation / Higher-Order Functions), though it doesn't cover multi-line expressions like this directly.

Is there another, more accepted/idiomatic way to write the function foo above?

UPDATE: 28-Jun-2011

Lots of great answers and discussion below. There doesn't appear to be a 100% "you must do it this way" answer, so I'm going to accept the most popular answer by votes, which is currently the for comprehension approach. Personally, I think I'm going to stick with the leading-dot notation for now and accept the risks that come with it.

回答1:

The example is slightly unrealistic, but for complex expressions, it's often far cleaner to use a comprehension:

def foo = {
  val results = for {
    x <- (1 to 100).view
    y = x + 3 if y > 10
    z <- table get y
  } yield z
  (results take 3).toList
}

The other advantage here is that you can name intermediate stages of the computation, and make it more self-documenting.

If brevity is your goal though, this can easily be made into a one-liner (the point-free style helps here):

def foo = (1 to 100).view.map{3+}.filter{10<}.flatMap{table.get}.take(3).toList
//or
def foo = ((1 to 100).view map {3+} filter {10<} flatMap {table.get} take 3).toList

and, as always, optimise your algorithm where possible:

def foo = ((1 to 100).view map {3+} filter {10<} flatMap {table.get} take 3).toList
def foo = ((4 to 103).view filter {10<} flatMap {table.get} take 3).toList
def foo = ((11 to 103).view flatMap {table.get} take 3).toList


回答2:

I wrap the entire expression into a set of parenthesis to group things and avoid dots if possible,

def foo: List[Int] =
  ( (1 to 100).view
    map { _ + 3 }
    filter { _ > 10 }
    flatMap { table.get }
    take(3)
    toList )


回答3:

Here's how extempore does it. You can't go wrong.

(specMember
  setInfo   subst(env, specMember.info.asSeenFrom(owner.thisType, sym.owner))
  setFlag   (SPECIALIZED)
  resetFlag (DEFERRED | CASEACCESSOR | ACCESSOR | LAZY)
)

Authentic compiler source!



回答4:

I prefer lots of vals:

def foo = {
  val range = (1 to 100).view
  val mappedRange = range map { _+3 }
  val importantValues = mappedRange filter { _ > 10 } flatMap { table.get }
  (importantValues take 3).toList
}

Because I don't know what you want to purpose with your code, I chose random names for the vals. There is a big advantage to choose vals instead of the other mentioned solutions:

It is obvious what your code does. In your example and in the solutions mentioned in most other answers anyone does not know at first sight what it does. There is too much information in one expression. Only in a for-expression, mentioned by @Kevin, it is possible to choose telling names but I don't like them because:

  1. They need more lines of code
  2. They are slower due to pattern match the declared values (I mentioned this here).
  3. Just my opinion, but I think they look ugly


回答5:

My rule: if the expression fits on a single (80-120 character) line, keep it on one line and omit the dots wherever possible:

def foo: List[Int] = 
   (1 to 100).view map { _ + 3 } filter { _ > 10 } flatMap table.get take 3 toList

As Kevin pointed out, the point-free style may improve brevity (but could harm readability for developers not familiar with it):

def foo: List[Int] = 
   (1 to 100).view map{3+} filter{10<} flatMap table.get take 3 toList

The leading dot notation is perfectly acceptable if you need to separate the expression over multiple lines due to length. Another reason to use this notation is when the operations need individual comments. If you need to spread an expression over multiple lines, due to its length or the need to comment individual operations, it's best to wrap the entire expression in parens (as Alex Boisvert suggests. In these situations, each (logical) operation should go on its own line (i.e. each operation goes on a single line, except where multiple consecutive operations can be described succinctly by a single comment):

def foo: List[Int] = 
   ( (1 to 100).view
     map { _ + 3 }
     filter { _ > 10 }
     flatMap table.get
     take 3
     toList )   

This technique avoids potential semicolon inference issues that can arise when using leading dot notation or calling a 0-arg method at the end of the expression.



回答6:

I usually try to avoid using dot for things like map and filter. So I would probably write it like the following:

def foo: List[Int] =
  (1 to 100).view map { x =>
    x + 3 } filter { x =>
    x > 10 } flatMap { table.get } take(3) toList

The leading dot notation is very readable. I might start using that.