We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies.

We use cookies and other tracking technologies to improve your browsing experience on our site, analyze site traffic, and understand where our audience is coming from. To find out more, please read our privacy policy.

By choosing 'I Accept', you consent to our use of cookies and other tracking technologies. Less

We use cookies and other tracking technologies... More

Login or register
to apply for this job!

Login or register
to save this job!

Login or register to start contributing with an article!

Login or register
to see more jobs from this company!

Login or register
to boost this post!

Show some love to the author of this blog by giving their post some rocket fuel 🚀.

Login or register to search for your ideal job!

Login or register to start working on this issue!

Engineers who find a new job through Functional Works average a 15% increase in salary 🚀

Blog hero image

Avoid pattern matching with List in Scala

Anton Fagerberg 8 March, 2018 | 3 min read

I recently debugged a strange issue which seemed to appear out of nowhere™. It all came down to a that we used List in a match while the methods signature was changed from List to Seq.

Below is a simplification of what happened and how it can be avoided. It's one of those things which seem obvious in hindsight, but in a complex system, these things can be tricky to spot.

In our code, we had a method which took a list of something and depending on the number of items in it we wanted to do something.

Here's an simplified example:

def specialSum(numbers: List[Int]): Int = {
  numbers match {
    case List(a, b) => a + b
    case Nil => -1
    case _ => 0
  }
}

println(specialSum(List(1, 2))) // 3
println(specialSum(List(1)))    // 0
println(specialSum(List.empty)) // -1

When starting to use this method in our code base, we had other collections than List that we wanted to use (and using .toList everywhere is tedious). So List was changed to Seq to accomodate this:

// numbers was changed from List to Seq
def specialSum(numbers: Seq[Int]): Int = {
  numbers match {
    case List(a, b) => a + b
    case Nil => -1
    case _ => 0
  }
}

println(specialSum(List(1, 2))) // 3
println(specialSum(List(1)))    // 0
println(specialSum(List.empty)) // -1

With List it still works. Now, one may write a unit test using Seq in order to verify that the method's changed type signature still works:

def specialSum(numbers: Seq[Int]): Int = {
  numbers match {
    case List(a, b) => a + b
    case Nil => -1
    case _ => 0
  }
}

println(specialSum(Seq(1, 2))) // 3
println(specialSum(Seq(1)))    // 0
println(specialSum(Seq.empty)) // -1

This still works but this is just luck (or unlucky for us). This is because the Seq's default implementation is List. So when we say Seq(1, 2) we actually (under the hood) get a List(1, 2).

You can verify this with:

Seq(1,2).isInstanceOf[List[_]]

What happened in our code was that we wanted to pass in some other type which inhertis from Seq, for example ArrayBuffer.

def specialSum(numbers: Seq[Int]): Int = {
  numbers match {
    case List(a, b) => a + b
    case Nil => -1
    case _ => 0
  }
}

println(specialSum(ArrayBuffer(1, 2))) // 0 <- not what we want
println(specialSum(ArrayBuffer(1)))    // 0
println(specialSum(ArrayBuffer.empty)) // -1

What happens here is that our ArrayBuffer is passed in and is being "downgraded" to a Seq.

The match has three cases:

  • Is the Seq of the more specific type List and does it have to elements. (This is no longer true.)
    • Nil is a short hand for empty List, but ArrayBuffer.empty[Int] == Nil will still hold true so it works.
    • _ will match everything independent of type.

Notice also that since we do match on _ all compiler warnings and runtime errors (MatchError) will dissapear since we cover all possible outcomes.

If we did:

  def specialSum(numbers: Seq[Int]): Int = {
  numbers match {
    case List(a, b) => a + b
    case Nil => -1
    case _: List[Int] => 0
  }
}

println(specialSum(ArrayBuffer(1, 2))) // 3
println(specialSum(ArrayBuffer(1)))    // 0
println(specialSum(ArrayBuffer.empty)) // -1

We'd get a MatchError.

In order to be safe, we can make sure that we always use Seq in the match since this will cover all collections which inherits from it (more info here).

def specialSum(numbers: Seq[Int]): Int = {
  numbers match {
    case Seq(a, b) => a + b
    case Seq() => -1 // or Nil will work
    case _ => 0
  }
}

println(specialSum(ArrayBuffer(1, 2))) // 3 
println(specialSum(ArrayBuffer(1)))    // 0
println(specialSum(ArrayBuffer.empty)) // 0

As a bonus, if you for some reason prefer to use the cons notation:

def specialSum(numbers: Seq[Int]): Int = {
  numbers match {
    case a :: b :: Nil => a + b
    case Nil => -1
    case _ => 0
  }
}

println(specialSum(ArrayBuffer(1, 2))) // 0 <-- wrong
println(specialSum(ArrayBuffer(1)))    // 0
println(specialSum(ArrayBuffer.empty)) // -1

You can make it work by using +: (since :: is List specific):

def specialSum(numbers: Seq[Int]): Int = {
  numbers match {
    case a +: b +: Nil => a + b
    case Nil => -1
    case _ => 0
  }
}

println(specialSum(ArrayBuffer(1, 2))) // 3
println(specialSum(ArrayBuffer(1)))    // 0
println(specialSum(ArrayBuffer.empty)) // -1

I wouldn't recommend it though, I personally don't find it readable (and I'm actually surprised it works with +: Nil, I thought +: Seq() was required in order to not get a List).

Update: It was pointed out to me that +: is slower than ::. If you are recursively traversing a very long sequence, then List and :: will give you much better performance.

Originally published on www.antonfagerberg.com

Author's avatar
Anton Fagerberg
Long time Scala developer, functional programming enthusiast, Øredev program committee
    OCaml
    Elm
    Erlang
    Elixir
    F#
    Scala
    Rust
    Clojure
    Haskell

Related Issues

viebel / klipse-clj
viebel / klipse-clj
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Open
  • 0
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • 1
  • 0
  • Intermediate
  • Clojure
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
  • $80
viebel / klipse
  • Open
  • 0
  • 0
  • Advanced
  • Clojure
  • $80
viebel / klipse
  • Started
  • 0
  • 2
  • Advanced
  • Clojure
  • $180
viebel / klipse
  • Started
  • 0
  • 1
  • Intermediate
  • Clojure
viebel / klipse
  • 1
  • 1
  • Advanced
  • Clojure
  • $300

Get hired!

Sign up now and apply for roles at companies that interest you.

Engineers who find a new job through Functional Works average a 15% increase in salary.

Start with GithubStart with Stack OverflowStart with Email