When imperative style fits better?
Asked Answered
R

4

15

From the Programming in Scala (second edition), bottom of the p.98:

A balanced attitude for Scala programmers

Prefer vals, immutable objects, and methods without side effects. Reach for them first. Use vars, mutable objects, and methods with side effects when you have a specific need and justification for them.

It is explained on previous pages why to prefer vals, immutable objects, and methods without side effects so this sentence makes perfect sense.

But second sentence:"Use vars, mutable objects, and methods with side effects when you have a specific need and justification for them." is not explained so well.

So my question is:

What is justification or specific need to use vars, mutable objects and methods with side effect?


P.s.: It would be great if someone could provide some examples for each of those (besides explanation).

Rolon answered 1/2, 2012 at 10:3 Comment(3)
A possibly too obvious example: any input/output your program does is a side effect and can't be done without methods with side effects.Logistic
It's great example! Not see the forest for the trees... I think the combination of your comment and @HeikoSeeberger answer explains it all...Rolon
My rule of thumb: try your best to use the immutable and pure approach, and if you have doubts or performance issues, ask on irc and StackOverflow. :)Commissure
D
16

In many cases functional programming increases the level of abstraction and hence makes your code more concise and easier/faster to write and understand. But there are situations where the resulting bytecode cannot be as optimized (fast) as for an imperative solution.

Currently (Scala 2.9.1) one good example is summing up ranges:

(1 to 1000000).foldLeft(0)(_ + _)

Versus:

var x = 1
var sum = 0
while (x <= 1000000) {
  sum += x
  x += 1
}

If you profile these you will notice a significant difference in execution speed. So sometimes performance is a really good justification.

Daguerre answered 1/2, 2012 at 10:48 Comment(5)
Sure, one can use sum. But I wanted to point out the differences in the "pure" approaches.Daguerre
def seriesSum(a1:Int,an:Int,n:Int) = n/2*(a1+an) ?Terracotta
@Rolon I find it very suspect that (1 to 1000000).sum was slower, since it runs in constant time, as opposed to the while loop.Hick
@DanielC.Sobral - Which version? It defaults to IndexedSeq's implementation as of 2.9.1 (but the docs don't highlight that fact). It looks ~25x slower to me.Birthplace
@Rolon Ah, ok, it only got changed last December. I probably confused it with size/length.Hick
B
8

Ease of Minor Updates

One reason to use mutability is if you're keeping track of some ongoing process. For example, let's suppose I am editing a large document and have a complex set of classes to keep track of the various elements of the text, the editing history, the cursor position, and so on. Now suppose the user clicks on a different part of the text. Do I recreate the document object, copying many fields but not the EditState field; recreate the EditState with new ViewBounds and documentCursorPosition? Or do I alter a mutable variable in one spot? As long as thread safety is not an issue then is is much simpler and less error-prone to just update a variable or two than to copy everything. If thread safety is an issue, then protecting from concurrent access may be more work than using the immutable approach and dealing with out-of-date requests.

Computational efficiency

Another reason to use mutability is for speed. Object creation is cheap, but simple method calls are cheaper, and operations on primitive types are cheaper yet.

Let's suppose, for example, that we have a map and we want to sum the values and the squares of the values.

val xs = List.range(1,10000).map(x => x.toString -> x).toMap
val sum = xs.values.sum
val sumsq = xs.values.map(x => x*x).sum

If you do this every once in a while, it's no big deal. But if you pay attention to what's going on, for every list element you first recreate it (values), then sum it (boxed), then recreate it again (values), then recreate it yet again in squared form with boxing (map), then sum it. This is at least six object creations and five full traversals just to do two adds and one multiply per item. Incredibly inefficient.

You might try to do better by avoiding the multiple recursion and passing through the map only once, using a fold:

val (sum,sumsq) = ((0,0) /: xs){ case ((sum,sumsq),(_,v)) => (sum + v, sumsq + v*v) }

And this is much better, with about 15x better performance on my machine. But you still have three object creations every iteration. If instead you

case class SSq(var sum: Int = 0, var sumsq: Int = 0) {
  def +=(i: Int) { sum += i; sumsq += i*i }
}
val ssq = SSq()
xs.foreach(x => ssq += x._2)

you're about twice as fast again because you cut the boxing down. If you have your data in an array and use a while loop, then you can avoid all object creation and boxing and speed up by another factor of 20.

Now, that said, you could also have chosen a recursive function for your array:

val ar = Array.range(0,10000)
def suma(xs: Array[Int], start: Int = 0, sum: Int = 0, sumsq: Int = 0): (Int,Int) = {
  if (start >= xs.length) (sum, sumsq)
  else suma(xs, start+1, sum+xs(start), sumsq + xs(start)*xs(start))
}

and written this way it's just as fast as the mutable SSq. But if we instead do this:

def sumb(xs: Array[Int], start: Int = 0, ssq: (Int,Int) = (0,0)): (Int,Int) = {
  if (start >= xs.length) ssq
  else sumb(xs, start+1, (ssq._1+xs(start), ssq._2 + xs(start)*xs(start)))
}

we're now 10x slower again because we have to create an object on each step.

So the bottom line is that it really only matters that you have immutability when you cannot conveniently carry your updating structure along as independent arguments to a method. Once you go beyond the complexity where that works, mutability can be a big win.

Cumulative Object Creation

If you need to build up a complex object with n fields from potentially faulty data, you can use a builder pattern that looks like so:

abstract class Built {
  def x: Int
  def y: String
  def z: Boolean
}
private class Building extends Built {
  var x: Int = _
  var y: String = _
  var z: Boolean = _
}

def buildFromWhatever: Option[Built] = {
  val b = new Building
  b.x = something
  if (thereIsAProblem) return None
  b.y = somethingElse
  // check
  ...
  Some(b)
}

This only works with mutable data. There are other options, of course:

class Built(val x: Int = 0, val y: String = "", val z: Boolean = false) {}
def buildFromWhatever: Option[Built] = {
  val b0 = new Built
  val b1 = b0.copy(x = something)
  if (thereIsAProblem) return None
  ...
  Some(b)
}

which in many ways is even cleaner, except you have to copy your object once for each change that you make, which can be painfully slow. And neither of these are particularly bulletproof; for that you'd probably want

class Built(val x: Int, val y: String, val z: Boolean) {}
class Building(
  val x: Option[Int] = None, val y: Option[String] = None, val z: Option[Boolean] = None
) {
  def build: Option[Built] = for (x0 <- x; y0 <- y; z0 <- z) yield new Built(x,y,z)
}

def buildFromWhatever: Option[Build] = {
  val b0 = new Building
  val b1 = b0.copy(x = somethingIfNotProblem)
  ...
  bN.build
}

but again, there's lots of overhead.

Birthplace answered 1/2, 2012 at 16:12 Comment(0)
A
5

I've found that imperative / mutable style is better fit for dynamic programming algorithms. If you insist on immutablility, it's harder to program for most people, and you end up using vast amounts of memory and / or overflowing the stack. One example: Dynamic programming in the functional paradigm

Autobiographical answered 1/2, 2012 at 15:14 Comment(0)
L
3

Some examples:

  1. (Originally a comment) Any program has to do some input and output (otherwise, it's useless). But by definition, input/output is a side effect and can't be done without calling methods with side effects.

  2. One major advantage of Scala is ability to use Java libraries. Many of them rely on mutable objects and methods with side-effects.

  3. Sometimes you need a var due to scoping. See Temperature4 in this blog post for an example.

  4. Concurrent programming. If you use actors, sending and receiving messages are a side effect; if you use threads, synchronizing on locks is a side effect and locks are mutable; event-driven concurrency is all about side effects; futures, concurrent collections, etc. are mutable.

Logistic answered 1/2, 2012 at 12:36 Comment(5)
Also, dataflow/frp handles concurrent/event-driven programming without side effects. STM handle locks without side-effects. The case in 3 can also be handled by dataflow/frp. Instead of futures, once can use continuations. So I don't think any of these examples makes the case for side effects or mutability.Hick
"and then, at main, just feed the I/O, as if it was a simple Unix pipe" But in Scala that still requires calling methods with side effects, and making main itself a method with side effects.Logistic
Does STM handle locks without side-effects anywhere in the implementation? ScalaSTM certainly doesn't, and I can't find where readTVar# etc. are defined in GHC.Logistic
@Daniel: You can't defer all I/O if your program interacts with other systems (or a user). Then your computation will depend on input, which in turn is affected by previous output of the program. Sure, you can compute the response to each input in a pure fashion, but sometimes the outer layer that deals with the I/O is the more complex part of your program. You can't fix that simply by deferring I/O and having main be the only impure function in your code.Hypophysis
@Daniel: I nearly mentioned FRP in my last comment, but decided it overcomplicated things. :) However that's still far from mainstream yet (I'm rather excited by it though). Even in Haskell, the usual method of dealing with complex I/O dependent code is to have an outer layer in the IO monad which is essentially an imperative program.Hypophysis

© 2022 - 2024 — McMap. All rights reserved.