Let's say I store bank accounts information in an immutable Map
:
val m = Map("Mark" -> 100, "Jonathan" -> 350, "Bob" -> 65)
and I want to withdraw, say, $50 from Mark's account. I can do it as follows:
val m2 = m + ("Mark" -> (m("Mark") - 50))
But this code seems ugly to me. Is there better way to write this?
There's no adjust
in the Map
API, unfortunately. I've sometimes used a function like the following (modeled on Haskell's Data.Map.adjust
, with a different order of arguments):
def adjust[A, B](m: Map[A, B], k: A)(f: B => B) = m.updated(k, f(m(k)))
Now adjust(m, "Mark")(_ - 50)
does what you want. You could also use the pimp-my-library pattern to get the more natural m.adjust("Mark")(_ - 50)
syntax, if you really wanted something cleaner.
(Note that the short version above throws an exception if k
isn't in the map, which is different from the Haskell behavior and probably something you'd want to fix in real code.)
This could be done with lenses. The very idea of a lens is to be able to zoom in on a particular part of an immutable structure, and be able to 1) retrieve the smaller part from a larger structure, or 2) create a new larger structure with a modified smaller part. In this case, what you desire is #2.
Firstly, a simple implementation of Lens
, stolen from this answer, stolen from scalaz:
case class Lens[A,B](get: A => B, set: (A,B) => A) extends Function1[A,B] with Immutable {
def apply(whole: A): B = get(whole)
def updated(whole: A, part: B): A = set(whole, part) // like on immutable maps
def mod(a: A)(f: B => B) = set(a, f(this(a)))
def compose[C](that: Lens[C,A]) = Lens[C,B](
c => this(that(c)),
(c, b) => that.mod(c)(set(_, b))
)
def andThen[C](that: Lens[B,C]) = that compose this
}
Next, a smart constructor to create a lens from "larger structure" Map[A,B]
to "smaller part" Option[B]
. We indicate which "smaller part" we want to look at by providing a particular key. (Inspired by what I remember from Edward Kmett's presentation on Lenses in Scala):
def containsKey[A,B](k: A) = Lens[Map[A,B], Option[B]](
get = (m:Map[A,B]) => m.get(k),
set = (m:Map[A,B], opt: Option[B]) => opt match {
case None => m - k
case Some(v) => m + (k -> v)
}
)
Now your code can be written:
val m2 = containsKey("Mark").mod(m)(_.map(_ - 50))
n.b. I actually changed mod
from the answer I stole it from so that it takes its inputs curried. This helps to avoid extra type annotations. Also notice _.map
, because remember, our lens is from Map[A,B]
to Option[B]
. This means the map will be unchanged if it does not contain the key "Mark"
. Otherwise, this solution ends up being very similar to the adjust
solution presented by Travis.
An SO Answer proposes another alternative, using the |+|
operator from scalaz
val m2 = m |+| Map("Mark" -> -50)
The |+|
operator will sum the values of an existing key, or insert the value under a new key.