typelevel / cats-mtl Goto Github PK
View Code? Open in Web Editor NEWcats transformer type classes.
Home Page: https://typelevel.org/cats-mtl/
License: Other
cats transformer type classes.
Home Page: https://typelevel.org/cats-mtl/
License: Other
@Zelenya created this great logo that I really think we should adopt:
A lot of the classes could use some nice syntax (e.g. a raise[F]
function enriched onto error types). Since we aren't using Simulacrum, this needs to be done… not with Simulacrum. It can be done by hand, or preferably with scalafix.
The following is a list of type classes with missing laws classes:
The ones that do not have documented laws are:
I always find it a bit surprising that e.g. evidence of MonadState
does not provide evidence of Monad
.
So if I want to write
def foo[F[_]](...)(implicit F: MonadState[F, MyState]) =
for {
a <- firstThing()
b <- secondThing(a)
} yield b
then I have to add a : Monad
context bound. But that feels redundant - it's a MonadState
, so of course it's a Monad
!
If you're using meow-mtl, this can be fixed by importing com.olegpy.meow.prelude._
, thanks to PrivateInstances.
Is there a good reason not to make that functionality available out of the box in cats-mtl?
They are using shapeless LowPriority
, which implies there might be cases where you would want your Monad[F]
evidence to be different from the val monad: Monad[F]
member of MonadState
, but I can't think of a use case where I would want that. Maybe I'm missing something.
MonadLayerControl
(and similar) are a clever mechanism for reducing the implementation boilerplate of MTL-like libraries (such as this one), but they also prevent such libraries from interacting with non-algebraic monad transformers, such as FreeT
, ContT
, and many more.
In order to avoid these issues, as well as make the library generally more maintainable and avoid some of the issues with implicit resolution, we should rewrite all of that away and use a more conventional lifting mechanism for each class. For trivial constructor algebras (such as MonadState
), we can still do auto-lifting using a mechanism like scalaz's MonadPartialOrder
, but non-algebraic effects (like handleErrorWith
or local
) need to be lifted more explicitly on a bespoke basis. This is, in fact, the greatest strength of the MTL approach: the fact that lifting those effects is tailored to the stack in question, rather than generalized in a way which causes unsoundness in certain permutations (such as the way in which Eff
works).
All of the instances should induct over ContT
nicely.
There's FunctorRaise
for raising errors. Should there also be an mtl class for catching errors?
Right now we have two instances that give you an ApplicativeAsk[F, E]
if there's a MonadState[F, E]
instance in scope, and a FunctorTell[F, L]
instance when MonadState[F, L]
is in scope.
These instances are probably useful and lawful, but it still doesn't quite feel right for me.
I kind of expect the environment parameter E
in ApplicativeAsk
to be immutable and never change, whereas if I have a StateT[F, E]
, it can change with each flatMap
.
I'm not sure if this is a problem or not, but wanted to see what others think about this. :)
MonadChronicle
is the mtl equivalent of IorT
and has some fairly nice properties if you ask me. We should consider adding support for it :)
For all users of this library that don't define their own transformers, they're likely to never see or use these type classes, so having them in the main package feels pretty unnecessary. @SystemFw suggested they go into their own lifting
package.
As many have said before, stacking multiple monad transformers can be quite inefficient, it'd be cool if we had some kind of story for supporting performant instances for different combination of mtl classes.
Oleg has already done some work in that regard with meow-mtl
and at the start of the year, I've also played around with specialized instances at cats-mtl-special.
This would serve two purposes. First, inductive instances over FreeT
. Second, we could define some nifty machinery to implement certain typeclasses via InjectK
algebrae within Free
/FreeT
, which would be awesome.
trait TFunctor[T[_[_], _]] {
def mapT[F[_], G[_], A](tfa: T[F, A]): T[G, A]
}
This unfortunately is not implementable for StateT
(F[S => F[(S, A)]]
), because you need to be able to map over one of the F[_]
levels to apply the natural transformation at the other level. Two solutions come to mind:
a) Require either the source F[_]
or the target G[_]
to have a Functor
instance, allowing the user to map over either if required (only for StateT
, afaik), by providing mapTS[F[_]: Functor, G[_], A](tfa: T[F, A]): T[G, A]
as well as mapTT[F[_], G[_]: Functor, A](tfa: T[F, A]): T[G, A]
to satisfy the maximum number of usecases. This is implemented currently.
b) Drop the TFunctor
instance for StateT
.
There are a number of type signatures which are backwards for no apparent reason other than the fact that Haskell did it that way. For example:
def local[A](f: E => E)(fa: F[A]): F[A]
This is exactly backwards from Scala's standpoint and forces a) poor call-site syntax, and b) extremely poor type inference in many cases. The two parameters should be reversed.
Hi!
The docs links mentioned in https://github.com/typelevel/cats-mtl#documentation return 404.
Are the docs available elsewhere?
Cheers!
Is anything holding this back?
For reference: typelevel/cats#2405
You know, I consider myself a reasonably advanced user of TravisCI and sbt, and it took me a zillion tries just to get a PR to build successfully. I still have no idea how to release anything. Some of those tries involved running sbt locally, having something fail, killing it, running the same command again and having it work.
I want to rip out a lot of the madness so that it's possible for third-parties to contribute without having to replicate what I did, because I'm not sure I can replicate what I did.
Especially in the wake of ApplicativeHandle
's new methods.
The following test classes need to be written:
There is some code which I am aware has no tests, most of which I'm not sure yet how to deal with.
As of July 26 2017, these account for every single coverage miss.
FunctorRaise
FunctorRaise
I'm not sure if FunctorRaise
is necessary.
zero
from MonadLayerControl
Not sure if necessary, if it is it'll be for some kind of bracketing.
Laws
and Tests
smart constructors which are not called because the calling code is always overridenReally not sure what to do about this one. I could use early initialisers but that's voodoo.
backwards
part of the local
and scope
isomorphismsThere are no MonadLayer
s that are not MonadLayerFunctor
s at the moment. Thus this generality is not currently used; ContT
would change this.
ApplicativeAsk
and FunctorTell
through layersCould be implemented with local implicit scoping.
The contents should guide users towards what to do. For example, for the Ask
classes, it should say something like this:
If you have a value of
$A
in scope, or a way of getting one, then you should wrap$F
inKleisli
(e.g.Kleisli[$F, $A, ...
) and use that type with this function. If you do not have a value of type$A
in scope, then you should add animplicit F: ApplicativeAsk[$F, $A]
parameter to your function.
We should be able to do this for all of the classes.
If we adapt the MonadLog
type class presented in IO & Logging Capabilities to fit as an extension to FunctorListen
, we end up with the following. We've replaced log
with tell
and extract
is replaced by listen
.
trait MonadLog[F[_], L] extends FunctorListen[F, L] {
val monad: Monad[F]
val monoid: Monoid[L]
def clear[A](fa: F[A]): F[A]
def flush[A](fa: F[A])(f: L => F[Unit]): F[A]
}
Personally, I don't think it's the right way to go:
Functor
hierarchy, while we require Monad
,Monoid
,F[(A, L)]
, and cannot be stored separately.Instead, I propose we adapt MonadLog
as an extension to MonadState
. In fact, any MonadState[F, L]
can be a MonadLog[F, L]
with a Monoid[L]
instance, meaning we can reuse the existing MonadState
instances. Such an adapted version of MonadLog
could be defined as follows.
trait MonadLog[F[_], L] extends MonadState[F, L] with Serializable {
val monoid: Monoid[L]
def log(l: L): F[Unit]
def clear: F[Unit]
def flush(f: L => F[Unit]): F[Unit]
}
Default implementation could be defined as follows.
trait DefaultMonadLog[F[_], L] extends DefaultMonadState[F, L] with MonadLog[F, L] {
def log(l: L): F[Unit] =
modify(monoid.combine(_, l))
def clear: F[Unit] =
set(monoid.empty)
def flush(f: L => F[Unit]): F[Unit] =
monad.flatMap(monad.flatMap(get)(f))(_ => clear)
}
Laws could be defined as follows.
trait MonadLogLaws[F[_], L] extends MonadStateLaws[F, L] {
implicit val logInstance: MonadLog[F, L]
implicit val monoid: Monoid[L] = logInstance.monoid
import logInstance._
import monad.{pure, unit}
import monoid.empty
def logThenClearDoesNothing(l: L) =
log(l) *> clear <-> unit
def logThenGetReturnsLog(l: L) =
log(l) *> get.flatMap(clear.as) <-> log(l) *> clear.as(l)
def logsThenGetReturnsCombinedLog(l1: L, l2: L) =
log(l1) *> log(l2) *> get.flatMap(clear.as) <->
log(l1) *> log(l2) *> clear.as(l1 combine l2)
def clearThenGetReturnsEmpty =
clear *> get <-> pure(empty)
def clearThenClearClearsOnce =
clear *> clear <-> clear
def flushIsGetThenClear(f: L => F[Unit]) =
flush(f) <-> get.flatMap(f) *> clear
}
The names here (e.g. FunctorTell
) are all super arbitrary. There's very little prior art here, since most other MTL systems use encodings that avoid orphans (e.g. Haskell and Scalaz 8), so the cats-mtl calculus is pretty unique. What we should ask ourselves is whether or not the names, as they are currently chosen, appropriately guide users to the meaning and use-cases.
The counterpart to TraverseFilter
in cats has to be filled in.
(note: must compose over Traverse
)
Detail:
Have a nice explanatory README.md, which gives users the gist of the library. I also plan to fold DESIGN.md and CONTRIBUTING.md into this file, for easy discovery.
Some pre-requisites for a 0.1.0 release are:
It would be nice to have a formulation for MonadBracket (#9) but it doesn't seem likely to appear in time.
I'm projecting that this will take a week and a half.
In Ask
:
def applicative: Applicative[F]
In Censor
:
val applicative: Applicative[F]
So if you define traits that extend both, like:
trait MyTrait[F[_], Env, Evt] extends Censor[F, Chain[Evt]] with Ask[F, Env]
trait MyTrait2[F[_], Env, Evt] extends Ask[F, Env] with Censor[F, Chain[Evt]]
The order becomes important. MyTrait2
compiles but MyTrait
fails with:
stable, immutable value required to override:
[error] val applicative: cats.Applicative[F] (defined in trait Censor)
[error] with def applicative: cats.Applicative[F] (defined in trait Ask)
[error] trait MyTrait[F[_], Env, Evt] extends Censor[F, Chain[Evt]] with Ask[F, Env]
How about using the same pattern to avoid such issues?
The dependency should be "test" scoped dependency
Need a way to think about bracketing in terms of monad stacks, to avoid any layers short-circuiting or using state incorrectly.
It can be convenient to allow a MonadState[F, S] to serve as an ApplicativeAsk[F, S]. We can think of the former constraint as the ability to read and update system state, and the latter as simply the ability to read it. If all we need is to read the state, the least privilege principle suggests that a ApplicativeAsk[F, S] constraint is preferable.
Traditionally, Reader monad is associated with reading outside configuration. However, IME it also works well to read the domain model of a stateful application, or some part thereof eg by the lensing techniques available in meow-mtl.
An example of value can be found in the meow-mtl readme use case
This is a proposal to consider an opt-in extra derivation that provides an ApplicativeAsk[F, S]
should a MonadState[F, S]
instance be available. "Opt-in" rather than automatic because the Ask- and State- may be unrelated, if S
-type is something common like Int
.
Explain why cats-mtl is designed the way it is, with justification.
Hi all.
Bracket typeclass, that exists in cats-effect, doesn't work well with an effect that relies on FunctorRaise.
I want to implement a clean-up logic in case of any error (raised either by MonadError or FunctorRaise).
Example:
import cats.data.EitherT
import cats.effect._
import cats.effect.syntax.bracket._
import cats.mtl._
import cats.mtl.implicits._
final case class AppError(reason: String)
class Service[F[_]: Sync: ApplicativeHandle[?[_], AppError]: FunctorRaise[?[_], AppError]] {
def createActive: F[String] =
create.bracketCase(uuid => makeActive(uuid)) {
case (_, ExitCase.Completed) => Sync[F].unit // There can be an error too
case (uuid, ExitCase.Error(e)) => destroy(uuid)
case (uuid, ExitCase.Canceled) => destroy(uuid)
}
def makeActive(uuid: String): F[String] = FunctorRaise[F, AppError].raise(AppError("Oops. Something went wrong"))
def create: F[String] = Sync[F].pure("some uuid")
def destroy(uuid: String): F[Unit] = Sync[F].unit
}
object Service {
def main(args: Array[String]): Unit = {
type Effect[A] = EitherT[IO, AppError, A]
val service = new Service[Effect]
println(service.createActive.value.unsafeRunSync()) // Left(AppError("Oops. Something went wrong"))
}
}
For sure this is the correct behavior of a Bracket typeclass and clean-up logic for a specific error can be managed inside the bracket:
create.bracketCase(uuid => makeActive(uuid).handleError[AppError](e => destroy(uuid) >> e.raise)) { ... }
But should there be an alternative Bracket that manages specific errors raised by FunctorRaise? For example, extended ADT can be used:
sealed trait ExitCase[+E1, +E2]
object ExitCase {
final case object Completed extends ExitCase[Nothing, Nothing]
final case class Error[E](cause: E) extends ExitCase[E, Nothing]
final case class UnhandledError[E](cause: E) extends ExitCase[Nothing, E]
final case object Canceled extends ExitCase[Nothing, Nothing]
}
The following is a list of type classes with unimplemented or mostly unimplemented syntax files:
For example, the Ask
classes should implicitly widen, while the Error
classes should implicitly narrow. The former is equivalent to making Kleisli
contravariant, while the latter is equivalent to making EitherT
covariant, but critically implicit encodings of this work even on polymorphic types. Quick example:
object ApplicativeAsk {
implicit def widen[F[_], A, B >: A](implicit F: ApplicativeAsk[F, A]): ApplicativeAsk[F, B] = ...
Given an ApplicativeError[F, E]
, we should be able to get a Handle[F, E]
(which implies Raise[F, E]
). This would be very useful in that it allows us to get something like Raise[IO, Throwable]
, which extends the generality of the library somewhat. It shouldn't cause any ambiguity issues if it is pushed to the lowest priority of the resolution chain in the Handle
companion.
I noticed that even thought there is a cats-mtl Gitter channel, the Travis build notifications are going to the Cats Gitter channel instead. I don't have write permissions to this repo, so I can't see the right URL to fix it, but someone with permissions should be able to go to the cats-mtl Gitter channel, click the settings knobs on the upper right, click Integrations
and set up a new webhook URL for Travis.
If you leave the URL hard-coded in the .travis.yml
then you will still get notifications for builds on forks. Also I don't think that there's anything preventing anyone from copying the URL and sending build notifications for unrelated projects (though I don't know what their motive would be). See typelevel/cats#2691 for how to easily transition to the preferred way of setting up these notifications.
I'm trying to stack a WriterT and EitherT together - ultimately something like IO[Writer[Log, Either[Throwable, A]]]. The idea is that even in the event of an error the log up to that point should be maintained.
I have created a POC which uses a transformer stack of EitherT[WriterT[IO, Log, *], Throwable, *] and when I materialise the program it works as expected. The logs are present even in the event of an error. However FunctorListen seems to short circuit in the event of an error. Here is the code itself:
object POC extends IOApp {
import cats.instances.vector._
import cats.mtl.implicits._
import cats.syntax.flatMap._
import cats.syntax.functor._
type Log = Vector[String]
override def run(args: List[String]): IO[ExitCode] = {
val name = getName[EitherT[WriterT[IO, Log, *], Throwable, *]]
// this works even with a raised error so the stack itself is working
name.value.written.flatTap(logs => IO(println(logs))).as(ExitCode.Success)
// this won't work
printLog(name).value.run.as(ExitCode.Success)
}
def printLog[F[_], A](fa: F[A])(implicit F: Sync[F], FL: FunctorListen[F, Log]): F[A] = {
// this is never called when an error is raised
fa.listen.flatMap { case (a, logs) => F.delay(println(logs)).as(a) }
}
def getName[F[_]](implicit F: Sync[F], FT: FunctorTell[F, Log], FR: FunctorRaise[F, Throwable]): F[String] = for {
_ <- FT.tell(Vector("getting name ..."))
a <- F.delay("bob")
// Exception here means FunctorListen cant find any logs
_ <- FR.raise(new Exception("boom!")): F[Unit]
} yield a
}
I'm not sure if this is a problem with FunctorListen or FunctorRaise. It could be that FunctorRaise is raising an error at the IO level not the Either/EitherT
Cats-MTL v0.7.1 allows both arrangements.
Cats-MTL v1.1.1 case works only when EitherT is on the top.
https://scastie.scala-lang.org/vIEWTZkrTlGyel2jBiwRoA
import cats.{Monad, Id}
import cats.data.{StateT, EitherT}
import cats.implicits._
import cats.mtl.{MonadState, FunctorRaise}
import cats.mtl.implicits._
object Main extends App {
def computation[F[_]: Monad: MonadState[*[_], Int]: FunctorRaise[*[_], String]](a: Int): F[Int] = a.pure[F]
type MyEither[A] = EitherT[Id, String, A]
type MyState[A] = StateT[MyEither, Int, A]
val result = computation[MyState](42).run(0).value // <====== NO ERROR
println(result)
}
https://scastie.scala-lang.org/bZE05pflSnKOjpPiGmksjg
import cats.{Monad, Id}
import cats.data.{StateT, EitherT}
import cats.implicits._
import cats.mtl.{Stateful, Raise}
import cats.mtl.implicits._
object Main extends App {
def computation[F[_]: Monad: Stateful[*[_], Int]: Raise[*[_], String]](a: Int): F[Int] = a.pure[F]
type MyEither[A] = EitherT[Id, String, A]
type MyState[A] = StateT[MyEither, Int, A]
val result = computation[MyState](42).run(0).value // <====== ERROR
println(result)
}
Could not find an implicit instance of Raise[Main.MyState, String]. If you have
a good way of handling errors of type String at this location, you may want
to construct a value of type EitherT for this call-site, rather than Main.MyState.
An example type:
EitherT[Main.MyState, String, *]
This is analogous to writing try/catch around this call. The EitherT will
"catch" the errors of type String.
If you do not wish to handle errors of type String at this location, you should
add an implicit parameter of this type to your function. For example:
(implicit fraise: Raise[Main.MyState, String])
Manually defining the missing Raise
instance using def raise(e) = StateT.liftF(EitherT.leftT(e))
makes it work.
The existing documentation is okayish but needs a LOT of fleshing out.
We should commit a versioning scheme pre-1.0.
I think going with no changes in patch versions and breakages in minor versions seems ideal for now. We should also document this somewhere :)
In short one may need to mimic creating and running ReaderT
subprogram using Tagless Final approach.
Consider this endpoint in some app without TF:
class Endpoints(fooService: FooService, barService: BarService, ...){
def endpoints = HttpRoutes.of[IO] {
case ROOT -> "handle" => for {
body <- req.as[Body]
trace = genTrace
logContext = buildCtx(body)
auth = req.authInfo
result <- handle.run(auth, trace, logContext)
response <- Ok(result)
} yield response
}
def handle(body: Body): ReaderT[IO, (Auth, Trace, LogCtx), Result] = ???
//this method calls multiple other services and methods which also return `ReaderT[IO, ...]`.
//some of them are using context from reader to provide contextual logging or to avoid passing authorization as method parameters
}
And AFAIK there is no way to represent this behavior in tagless final code right now.
So here comes my proposal: cats-mtl should have some typeclass which can represent this, let's call it Run
:
trait Run[F[_], G[_], R] {
def run(fa: G[A])(r: R): F[A]
}
it should provide context of type R
to computation ga
of type G[A]
to evaluate it to another computation of typeF[A]
.
So now we can write the endpoint like that:
class Endpoints[Main[_], Contextual[_]](fooService: FooService[Contextual], barService: BarService[Contextual], ...)(implicit run: Run[Main, Contextual, (Auth, Trace, LogCtx)]) {
def endpoints = HttpRoutes.of[Main] {
case ROOT -> "handle" => for {
body <- req.as[Body]
trace = genTrace
logContext = buildCtx(body)
auth = req.authInfo
result <- run.run(handle(body))((auth, trace, logContext)) // Main[Result]
response <- Ok(result)
} yield response
}
def handle(body: Body): Contextual[Result] = ???
//this method calls multiple services and methods which look like FooService[Contextual]`.
}
//and creation of endpoints:
type Context = (Auth, Trace, LogCtx)
val endpoints = Endpoint[IO, ReaderT[IO, Context, *], Context](foo, bar) // or could be zio.Task and zio.RIO[Context, *]
So what do you think about this proposal? I could try to implement this if it seems like a good idea.
This scalaz PR targeting well-known ambigous-subtype implicit problem resolving it without repackaging base typeclass instances inside sub-typeclass es.
I wonder if this method is suitable for cats-mtl project.
PR welcome?
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.