Jawap
Jawap

Reputation: 2543

How to "map" an array to a single new object?

I have an Array[Float] and I'd like to use the elements of that array to initialize a new object.

In a classic, imperative way, I could of course do simply do:

val array: Array[Float] = ...
val rect: Rect = new Rect(Point(array(0), array(1), Size(array(2), array(3))

Is there a way to do it in a more functional way using some type of a "map"? Something like this:

array => new Rect(Point(_(0), _(1)), Size(_(2), _(3)))

I'm trying to parse a CSV file:

string.split("\n") map {
  line => line.split(".") map { 
    value => value.trim.toDouble
  } => new Rect(Point(_(0), _(1)), Size(_(2), _(3))) // something like this?
}

Upvotes: 2

Views: 122

Answers (3)

Markus1189
Markus1189

Reputation: 2869

Is the size of the array fixed? You could use pattern matching:

.map { case Array(x, y, w, h) => new Rect(Point(x, y), Size(w, h)) }

Example:

scala> "1,2,3,4\n5,6,7,8".split("\n").map(_.split(",")).map { case Array(x,y,w,h) => (x,y,w,h) }
res3: Array[(String, String, String, String)] = Array((1,2,3,4), (5,6,7,8))

Upvotes: 2

flavian
flavian

Reputation: 28511

The above suggestion is good but error prone, it uses an extractor, eg the unnapply on Array added by Scala. The problem I see is getting a MatchError if the CSV file is corrupted.

This approach would simply skip the lines in the file which don't match the criteria, but what if you just want to take the first 4 lines?

csv.lines.map {
  case Array(x, y, w, h) => Some(new Rect(Point(x, y), Size(w, h)))
  case _ => None
} flatten

If you want to simply extract the first four variables, even if the CSV line has more than 4 elements, you can use the a slightly different syntax, which is a way of saying the line should have "at least 4 elements".

csv.lines.map {
   case Array(x, y, w, h, _*) => Some(new Rect(Point(x, y), Size(w, h)))
   case _ => None
} flatten

You can even obtain a reference to the "rest of the elements" with the tail @ _* syntax. If you would like to have more fine grained control over the parsing and have some nice error reporting:

val detailed: Array[Try[Rect]] = csv.lines.map {
  case Array(x, y, w, h, tail @ _: *) => {
    Success(new Rect(Point(x, y), Size(w, h)))
  }
  case arr @ _ => Failure(s"Entry had ${arr.size} elements, expected at least 4. ${arr.mkString(",")}")
}

For even better flow control, I'd separate the map logic in something like parseRow(input: Array[String]): Try[Rect].

def parseRow(input: Array[String]): Try[Rect] = input match {
  case Array(x, y, w, h, tail @ _: *) => {
    Success(new Rect(Point(x, y), Size(w, h)))
  }
  case arr @ _ => Failure(s"Entry had ${arr.size} elements, expected at least 4. ${arr.mkString(",")}")
}

You could even think about more fine-grained handling of any conversions you will have to perform, to go from instance from a string input to a Double or whatever math type is needed for Rect.

Here's a full setup that I would use:

  case class Point(x: Double, y: Double)
  case class Size(x: Double, y: Double)
  class Rect(val p: Point, val s: Size)

  def parseDouble(input: String): Try[Double] = Try(input.toDouble)

  def parseRow(input: Array[String]): Try[Rect] = input match {
    case Array(x, y, w, h, tail @ _*) => {
      for {
        pointX <- parseDouble(x)
        pointY <- parseDouble(y)
        pointW <- parseDouble(w)
        pointY <- parseDouble(y)
      } yield {
        new Rect(Point(pointX, pointY), Size(pointW, pointY))
      }
    }
    case arr @ _ => Failure(new Exception(s"Entry had ${arr.length} elements, expected at least 4. ${arr.mkString(",")}"))
  }

And you can very easily re-use the above:

val rects = csv.lines.map(parseRow)

Upvotes: 1

Victor Moroz
Victor Moroz

Reputation: 9225

Not sure about being functional, but repeating variable name 4 times like array(n) can arguably be improved by destructuring:

val Array(x, y, w, h) = "1,2,3,4".split(",").map(_.trim.toDouble)
new Rect(Point(x, y), Size(w, h))

(similar to the Markus1189 answer, only it doesn't have to be an anonymous function)

I would say it's more about readability than being functional

Upvotes: 2

Related Questions