domata-core_3.0.12.4.source-code.Edomaton.scala Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of edomata-core_3 Show documentation
Show all versions of edomata-core_3 Show documentation
Purely functional event-driven automata
The newest version!
/*
* Copyright 2021 Hossein Naderi
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package edomata.core
import cats.*
import cats.arrow.FunctionK
import cats.data.*
import cats.implicits.*
import cats.kernel.Eq
import Edomaton.*
/** Represents programs that are event driven state machines (a Mealy machine)
*
* these programs can use input to decide on a state transition, and optionally
* emit a sequence of notifications for communication.
*
* @tparam F
* effect type
* @tparam Env
* input type
* @tparam R
* rejection type
* @tparam E
* internal event type
* @tparam N
* notification type, a.k.a external event, integration event
* @tparam A
* output type
*/
final case class Edomaton[F[_], -Env, R, E, N, A](
run: Env => F[ResponseD[R, E, N, A]]
) extends AnyVal {
/** maps output result */
def map[B](f: A => B)(using Functor[F]): Edomaton[F, Env, R, E, N, B] =
transform(_.map(f))
/** creates a new edomaton that translates some input to what this edomaton
* can understand
*/
def contramap[Env2](f: Env2 => Env): Edomaton[F, Env2, R, E, N, A] =
Edomaton(
run.compose(f)
)
/** binds another edomaton to this one. */
def flatMap[Env2 <: Env, B](
f: A => Edomaton[F, Env2, R, E, N, B]
)(using Monad[F]): Edomaton[F, Env2, R, E, N, B] =
Edomaton(env =>
run(env).flatMap { r =>
r.result.visit(
err => r.copy(result = Decision.Rejected(err)).pure[F],
f(_)
.run(env)
.map(o =>
o.result.visit(
_ => o,
_ => r >> o
)
)
)
}
)
/** alias for flatMap */
inline def >>=[Env2 <: Env, B](
f: A => Edomaton[F, Env2, R, E, N, B]
)(using Monad[F]): Edomaton[F, Env2, R, E, N, B] = flatMap(f)
/** alias for andThen, flatMap(_ => f) */
inline def >>[Env2 <: Env, B](
f: => Edomaton[F, Env2, R, E, N, B]
)(using Monad[F]): Edomaton[F, Env2, R, E, N, B] = andThen(f)
/** sequences another edomaton without considering output for this one
*
* alias for flatMap(_ => f)
*/
inline def andThen[Env2 <: Env, B](
f: => Edomaton[F, Env2, R, E, N, B]
)(using Monad[F]): Edomaton[F, Env2, R, E, N, B] = flatMap(_ => f)
/** transforms underlying response
*/
def transform[R2, E2, N2, B](
f: ResponseD[R, E, N, A] => ResponseD[R2, E2, N2, B]
)(using
Functor[F]
): Edomaton[F, Env, R2, E2, N2, B] =
Edomaton(run.andThen(_.map(f)))
/** translates this program in another language mapped by a natural
* transformation
*/
def mapK[G[_]](fk: FunctionK[F, G]): Edomaton[G, Env, R, E, N, A] =
Edomaton(run.andThen(fk.apply))
/** evaluates an effect using output of this edomaton and uses result of
* evaluation as new output
*/
def evalMap[B](f: A => F[B])(using
Monad[F]
): Edomaton[F, Env, R, E, N, B] =
flatMap(a => liftF(f(a).map(ResponseD.pure)))
/** like evalMap but ignores evaluation result
*/
def evalTap[B](f: A => F[B])(using
Monad[F]
): Edomaton[F, Env, R, E, N, A] =
flatMap(a => liftF(f(a).map(ResponseD.pure)).as(a))
/** evaluates an effect and uses its result as new output */
def eval[B](f: => F[B])(using Monad[F]): Edomaton[F, Env, R, E, N, A] =
evalTap(_ => f)
/** alias for map(_=> b) */
inline def as[B](b: B)(using Functor[F]): Edomaton[F, Env, R, E, N, B] =
map(_ => b)
/** Ignores output value */
def void(using Functor[F]): Edomaton[F, Env, R, E, N, Unit] = as(())
/** Decides based on output */
def decide[B](f: A => Decision[R, E, B])(using
Monad[F]
): Edomaton[F, Env, R, E, N, B] =
flatMap(a => Edomaton.decide(f(a)))
/** Clears all notifications so far */
def reset(using Functor[F]): Edomaton[F, Env, R, E, N, A] =
transform(_.reset)
/** Adds notification without considering decision state */
def publish(ns: N*)(using Functor[F]): Edomaton[F, Env, R, E, N, A] =
transform(_.publish(ns: _*))
/** If this edomaton is rejected, uses given function to decide what to
* publish
*/
def publishOnRejectionWith(
f: NonEmptyChain[R] => Seq[N]
)(using Functor[F]): Edomaton[F, Env, R, E, N, A] = transform(
_.publishOnRejectionWith(f)
)
/** publishes these notifications if this edomaton is rejected */
def publishOnRejection(ns: N*)(using
Functor[F]
): Edomaton[F, Env, R, E, N, A] =
transform(_.publishOnRejection(ns: _*))
}
object Edomaton extends EdomatonInstances, EdomatonConstructors
sealed transparent trait EdomatonInstances {
given [F[_]: Monad, Env, R, E, N]
: Monad[[t] =>> Edomaton[F, Env, R, E, N, t]] =
new Monad {
type G[T] = Edomaton[F, Env, R, E, N, T]
override def pure[A](x: A): G[A] =
Edomaton(_ => ResponseD.pure(x).pure[F])
override def map[A, B](fa: G[A])(f: A => B): G[B] = fa.map(f)
override def flatMap[A, B](fa: G[A])(f: A => G[B]): G[B] = fa.flatMap(f)
override def tailRecM[A, B](a: A)(
f: A => G[Either[A, B]]
): G[B] = Edomaton(env =>
Monad[F].tailRecM(ResponseD.pure(a): ResponseD[R, E, N, A])(rma =>
rma.result.visit(
_ => ???, // This cannot happen
a =>
f(a)
.run(env)
.map(rma >> _)
.map(o =>
o.result
.visit(
rej => o.copy(result = Decision.Rejected(rej)).asRight,
{
case Left(a) => o.as(a).asLeft
case Right(b) => o.as(b).asRight
}
)
)
)
)
)
}
given [F[_], Env, R, E, N, T](using
Eq[Env => F[ResponseD[R, E, N, T]]]
): Eq[Edomaton[F, Env, R, E, N, T]] =
Eq.by(_.run)
given [F[_], R, E, N, T]
: Contravariant[[env] =>> Edomaton[F, env, R, E, N, T]] =
new Contravariant {
override def contramap[A, B](fa: Edomaton[F, A, R, E, N, T])(
f: B => A
): Edomaton[F, B, R, E, N, T] = fa.contramap(f)
}
}
sealed transparent trait EdomatonConstructors {
/** constructs an edomaton that outputs a pure value */
def pure[F[_]: Applicative, Env, R, E, N, T](
t: T
): Edomaton[F, Env, R, E, N, T] =
Edomaton(_ => ResponseD.pure(t).pure)
/** an edomaton with trivial output */
def unit[F[_]: Applicative, Env, R, E, N, T]
: Edomaton[F, Env, R, E, N, Unit] =
pure(())
/** constructs an edomaton from an effect that results in a response */
def liftF[F[_], Env, R, E, N, T](
f: F[ResponseD[R, E, N, T]]
): Edomaton[F, Env, R, E, N, T] = Edomaton(_ => f)
/** constructs an edomaton with given response */
def lift[F[_]: Applicative, Env, R, E, N, T](
f: ResponseD[R, E, N, T]
): Edomaton[F, Env, R, E, N, T] = liftF(f.pure)
/** constructs an edomaton that evaluates an effect */
def eval[F[_]: Applicative, Env, R, E, N, T](
f: F[T]
): Edomaton[F, Env, R, E, N, T] = liftF(f.map(ResponseD.pure))
/** constructs an edomaton that runs an effect using its input */
def run[F[_]: Applicative, Env, R, E, N, T](
f: Env => F[T]
): Edomaton[F, Env, R, E, N, T] = Edomaton(
f.andThen(_.map(ResponseD.pure))
)
/** constructs an edomaton that outputs what's read */
def read[F[_]: Applicative, Env, R, E, N, T]: Edomaton[F, Env, R, E, N, Env] =
run(_.pure[F])
/** constructs an edomaton that publishes given notifications */
def publish[F[_]: Applicative, Env, R, E, N](
ns: N*
): Edomaton[F, Env, R, E, N, Unit] = lift(ResponseD.publish(ns: _*))
/** constructs an edomaton that rejects with given rejections */
def reject[F[_]: Applicative, Env, R, E, N, T](
r: R,
rs: R*
): Edomaton[F, Env, R, E, N, T] = lift(ResponseD.reject(r, rs: _*))
/** constructs an edomaton that decides the given decision */
def decide[F[_]: Applicative, Env, R, E, N, T](
d: Decision[R, E, T]
): Edomaton[F, Env, R, E, N, T] = lift(ResponseD(d))
def validate[F[_]: Applicative, Env, R, E, N, T](
v: ValidatedNec[R, T]
): Edomaton[F, Env, R, E, N, T] = lift(ResponseD.validate(v))
/** Constructs a program from an optional value, that outputs value if exists
* or rejects otherwise
*
* You can also use .toDecision syntax for more convenience
*/
def fromOption[F[_]: Applicative, Env, R, E, N, T](
opt: Option[T],
orElse: R,
other: R*
): Edomaton[F, Env, R, E, N, T] = opt.fold(reject(orElse, other: _*))(pure(_))
/** Constructs a program that either outputs a value or rejects
*/
def fromEither[F[_]: Applicative, Env, R, E, N, T](
eit: Either[R, T]
): Edomaton[F, Env, R, E, N, T] = eit.fold(reject(_), pure(_))
/** Constructs a program that either outputs a value or rejects
*
* You can also use .toDecision syntax for more convenience.
*/
def fromEitherNec[F[_]: Applicative, Env, R, E, N, T](
eit: EitherNec[R, T]
): Edomaton[F, Env, R, E, N, T] =
eit.fold(errs => lift(ResponseD(Decision.Rejected(errs))), pure(_))
}