3

I want to implement a function called map that receives a list and a function and the yields the result of applying the function to each element of the input list. So I have this so far:

def map(list: List[Int], function: (Int) => Int): List[Int] = {
  def loop(list: List[Int], acc: List[Int]): List[Int] = {
    list match {
      case Nil => acc
      case head :: tail => loop(list.tail, function(head) :: acc)
    }
  }
  loop(list.reverse, Nil)
}

This code works and gives me the expected result, but I cannot help thinking there is a more elegant and efficient way that doesn't involve using reverse or another list method that isn't head or tail.

1
  • 3
    No, it is not. You had three options: 1) Two tail-recursive traverses over the list (what you did, however it is more common to reverse the list at the end). 2) Traverse the list once with a non-tail recursive function that can blow up your stack. 3) Use mutation and a standard while loop to achieve stack safety and only one traverse (that is what the implementation of the standard library does). - Well, you can also implement map in terms of other functions, like flatMap, foldLeft, foldRight. But, all of these converge to the previous three cases. Commented Apr 24, 2019 at 20:38

2 Answers 2

8

As far as rolling your own implementation of a purely functional map function, without using any mutable state or built-in List higher-order functions, you've done a great job! Having to reverse the list does seem unnecessary, but it's worth it because prepending to a list is a very efficient operation.

As well as the other methods suggested in the comments, you could make acc a scala.collection.mutable.ListBuffer (which supports efficient append and prepend operations), then converting it to a list when you're done. However, the conversion process isn't a great improvement over reverse.

Otherwise, there are a few minor things you can do to improve your map function:

  • By using curried arguments, you can use map more elegantly.
  • Functions that need to be tail-recursive should always be decorated with the scala.annotation.tailrec attribute. The Scala compiler then knows your intention, and will issue an error if the function cannot be optimized to use tail recursion.
  • It's trivial to make this function generic, which will make it far more widely useful.
  • It's generally more idiomatic to perform the reverse operation on the result. It makes no difference for a map operation, but if you were to write a filter operation, for example, then it would be more efficient to reverse the potentially smaller set of filtered elements.

Here's how that would then look:

import scala.annotation.tailrec

def map[A, B](list: List[A])(function: (A) => B): List[B] = {

  @tailrec
  def loop(rem: List[A], acc: List[B]): List[B] = rem match {
    case Nil => acc.reverse
    case head :: tail => loop(tail, function(head) :: acc)
  }
  loop(list, Nil)
}
Sign up to request clarification or add additional context in comments.

Comments

4

Because head :: tail is the most efficient way to deconstruct and reconstruct a List, what you've done is a pretty common pattern. The .reverse is unfortunate but usually amounts to a minor performance penalty.

As mentioned in the comments, map can also be implemented via other Standard Library methods. Here it's done with foldRight:

def map(list :List[Int], function :Int => Int) :List[Int] = 
  list.foldRight(List.empty[Int])(function(_) :: _)

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.