1

I was going through "Scala with Cats" by Underscore and stumbled upon the following statement:

Cats provides a cats.syntax.apply module that makes use of Semigroupal and Functor to allow users to sequence functions with multiple arguments.

As far as I understand we can only sequence functions with single arguments because functions can return only single value.

  import cats.implicits._

  val f1: Int => Int = x => x + 1
  val f2: Int => Int = x => x * 4
  val f3: Int => String = x => s"x = $x"

  f1.map(f2).map(f3)(5)

For the following example, it says: Internally mapN uses the Semigroupal to extract the values from the Option and the Functor to apply the values to the function.

final case class Cat(name: String, born: Int, color: String)

(
  Option("Garfield"),
  Option(1978),
  Option("Orange & black")
).mapN(Cat.apply)

// res10: Option[Cat] = Some(Cat("Garfield", 1978, "Orange & black"))

Here Cat.apply is indeed a function with multiple arguments but it is chained to a function that itself returns a tuple of options i.e., a single value. We could probably make it accept multiple arguments like so:

  final case class Cat(name: String, born: Int, color: String)

  val f: (String, Int, String) => (Option[String], Option[Int], Option[String]) =
    (name, year, color) => (Option(name), Option(year), Option(color))

  f("Garfield", 1978, "Orange & black").mapN(Cat.apply)

Now we have functions f and Cat.apply that accept multiple arguments and are chained together. Is it what the above statement was pointing to? But then I can't seem to find a way to chain more functions further. Is the statement applicable to chaining multiple argument functions only at one level? Also, notice here that function f is applied eagerly in contrast to the single argument function chaining example depicted above. Is is possible to apply functions lazily here?

There isn't much explanation I could find anywhere on Semigroupal on internet. Could anyone please explain this statement with an example? TIA.

iamsmkr
  • 800
  • 2
  • 10
  • 29

1 Answers1

2

But then I can't seem to find a way to chain more functions further.

I think we can because we can continue mapping over the context for example

f("Garfield", 1978, "Orange & black")
  .mapN(Cat.apply)
  .map(_.name) // here is another step in the chain of operations

mapN is product + map behind the scenes; we can reveal the constituent map like so

f("Garfield", 1978, "Orange & black")
  .tupled
  .map { case (a, b, c) => Cat.apply(a, b, c) }
  .map { _.name }

where tupled extension method eventually calls Semigroupal#product.

When they say

...allow users to sequence functions with multiple arguments.

my interpretation is not that we keep chaining with just mapN, but instead we can continue chaining over the context in the general sense of the functor, and if at some point in the chain, usually the beginning, we have multiple values within multiple contexts of the same type, then semigroupal + functor allows us to join the values within the single context and continue chaining.

Also, notice here that function f is applied eagerly in contrast to the single argument function chaining example depicted above. Is is possible to apply functions lazily here?

That is kind of the main selling point of Semigroupal over the Monad, that is, to be able to "eagerly" execute independent operations, join the resulting values within the context and then continue chaining. With monadic chaining even if operations are independent one would still have to wait for the other before continuing the chain.

Mario Galic
  • 47,285
  • 6
  • 56
  • 98
  • I have a follow up question if you don't mind: https://stackoverflow.com/questions/67945436/why-semigroupal-for-types-that-have-monad-instances-dont-combine – iamsmkr Jun 12 '21 at 03:45