It is built for meow-mtl again in Scala 3.
It provides the similar functionality with meow-mtl, but limited magics in Scala 3:
Available for Scala JVM, Scalajs:
libraryDependencies += "io.github.pismute" %% "classy-mtl" % "<version>"
libraryDependencies += "io.github.pismute" %% "classy-effect" % "<version>"Inspired by Next-level MTL talk and meow-mtl
import classy.mtl.*
object app:
case class AppEnv(httpEnv: HttpEnv, dbEnv: DbEnv)
given [F[_]](using Ask[F, AppEnv]): Ask[F, HttpEnv] = deriveAsk
given [F[_]](using Ask[F, AppEnv]): Ask[F, DbEnv] = deriveAsk
enum AppError derives Show:
case AppHttpError(error: HttpError)
case AppDbError(error: DbError)
given [F[_]](using Raise[F, AppError]): Raise[F, HttpError] = deriveRaise
given [F[_]](using Raise[F, AppError]): Raise[F, DbError] = deriveRaise
import app.*
object appt:
private type App[A] = EitherT[ReaderT[IO, AppEnv, *], AppError, A]
opaque type AppT[A] = App[A]
object AppT:
def apply[A](x: App[A]): AppT[A] = x
given (using inst: Ask[App, AppEnv]): Ask[AppT, AppEnv] = inst
given (using inst: Async[App]): Async[AppT] = inst
given (using inst: LiftIO[App]): LiftIO[AppT] = inst
given (using inst: Raise[App, AppError]): Raise[AppT, AppError] = inst
import appt.*
class HttpService[F[_]](using ask: Ask[F, HttpEnv], raise: Raise[F, HttpError]):
...
end HttpService
class DbService[F[_]](using ask: Ask[F, DbEnv], raise: Raise[F, DbError]):
...
end DbService
val httpService = HttpService[AppT]
val DbService = DbService[AppT]classy-optics has the same goal with meow-mtl, but focused on having less ambiguity in Scala 3. classy-optics can narrow MTL type classes with classy optics, Optic instances are automatically generated. It does not support identical optics because we do not need regenerate existing MTL instances. Also, It gives compilation error on ambiguous data type as possible.
case class Mono(i: Int)
summon[Getter[Mono, Int]]
summon[Lens[Mono, Int]]
summon[Iso[Mono, Int]]
case class MyProduct(i: Int, s: String)
summon[Getter[MyProduct, Int]]
summon[Getter[MyProduct, String]]
summon[Lens[MyProduct, Int]]
summon[Lens[MyProduct, String]]
summon[Iso[MyProduct, Int]] // error because MyProduct is not monomorphic.
summon[Getter[MyProduct, MyProduct]] // error
summon[Lens[MyProduct, MyProduct]] // error
summon[Iso[MyProduct, MyProduct]] // error
case class AmbiguousProduct(i1: Int, i2: Int)
summon[Getter[AmbiguousProduct, Int]] // error
summon[Lens[AmbiguousProduct, Int]] // errorenum HttpError:
case TooManyRequest(n: Int)
case Unauthorized
enum DbError:
case LostConnection, QueryTimeout
enum AppError:
case DbAppError(error: DbError)
case HttpAppError(error: HttpError)
case AnotherAppError(code: Int, msg: String)
summon[Review[AppError, DbError]]
summon[Review[AppError, HttpError]]
summon[Prism[AppError, DbError]]
summon[Prism[AppError, HttpError]]
summon[Review[AppError, AppError.AnotherAppError]]
summon[Prism[AppError, AppError.AnotherAppError]]
summon[Review[AppError, Int]] // error
summon[Review[AppError, String]] // error
summon[Prism[AppError, Int]] // error
summon[Prism[AppError, String]] // errorclassy-optics does not traverse all hierarchical tree. It looks for only first level, and root level for sum types. So data hierarchy should be kept along with application hierarchy:
summon[Prism[AppError, HttpError.TooManyRequest]] // error on second level.classy-optics derive MTL instances via optics from base instances
import classy.mtl.*
case class MyType(subtype: MySubtype)
given Ask[AppT, MyType] = ...
given Ask[AppT, MySubType] = deriveAsk
summon[Ask[AppT, MySubType]]Automatic derivation is also supported:
import classy.mtl.auto.*
case class MyType(subtype: MySubtype)
given Ask[AppT, MyType] = ...
summon[Ask[AppT, MySubType]]Supported typeclasses:
| Typeclass | Required optic |
|---|---|
| Handle | Prism |
| Raise | Review |
| Tell | Review |
| Ask | Getter |
| Local | Lens |
| Stateful | Lens |
classy-effect provides MTL instances for some cats effect types:
| cats-effect | Mtl |
|---|---|
| AtomicCell | Ask, Stateful, Tell |
| IOLocal | Ask, Stateful, Tell |
| Ref | Ask, Ref(?), Tell |
case class AppEnv(httpEnv: HttpEnv, dbEnv: DbEnv)
given [F[_]](using Ask[F, AppEnv]): Ask[F, HttpEnv] = deriveAsk
for
ref <- Ref.of[AppT, AppEnv](AppEnv(...))
given Ask[Appt, AppEnv] = ref.ask
yield ...
Ref could not have Stateful instance because Stateful is not atomic, see typelevel/cats-mtl#120.
Instead, we can use Ref directly like the other MTL type classes.
import classy.effect.*
case class AppCache(http: HttpCache, db: DbCache)
given [F[_]](using Ref[F, AppCache]): Ref[F, HttpCache] = deriveRef
for
given Ref[AppT, AppCache] <- Ref.of[AppT, AppCache](...)
_ = summon[Ref[AppT, HttpCache]]
yield ... An intersection type is a type alias, it is a product-like alias. We can think the cardinality of ProductLike is 4 as of a product of HasBoolean1's one and HasBoolean2's one:
case class HasBoolean1(b: Boolean)
case class HasBoolean2(b: Boolean)
type ProductLike = HasBoolean1 & HasBoolean2Maybe, We might use Intersection type in our data. Compiler does not provide goodies like case class, though, there is some free lunches from the type inference. For example:
trait DbConfig:
def dbPort: Int
trait HttpConfig:
def httpPort: Int
case case AppConfigCake(dbPort: Int, httpPort: Int) extends DbConfig with HttpConfig derives Show
type AppConfig = DbConfig & HttpConfigAppConfigCake is needed to materialize AppConfig. Also we can take case class's goodies like derives Show, in this case.
As a product type, intersection types can have optics. AppConfig is visible as DbConfig or HttpConfig. So, intersection types can have Getter instances. If it has Getter instances, then we can derive Ask instances automatically. It is the idea of this library.
However, we do not need even Getter instances for intersection types to have Ask instances, because Ask is covariant. Ask[F[_], +E]'s E is covariant. When we ask Scala compiler to bring Ask instances from given space, the compiler will bring the instance what exactly we want. The compiler will bring Ask[F[_], AppConfig] instance for Ask[F[_], DbConfig] without optics. It is free lunches, free optics.
Intersection types can not have Lens optic, because they do not have the standard way of mutation, which is a way of set. Without Lens optics, narrowing Local and Stateful can not work with intersection types.
So, classy-optics does not provide any optics for intersection types.
Union type is a sum-like alias, the cardinality of AppError is 4 as of a sum of DbError and HttpError:
enum DbError:
case LostConnection, ExceedLimit
enum HttpError:
case LostConnection, ExceedLimit
type AppError = DbError | HttpErrorUnlike intersection types, Mutation is not required for Prism types. They can have both of a Prism instance and a Review instance. It is simple:
class APrism[AppError, DbError] extends Prism[AppError, DbError]:
def preview: AppError => Option[DbError] = { case x: DbError => x }.lift
def review: DbError => AppError = x => xAlso, like Ask[F[_], +E] type class, the parameters of Raise[F[_], -E] and Tell[F[_], -L] are contravariant. So deriving their sub instances is not needed. If Raise[F[_], AppError] and Tell[F[_], AppError] exists in given space. Compiler will return those instances for Raise[F[_], DbError], Tell[F[_], DbError] and so on. In case of Raise and Tell, we do not need optics.
But, Handle[F[_], E]'s type parameter is invariant. we will not get Handle[F[_], AppError] instance for Handle[F[_], DbError]. So, Handle need a Prism optic to narrow it.
Now, classy-optics provides Prism instances for Union types. It works seamlessly:
type AppError = DbError | HttpError
summon[Raise[F, DbError]] // works if Raise[F, AppError] exists
// works because Prism[AppError, DbError] is derived automatically
given [F[_]](using Handle[F, AppError]): Handle[F, DbError] = deriveHandleOk, it looks simpler than enum AppError. But, yet, it is not. There are no instances for essential type classes like Eq, Show and so on. The instances need to be provided manually.
MIT