16

Coming from a C/C++ background, I'm not very familiar with the functional style of programming so all my code tends to be very imperative, as in most cases I just can't see a better way of doing it.

I'm just wondering if there is a way of making this block of Scala code more "functional"?

var line:String = "";
var lines:String = "";

do {
    line = reader.readLine();
    lines += line;
} while (line != null)
Kristina
  • 15,859
  • 29
  • 111
  • 181
  • Psst, reading an entire file by doing line-by-line string appends is liable to be an `O(n^2)` operation! (Formally it is; optimizations _might_ save you, but don't count on it.) So this isn't a good way to write even imperative code unless your files are tiny. (You'd use `StringBuilder` instead as the accumulator.) – Rex Kerr Aug 13 '11 at 19:30
  • **@Rex** Is it because of the abandoned String objects which are left behind when you do `+=` as Strings are immutable? – Kristina Aug 13 '11 at 20:22
  • 1
    @Nick The main problem is that every time you do `s1 + s2` it has to create an entirely new string of size `s1.size + s2.size`, at proportional cost. Whereas when you append something to a `StringBuilder`, you don't have to pay again for the current contents of the `StringBuilder` (amortized cost, anyway). – Kipton Barros Aug 13 '11 at 20:43

4 Answers4

30

How about this?

val lines = Iterator.continually(reader.readLine()).takeWhile(_ != null).mkString
Kipton Barros
  • 21,002
  • 4
  • 67
  • 80
14

Well, in Scala you can actually say:

val lines = scala.io.Source.fromFile("file.txt").mkString

But this is just a library sugar. See Read entire file in Scala? for other possiblities. What you are actually asking is how to apply functional paradigm to this problem. Here is a hint:

Source.fromFile("file.txt").getLines().foreach {println}

Do you get the idea behind this? foreach line in the file execute println function. BTW don't worry, getLines() returns an iterator, not the whole file. Now something more serious:

lines filter {_.startsWith("ab")} map {_.toUpperCase} foreach {println}

See the idea? Take lines (it can be an array, list, set, iterator, whatever that can be filtered and which contains an items having startsWith method) and filter taking only the items starting with "ab". Now take every item and map it by applying toUpperCase method. Finally foreach resulting item print it.

The last thought: you are not limited to a single type. For instance say you have a file containing integer number, one per line. If you want to read that file, parse the number and sum them up, simply say:

lines.map(_.toInt).sum
Community
  • 1
  • 1
Tomasz Nurkiewicz
  • 334,321
  • 69
  • 703
  • 674
2

To add how the same can be achieved using the formerly new nio files which I vote to use because it has several advantages:

val path: Path = Paths.get("foo.txt")
val lines = Source.fromInputStream(Files.newInputStream(path)).getLines()

// Now we can iterate the file or do anything we want,
// e.g. using functional operations such as map. Or simply concatenate.
val result = lines.mkString

Don't forget to close the stream afterwards.

kap
  • 1,456
  • 2
  • 19
  • 24
  • Isn't this equivalent to `val lines = Source.fromFile("foo.txt").getLines(); val result = lines.mkString`? What are the advantages? – user unknown Mar 06 '18 at 09:45
  • it uses an input stream which is an advantage, e.g. when testing. In actual code, when the input stream is part of the interface and not a string for a file name, the file can easily be replaced with some in memory data, e.g. buffered string or even an in memory file system. – kap Mar 06 '18 at 10:28
1

I find that Stream is a pretty nice approach: it create a re-traversible (if needed) sequence:

def loadLines(in: java.io.BufferedReader): Stream[String] = {
  val line = in.readLine
  if (line == null) Stream.Empty
  else Stream.cons(line, loadLines(in))
}

Each Stream element has a value (a String, line, in this case), and calls a function (loadLines(in), in this example) which will yield the next element, lazily, on demand. This makes for a good memory usage profile, especially with large data sets -- lines aren't read until they're needed, and aren't retained unless something is actually still holding onto them. Yet you can also go back to a previous Stream element and traverse forward again, yielding the exact same result.

Tim
  • 766
  • 1
  • 7
  • 13