Reputation: 2543
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
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
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
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