2
votes

I had spent weeks on trying to understand the idea behind "lifting" in scala.

Originally, it was from the example related to Chapter 4 of book "Functional Programming in Scala"

Then I found below topic "How map work on Options in Scala?"

The selected answer specify that:

def map[B](f: A => B): Option[B] = this match (Let's considered this as (*) )

So, from above code, I assume that function "map" is derived from function match. Hence, the mechanism behind "map" is a kind of pattern matching to provide a case selection between Some, and None

Then, I created below examples by using function map for Seq, Option, and Map (Let's considered below examples as (**) )

Example 1: map for Seq
  val xs = Seq(1, 2, 3)
  xs.map(println)

Example 2: map for Option
val a:Option[Int] = Some(5)
  a.map(println)
val b:Option[Int] = None
  b.map(println)

Example 3: map for Map
val capitals = Map("France" -> "Paris", "Japan" -> "Tokyo")
capitals.map(println)

From (*) and (**), I could not know whether "map" is a pattern matching or an iteration, or both.

Thank you for helping me to understand this.

3

3 Answers

2
votes

From the ScalaDocs page we can see that the type profile for the Standard Library map() method is a little different.

def map[B](f: (A) => B): Seq[B]

So the Standard Library map() is the means to transition from a collection of elements of type A to the same collection but the elements are type B. (A and B might be the same type. They aren't required to be different.)

So, yes, it iterates through the collection applying function f() to each element A to create each new element B. And function f() might use pattern matching in its code, but it doesn't have to.

Now consider a.map(println). Every element of a is sent to println which returns Unit. So if a is List[Int] then the result of a.map(println) is List[Unit], which isn't terribly useful.

When all we want is the side effect of sending information to StdOut then we use foreach() which doesn't create a new collection: a.foreach(println)

2
votes

@Jwvh provided a more programming based answer but I want to dig a little bit deeper.

I certainly appreciate you trying to understand how things work in Scala, however if you really want to dig that deep, I am afraid you will need to obtain some basic knowledge of Category Theory since there is no "idea behind lifting in scala" but just the "idea behind lifting"

This is also why functions like "map" can be very confusing. Inherently, programmers are taught map etc. as operations on collections, where as they are actually operations that come with Functors and Natural Transformations (this is normally referred to as fmap in Category Theory and also Haskell).

Before I move on, the short answer is it is a pattern matching in the examples you gave and in some of them it is both. Map is defined specifically to the case, the only condition is that it maintains functoriality

Attention: I will not be defining every single term below, since I would need to write a book to build up to some of the following definitions, interested readers are welcome to research them on their own. You should be able to get some basic understanding by following the types

Let's consider these as Functors, the definition will be something along the lines of this:

In (very very) short, we consider types as objects in the category of our language. The functions between these types (type constructors) are morphisms between types in this category. The set of these transformations are called Endo-Functors (take us from the category of Scala and drop us back in the category of Scala). Functors have to have a polymorphic (which actually has a whole different (extra) definition in category theory) map function, that will take some object A, through some type constructor turn it into object B.

implicit val option: Functor[Option] = new Functor[Option] {
    override def map[A,B](optA: Option[A])(f: (A) => B): Option[B] = optA match{
        case Some(a) => Some(f(a))
        case _ => None
    }
}


implicit val seq: Functor[Seq[_]] = new Functor[Seq[_]] {
  override def map[A,B](sA: Seq[A])(f: (A) => B): Seq[B] = sA match{
     case a :: tail => Seq(f(a), map(tail)(f))
     case Nil => Nil
  }
}

As you can see in the second case, there is a little bit of both (more of a recursion than iteration but still).

Now before the internet blows up on me, I will say you cant pattern match on Seq in Scala. It works here because the default Seq is also a List. I just provided this example because it is simpler to understand. The underlying definition something along the lines of that.

Now hold on a second. If you look at these types, you see that they also have flatMap defined on them. This means they are something more special than plain Functors. They are Monads. So beyond satisfying functoriality, they obey the monadic laws.

Turns out Monad has a different kind of meaning in the core scala, more on that here: What exactly makes Option a monad in Scala?

But again very very short, this means that we are now in a category where the endofunctors from our previous category are the objects and the mappings between them are morphisms (natural transformations), this is slightly more accurate because if you think about it when you take a type and transform it, you take (carry over) all of it's internal type constructors (2-cell or internal morphisms) with it, you do not only take this sole idea of a type without it's functions.

implicit val optionMonad: Monad[Option] = new Monad[Option] {
 override def flatMap[A, B](optA: Option[A])(f: (A) => Option[B]): Option[B] =  optA match{
   case Some(a) => f(a) 
   case _ => None 
 } 

 def pure[A](a: A): Option[A] = Some(a)

   //You can define map using pure and flatmap

}

implicit val seqMonad: Monad[Seq[_]] = new Monad[Seq[_]] {
   override def flatMap[A, B](sA: Seq[A])(f: (A) => Seq[B]): Seq[B] = sA match{
      case x :: xs => f(a).append(flatMap(tail)(f))
      case Nil => Nil
   } 
   override def pure[A](a: A): Seq[A] = Seq(a) 
   //Same warning as above, also you can implement map with the above 2 funcs 
 }  

One thing you can always count on is map being having pattern match (or some if statement). Why? In order to satisfy the identity laws, we need to have some sort of a "base case", a unit object and in many cases (such as Lists) those types are gonna be what we call either a product or coproduct.

Hopefully, this did not confuse you further. I wish I could get into every detail of this but it would simply take pages, I highly recommend getting into categories to fully understand where these come from.

1
votes

Function map for Option isn't about pattern matching. The match/case used in your referred link is just one of the many ways to define the function. It could've been defined using if/else. In fact, that's how it's defined in Scala 2.13 source of class Option:

sealed abstract class Option[+A] extends IterableOnce[A] with Product with Serializable {
  self =>
    ...
    final def map[B](f: A => B): Option[B] =
      if (isEmpty) None else Some(f(this.get))
    ...
  }

If you view Option like a "collection" of either one element (Some(x)) or no elements (None), it might be easier to see the resemblance of how map transforms an Option versus, say, a List:

val f: Int => Int = _ + 1

List(42).map(f)
// res1: List[Int] = List(43)

List.empty[Int].map(f)
// res2: List[Int] = List()

Some(42).map(f)
// res3: Option[Int] = Some(43)

None.map(f)
// res4: Option[Int] = None