typelevel / cats

Lightweight, modular, and extensible library for functional programming.
https://typelevel.org/cats/
Other
5.24k stars 1.21k forks source link

Consider adding an InvariantMonad #2160

Open LukaJCB opened 6 years ago

LukaJCB commented 6 years ago

I came across the need for one recently and I think they can be fairly useful sometimes. I've sketched out something based on @mpilquist's article


import cats._
import cats.implicits._

trait InvariantFlatMap[F[_]] extends InvariantSemigroupal[F] {

  def iflatMap[A, B](fa: F[A])(f: A => F[B])(g: B => A): F[B]

  override def product[A, B](fa: F[A], fb: F[B]): F[(A, B)] =
    iflatMap[A, (A, B)](fa)(a => imap[B, (A, B)](fb)(b => (a, b))(ab => ab._2))(ab => ab._1)
}

trait InvariantMonad[F[_]] extends InvariantMonoidal[F] with InvariantFlatMap[F] {
  def flatten[A](ffa: F[F[A]]): F[A] = iflatMap(ffa)(identity)(a => point(a))

  override def imap[A, B](fa: F[A])(f: A => B)(g: B => A): F[B] =
    iflatMap(fa)(a => point(f(a)))(g)
}

I think all of our current instances of InvariantMonoidal can also be InvariantMonads. I think it comes up enough to be useful in cats-core, but one could also envision this belonging into something like the proposed cats-more.

johnynek commented 6 years ago

can you give some examples of the type class? (i.e. some instances).

LukaJCB commented 6 years ago

Oh sorry, I forgot to link the actual article: https://mpilquist.github.io/blog/2015/06/18/invariant-shadows/

onzo-vlamp commented 6 years ago

Hi, just to add a concrete use case I found:

import cats.implicits._
import cats.{Applicative, Monad}

import scala.concurrent.{ExecutionContext, Future}

trait Repository[F[_], T, K] {
  implicitly[Applicative[Option]]
  def transform[T2](mapF: T => F[T2], contraMapF: T2 => T)(implicit F: Monad[F], ec: ExecutionContext): Repository[F, T2, K] = {
    val self = this
    new Repository[F, T2, K] {
      override def store(record: T2): Future[F[Unit]] = self.store(contraMapF(record))
      override def queryKey(key: K): Future[F[Option[T2]]] =
        self.queryKey(key).map(_.flatMap(_.traverse(mapF)))
    }
  }
  def store(record: T): Future[F[Unit]]
  def queryKey(key: K): Future[F[Option[T]]]
}

We often have a more raw representation of a record for storing that is then transformed into a friendlier representation after some validation. But going back from the friendlier format to the raw cannot fail. Also found similar needs for encoder/decoder representations.

Hope it helps drive this 🙂

Jacoby6000 commented 6 years ago

Is there a version of this which uses

def iflatMap[A, B](fa: F[A])(f: A => F[B])(g: B => F[A]): F[B]

?

I would think that iflatMap would adjust both f and g equally, to return an F.