Confusion in how functions are composed and called

I have excercises from book “functional programming in scala” where we define custom Option object with some functions. I cant understand how flatMap and orElse work. First of all map take function A => B and return Option[B] but in flatMap map take f: A => Option[B] why?
I dont understand what in flatMap: “map(f) getOrElse None” means why not map f getOrElse None for example and in orElse: “this map (Some(_)) getOrElse ob” why we put “this” in that place and nowhere else, why map is not called with map(sth) etc

trait Option[+A] {
  def map[B](f: A => B): Option[B] =
    this match {
      case None => None
      case Some(v) => Some(f(v))
    }

  def flatMap[B](f: A => Option[B]): Option[B] =
    map(f) getOrElse None

  def getOrElse[B >: A](default: => B): B =
    this match {
      case None => default
      case Some(v) => v
    }

  def orElse[B>:A](ob: => Option[B]): Option[B] =
    this map (Some(_)) getOrElse ob
}
case class Some[+A](get: A) extends Option[A]
case object None extends Option[Nothing]

To answer the syntax questions first:

Scala does in some cases allow a few different notations for calling methods. Furthermore, in many cases, specifying this is optional.
If we look at the flatMap method, you’d call it as someOption.flatMap(someFunc). In the method body, someOption will be referred to as this. To make the code less verbose, a method call like the one to map, or a variable name, will refer to the method or field on this, if there isn’t a local variable or method with the same name:

//in flatMap, these would be equivalent:
this.map(f)
map(f)

As the name map is unambiguous in orElse, the this can be left off, the book’s code style is a bit inconsistent here.

The second feature playing into this here is the posibility to write methods like an infix operator: if you have some code of the form obj.method(param), you can also write this as obj method param. This will only work as expected with methods taking one parameter, and the object must be specified (which is why you can’t write map f getOrElse None, here a this would be needed for infix notation).

Generally, the use of infix notation is only recommended for symbolic operators and maybe higher order functions (see Infix Notation docs). I generally don’t use it for non-symbolic functions and would recommend to write the flatMap method as map(f).getOrElse(None), as it makes the evaluation order clearer in my opinion.

Now for the semantic question:

The important thing to note here is that the type B is a type parameter on the methods, so it can be different for two method calls, and different for the map call from the B of the flatMap call. It is a local type variable.

So when you call flatMap with a function f: A => Option[B], the B for flatMap is the type inside the returned option. But when you then pass on f to map, its parameter B is separately inferred. To make things a bit clearer, here is an implementation with different variable names per method (it is fully equivalent to the solution from the book):

  def map[C](f: A => C): Option[C] =
    this match {
      case None => None
      case Some(v) => Some(f(v))
    }

  def flatMap[B](f: A => Option[B]): Option[B] =
    map(f) getOrElse None

Now in flatMap, we pass f: A => Option[B] to map, which expects a function of type A => C. This is not a problem, if we set C = Option[B]: map will handle the Option[B] like any other value and wrap it into a Some. So the result will be a Option[Option[B]].

As flatMap should not return a nested Option, getOrElse is used to remove the nesting. If this was already None, it returns the given default, which is also None. If it was a Some[A], it will now be a Some[Some[B]] and getOrElse will return the inner Some.

2 Likes

The book is using a common implementation of flatMap as map + flatten.

However, I also find such implementation (as well as the syntax used by the book) to be confusing.

I would do this:

sealed trait Option[+A] {
  def map[B](f: A => B): Option[B] = this match {
    case None => None
    case Some(a) => Some(f(a))
  }

  def flatMap[B](f: A => Option[B]): Option[B] = this match {
    case None => None
    case Some(a) => f(a)
  }

  def getOrElse[B >: A](default: => B): B = this match {
    case None => default
    case Some(v) => v
  }

  def orElse[B >: A](that: => Option[B]): Option[B] = this match {
    case None => that
    case someA => someA
  }
}

final case class Some[+A](get: A) extends Option[A]
final case object None extends Option[Nothing]
2 Likes