mapN methods for common Apply types

So I added these as extension methods, but shouldn’t they come with the Standard Library out of the box?

  val op1: Option[Int] = Some(5)
  val op2: Option[String] = Some("Wow")
  val op3: Option[Double] = Some(3.2)
  val op4: Option[Int] = None
  test("Option companion object")
  { Option.map2(op1, op2)(_ + "! " + _) ==> Some("5! Wow")
    Option.map3(op1, op2, op3)((s1, s2, s3) => s1 + s2.length + s3) ==> Some(11.2)
    Option.map4(op1, op2, op3, op4)((s1, s2, s3, s4) => s1 + s2.length + s3 + s4) ==> None
  }

People use for comprehensions for these.

for
  s1 <- op1
  s2 <- op2
  s3 <- op3
yield s1 + s2.length + s3

I mean, this brings us to the long-standing discussion of whether we should have an Applicative equivalent to for’s Monadic approach. In principle, that might be lovely, but we’ve been down that road fruitlessly several times.

(And of course, nowadays folks tend to get mapN methods from Cats, so the pressure to have it in the stdlib is low.)

I thought part of the motivation for ZIO.Prelude is that calling such methods from Cats Core is not intuitive enough.

I think that’s a subjective difference of opinion…

In my two cents, it should be defined in the tuple, not in the Option.
Actually, we can use something as following just now:


> scala-cli
Welcome to Scala 3.4.0 (17, Java OpenJDK 64-Bit Server VM).
Type in expressions for evaluation. Or try :help.

scala>   val op1: Option[Int] = Some(5)
scala>   val op2: Option[String] = Some("Wow")
scala>   val op3: Option[Double] = Some(3.2)

scala> (op1, op2).zipped.map(_ + "! " + _)
val res0: Iterable[String] = List(5! Wow)

scala> (op1, op2, op3).zipped.map(_ + _.size + _)
val res1: Iterable[Double] = List(11.2)

We only need to simplify the zipped.map part to zippedApply or something, and make the return type more precise. I think it is not too hard to implement for all TupleN with the generic programming support for tuple.

2 Likes

Not sure if this directly answers your question, but in Scala 3 you can easily define a generic mapN method:

extension [Tup <: Tuple](tup: Tup)(using Tuple.Union[Tup] <:< Option[?])
  def mapN[A](f: Tuple.InverseMap[Tup, Option] => A): Option[A] =
    val xs = tup.toList.asInstanceOf[List[Option[Any]]]
    Option.when(xs.forall(_.isDefined)):
      val vs = xs.map(_.get)
      val vtup = vs.foldRight[Tuple](EmptyTuple)(_ *: _).asInstanceOf[Tuple.InverseMap[Tup, Option]]
      f(vtup)

@main def main = 

  val op1: Option[Int] = Some(5)
  val op2: Option[String] = Some("Wow")
  val op3: Option[Double] = Some(3.2)
  val op4: Option[Int] = None

  (op1, op2).mapN(_ + "! " + _)
  
  (op1, op2, op3).mapN((s1, s2, s3) => s1 + s2.length + s3)

  (op1, op2, op3, op4).mapN((s1, s2, s3, s4) => s1 + s2.length + s3 + s4)
  

2 Likes

I mean a generic mapN, such that
(Option[A], Option[B], Option[C]).mapN((A, B, C) => D) got a Option[D]
and
(Seq[A], Seq[B], Seq[C]).mapN((A, B, C) => D) got a Seq[D]
and so on.

Here is the current zipped.map looks like:

scala> (Seq(1, 2), Seq(2, 3), Seq(4, 5)).zipped.map(_ * _ - _)
val res0: Seq[Int] = List(-2, 1) // Here is Seq[Int]

scala> (Set(1, 2), Set(2, 3), Set(4, 5)).zipped.map(_ * _ - _)
val res1: Set[Int] = Set(-2, 1) // Here is Set[Int]

Oh for sure. But we have at least 3 major eco systems Li Haoyi, Cats, Zio. I think where we can find agreement, we should seek for as much commonality as possible and encode that in the language and standard library.

The reason I prefer Option.map3, Future.map3, MyMonadicClass.map3 over for comprehensions is because when I look at them, for me at a glance it conveys what is going on. If you are to use the power of Scala you have to become comfortable with for comprehensions, but that doesn’t mean that we shouldn’t minimise the use of for comprehensions.

I thought we were finally reaching a consensus that writing one’s whole programme in a for comprehensions is not ideal. I also think for beginners to functional programming its easier to move from for i = 0, i < length i++, to foreach, to map and from there to flatMap and map2 and map3. Option.map2 and Option.map3 just looks like classic static methods in Java.

Not exactly? That paints the dichotomy as being much starker than it really is.

In particular, Cats != Typelevel. Cats is a pretty focused library with very broad applicability, and works fine with the others. My dayjob is ZIO-based, and we use Cats routinely within it. ZIO deals with program composition; Cats deals with all these concepts from category theory like Monads and Monoids and Applicatives (oh my!). They’re complementary.

I think you’re maybe thinking of cats-effect, which is a very different library from Cats. (The relationship of names is mostly a historical artifact.) ZIO and cats-effect are doing similar things, so while it’s possible to mix them to some degree (mainly at the typeclass level), it’s less common.

No, I really don’t think that’s true. Some folks have loudly proclaimed that for comprehensions are sub-optimal and we should replace them, but many of us are quite happy with them and not particularly inspired by the alternatives presented so far.

The perception of a consensus, I believe, is just because the anti-comprehension crowd are louder than those of us who think the status quo works nicely.

But as mentioned above, I don’t think for comprehensions are the right way to slice this particular problem, so I think this is a bit of a side-track – for comprehensions are monadic, whereas what you’re trying to do here is applicative. We don’t have a formalism in the language for applicatives, the way we do for monads. Possibly we should. I don’t adore the idea of adding these methods in an ad-hoc way, but if the language supported them the way we do for flatMap I might be more interested.

3 Likes

Er, who thinks map3, map4, etc. is better? I don’t think this is actually a common view?

Either it should be general, mapN, or use some other modality entirely.

For example, in my kse3 library I have a Rust-like .? operator (using Scala 3 boundaries and macros), so there, without for comprehensions, one would write

Option.Ret:
  op1.? + op2.?.length + op3.?

This is about as compact as you can get (and also as fast as you can get). And you don’t have to worry about refactoring accidentally mucking up the order-agreement, like with map3(op1, op2, op3)((s1, s2, s3) => ...). If you are numbering them then it’s super-easy to get right, but otherwise it’s just another spot where error can slip in.

No ecosystem does this now, however. (I obviously like it enough to maintain a library that does it.)

(Can play with it here: Scastie - An interactive playground for Scala.)

4 Likes