All Downloads are FREE. Search and download functionalities are using the official Maven repository.

crystal.viewF.scala Maven / Gradle / Ivy

The newest version!
// Copyright (c) 2016-2023 Association of Universities for Research in Astronomy, Inc. (AURA)
// For license information see LICENSE or https://opensource.org/licenses/BSD-3-Clause

package crystal

import cats.Id
import cats.Monad
import cats.Monoid
import cats.effect.*
import cats.syntax.all.*
import monocle.Iso
import monocle.Lens
import monocle.Optional
import monocle.Prism
import monocle.Traversal

import scala.annotation.targetName

sealed abstract class ViewOps[F[_]: Monad, G[_], A] {
  val get: G[A]

  val setCB: (A, (G[A], G[A]) => F[Unit]) => F[Unit] = (a, cb) => modCB(_ => a, cb)

  def setCB(v: A, cb: G[A] => F[Unit]): F[Unit] = setCB(v, (_, a) => cb(a))

  val set: A => F[Unit] = a => mod(_ => a)

  val modCB: (A => A, (G[A], G[A]) => F[Unit]) => F[Unit]

  def modCB(f: A => A, cb: G[A] => F[Unit]): F[Unit] = modCB(f, (_, a) => cb(a))

  val mod: (A => A) => F[Unit] = f => modCB(f, (_, _) => Monad[F].unit)

  def modAndGet(f: A => A)(using Async[F]): F[G[A]]
}

// The difference between a View and a StateSnapshot is that the modifier doesn't act on the current value,
// but passes the modifier function to an external source of truth. Since we are defining no getter
// from such source of truth, a View is defined in terms of a modifier function instead of a setter.
// Also, Views remember their previous values.
class ViewF[F[_]: Monad, A](val get: A, val modCB: (A => A, (A, A) => F[Unit]) => F[Unit])
    extends ViewOps[F, Id, A] { self =>
  def modAndExtract[B](f: (A => (A, B)))(using F: Async[F]): F[B] =
    F.async { cb =>
      mod { (a: A) =>
        val (fa, b) = f(a)
        cb(b.asRight)
        fa
      // No need to run cb on errors, it will fail the async installation effect.
      }.as(F.unit.some)
    }

  // In a ViewF, we can derive modAndGet. In ViewOptF and ViewListF we have to pass it, since their
  // mod functions have no idea of the enclosing structure.
  def modAndGet(f: A => A)(using Async[F]): F[A] =
    modAndExtract(f.andThen(a => (a, a)))

  def zoom[B](getB: A => B)(modB: (B => B) => A => A): ViewF[F, B] =
    new ViewF(
      getB(get),
      (f: B => B, cb: (B, B) => F[Unit]) =>
        modCB(modB(f), (previous, current) => cb(getB(previous), getB(current)))
    )

  def zoomOpt[B](
    getB: A => Option[B]
  )(modB: (B => B) => A => A): ViewOptF[F, B] =
    new ViewOptF(
      getB(get),
      (f: B => B, cb: (Option[B], Option[B]) => F[Unit]) =>
        modCB(modB(f), (previous, current) => cb(getB(previous), getB(current)))
    ) {
      def modAndGet(f: B => B)(using Async[F]): F[Option[B]] =
        self.modAndGet(modB(f)).map(getB)
    }

  def zoomList[B](
    getB: A => List[B]
  )(modB: (B => B) => A => A): ViewListF[F, B] =
    new ViewListF(
      getB(get),
      (f: B => B, cb: (List[B], List[B]) => F[Unit]) =>
        modCB(modB(f), (previous, current) => cb(getB(previous), getB(current)))
    ) {
      def modAndGet(f: B => B)(using Async[F]): F[List[B]] =
        self.modAndGet(modB(f)).map(getB)
    }

  def as[B](iso: Iso[A, B]): ViewF[F, B] = zoom(iso.asLens)

  def asViewOpt: ViewOptF[F, A] = zoom(Iso.id[A].asOptional)

  def asViewList: ViewListF[F, A] = zoom(Iso.id[A].asTraversal)

  def zoom[B](lens: Lens[A, B]): ViewF[F, B] =
    zoom(lens.get)(lens.modify)

  def zoom[B](optional: Optional[A, B]): ViewOptF[F, B] =
    zoomOpt(optional.getOption)(optional.modify)

  def zoom[B](prism: Prism[A, B]): ViewOptF[F, B] =
    zoomOpt(prism.getOption)(prism.modify)

  def zoom[B](traversal: Traversal[A, B]): ViewListF[F, B] =
    zoomList(traversal.getAll)(traversal.modify)

  def withOnMod(f: (A, A) => F[Unit]): ViewF[F, A] =
    new ViewF[F, A](
      get,
      (modF, cb) =>
        modCB(modF, (previous, current) => f(previous, current) >> cb(previous, current))
    )

  def withOnMod(f: A => F[Unit]): ViewF[F, A] =
    withOnMod((_, current) => f(current))

  def widen[B >: A]: ViewF[F, B] = self.asInstanceOf[ViewF[F, B]]

  def unsafeNarrow[B <: A]: ViewF[F, B] =
    zoom(_.asInstanceOf[B])(modB => a => modB(a.asInstanceOf[B]))

  def to[F1[_]: Monad](toF1: F[Unit] => F1[Unit], fromF1: F1[Unit] => F[Unit]): ViewF[F1, A] =
    ViewF(get, (mod, cb) => toF1(modCB(mod, (previous, current) => fromF1(cb(previous, current)))))

  def mapValue[B, C](f: ViewF[F, B] => C)(using ev: A =:= Option[B]): Option[C] =
    // _.get is safe here since it's only being called when the value is defined.
    // The zoom getter function is stored to use in callbacks, so we have to pass _.get
    // instead of capturing the value here. Otherwise, callbacks see a stale value.
    get.map(_ => f(zoom(_.get)(f => a1 => ev.flip(a1.map(f)))))

  def toOptionView[B](using ev: A =:= Option[B]): Option[ViewF[F, B]] =
    mapValue[B, ViewF[F, B]](identity)

  @targetName("mapValuePot")
  def mapValue[B, C](f: ViewF[F, B] => C)(using ev: A =:= Pot[B]): Pot[C] =
    // _.get is safe here since it's only being called when the value is defined.
    // The zoom getter function is stored to use in callbacks, so we have to pass _.get
    // instead of capturing the value here. Otherwise, callbacks see a stale value.
    get.map(_ => f(zoom(_.toOption.get)(f => a1 => ev.flip(a1.map(f)))))

  def toPotView[B](using ev: A =:= Pot[B]): Pot[ViewF[F, B]] =
    mapValue[B, ViewF[F, B]](identity)

  @targetName("mapValuePotOption")
  def mapValue[B, C](f: ViewF[F, B] => C)(using ev: A =:= PotOption[B]): PotOption[C] =
    // _.get is safe here since it's only being called when the value is defined.
    // The zoom getter function is stored to use in callbacks, so we have to pass _.get
    // instead of capturing the value here. Otherwise, callbacks see a stale value.
    get.map(_ => f(zoom(_.toOption.get)(f => a1 => ev.flip(a1.map(f)))))

  def toPotOptionView[B](using ev: A =:= PotOption[B]): PotOption[ViewF[F, B]] =
    mapValue[B, ViewF[F, B]](identity)

  def when(cond: A => Boolean): Boolean = cond(get)

  def unless(cond: A => Boolean): Boolean = !when(cond)

  override def toString(): String = s"ViewF($get, )"
}

object ViewF {
  def apply[F[_]: Monad, A](
    value: A,
    modCB: (A => A, (A, A) => F[Unit]) => F[Unit]
  ): ViewF[F, A] =
    new ViewF(value, modCB)
}

abstract class ViewOptF[F[_]: Monad, A](
  val get:   Option[A],
  val modCB: (A => A, (Option[A], Option[A]) => F[Unit]) => F[Unit]
) extends ViewOps[F, Option, A] { self =>
  def as[B](iso: Iso[A, B]): ViewOptF[F, B] = zoom(iso.asLens)

  def exists(cond: A => Boolean): Boolean = get.exists(cond)

  def forall(cond: A => Boolean): Boolean = get.forall(cond)

  def contains(cond: A): Boolean = get.contains(cond)

  def asViewList: ViewListF[F, A] = zoom(Iso.id[A].asTraversal)

  def asView(using Monoid[F[Unit]]): Option[ViewF[F, A]] = mapValue(identity)

  def zoom[B](getB: A => B)(modB: (B => B) => A => A): ViewOptF[F, B] =
    new ViewOptF(
      get.map(getB),
      (f: B => B, cb: (Option[B], Option[B]) => F[Unit]) =>
        modCB(modB(f), (previous, current) => cb(previous.map(getB), current.map(getB)))
    ) {
      def modAndGet(f: B => B)(using Async[F]): F[Option[B]] =
        self.modAndGet(modB(f)).map(_.map(getB))
    }

  def zoomOpt[B](
    getB: A => Option[B]
  )(modB: (B => B) => A => A): ViewOptF[F, B] =
    new ViewOptF(
      get.flatMap(getB),
      (f: B => B, cb: (Option[B], Option[B]) => F[Unit]) =>
        modCB(
          modB(f),
          (previous, current) => cb(previous.flatMap(getB), current.flatMap(getB))
        )
    ) {
      def modAndGet(f: B => B)(using Async[F]): F[Option[B]] =
        self.modAndGet(modB(f)).map(_.flatMap(getB))
    }

  def zoomList[B](
    getB: A => List[B]
  )(modB: (B => B) => A => A): ViewListF[F, B] =
    new ViewListF(
      get.map(getB).orEmpty,
      (f: B => B, cb: (List[B], List[B]) => F[Unit]) =>
        modCB(
          modB(f),
          (previous, current) => cb(previous.toList.flatMap(getB), current.toList.flatMap(getB))
        )
    ) {
      def modAndGet(f: B => B)(using Async[F]): F[List[B]] =
        self.modAndGet(modB(f)).map(_.map(getB).orEmpty)
    }

  def zoom[B](lens: Lens[A, B]): ViewOptF[F, B] =
    zoom(lens.get)(lens.modify)

  def zoom[B](optional: Optional[A, B]): ViewOptF[F, B] =
    zoomOpt(optional.getOption)(optional.modify)

  def zoom[B](prism: Prism[A, B]): ViewOptF[F, B] =
    zoomOpt(prism.getOption)(prism.modify)

  def zoom[B](
    traversal: Traversal[A, B]
  ): ViewListF[F, B] =
    zoomList(traversal.getAll)(traversal.modify)

  def withOnMod(f: (Option[A], Option[A]) => F[Unit]): ViewOptF[F, A] =
    new ViewOptF[F, A](
      get,
      (modF, cb) =>
        modCB(modF, (previous, current) => f(previous, current) >> cb(previous, current))
    ) {
      def modAndGet(f: A => A)(using Async[F]): F[Option[A]] =
        self.modAndGet(f)
    }

  def withOnMod(f: Option[A] => F[Unit]): ViewOptF[F, A] =
    withOnMod((_, current) => f(current))

  def widen[B >: A]: ViewOptF[F, B] = self.asInstanceOf[ViewOptF[F, B]]

  def unsafeNarrow[B <: A]: ViewOptF[F, B] =
    zoom(_.asInstanceOf[B])(modB => a => modB(a.asInstanceOf[B]))

  def mapValue[B](f: ViewF[F, A] => B)(using Monoid[F[Unit]]): Option[B] =
    get.map(a =>
      f(
        ViewF[F, A](
          a,
          (mod, cb) =>
            modCB(
              mod,
              (previous, current) => (previous, current).tupled.foldMap((p, c) => cb(p, c))
            )
        )
      )
    )

  def toOptionView(using Monoid[F[Unit]]): Option[ViewF[F, A]] =
    mapValue(identity)

  override def toString(): String = s"ViewOptF($get, )"
}

abstract class ViewListF[F[_]: Monad, A](
  val get:   List[A],
  val modCB: (A => A, (List[A], List[A]) => F[Unit]) => F[Unit]
) extends ViewOps[F, List, A] { self =>
  def as[B](iso: Iso[A, B]): ViewListF[F, B] = zoom(iso.asLens)

  def exists(cond: A => Boolean): Boolean = get.exists(cond)

  def forall(cond: A => Boolean): Boolean = get.forall(cond)

  def zoom[B](getB: A => B)(modB: (B => B) => A => A): ViewListF[F, B] =
    new ViewListF(
      get.map(getB),
      (f: B => B, cb: (List[B], List[B]) => F[Unit]) =>
        modCB(modB(f), (previous, current) => cb(previous.map(getB), current.map(getB)))
    ) {
      def modAndGet(f: B => B)(using Async[F]): F[List[B]] =
        self.modAndGet(modB(f)).map(_.map(getB))
    }

  def zoomOpt[B](
    getB: A => Option[B]
  )(modB: (B => B) => A => A): ViewListF[F, B] =
    new ViewListF(
      get.flatMap(getB),
      (f: B => B, cb: (List[B], List[B]) => F[Unit]) =>
        modCB(
          modB(f),
          (previous, current) => cb(previous.flatMap(getB), current.flatMap(getB))
        )
    ) {
      def modAndGet(f: B => B)(using Async[F]): F[List[B]] =
        self.modAndGet(modB(f)).map(_.flatMap(getB))
    }

  def zoomList[B](
    getB: A => List[B]
  )(modB: (B => B) => A => A): ViewListF[F, B] =
    new ViewListF(
      get.flatMap(getB),
      (f: B => B, cb: (List[B], List[B]) => F[Unit]) =>
        modCB(
          modB(f),
          (previous, current) => cb(previous.flatMap(getB), current.flatMap(getB))
        )
    ) {
      def modAndGet(f: B => B)(using Async[F]): F[List[B]] =
        self.modAndGet(modB(f)).map(_.flatMap(getB))
    }

  def zoom[B](lens: Lens[A, B]): ViewListF[F, B] =
    zoom(lens.get)(lens.modify)

  def zoom[B](optional: Optional[A, B]): ViewListF[F, B] =
    zoomOpt(optional.getOption)(optional.modify)

  def zoom[B](prism: Prism[A, B]): ViewListF[F, B] =
    zoomOpt(prism.getOption)(prism.modify)

  def zoom[B](
    traversal: Traversal[A, B]
  ): ViewListF[F, B] =
    zoomList(traversal.getAll)(traversal.modify)

  def withOnMod(f: (List[A], List[A]) => F[Unit]): ViewListF[F, A] =
    new ViewListF[F, A](
      get,
      (modF, cb) =>
        modCB(modF, (previous, current) => f(previous, current) >> cb(previous, current))
    ) {
      def modAndGet(f: A => A)(using Async[F]): F[List[A]] =
        self.modAndGet(f)
    }

  def withOnMod(f: List[A] => F[Unit]): ViewListF[F, A] =
    withOnMod((_, current) => f(current))

  def widen[B >: A]: ViewListF[F, B] = self.asInstanceOf[ViewListF[F, B]]

  override def toString(): String = s"ViewListF($get, )"
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy