Skip to main content

WithContext

Installation

"tf.tofu" %% "tofu" % tofu-version
or as a standalone dependency "tf.tofu" %% "tofu-core-*" % tofu-version (replace suffix * with ce2 or ce3 depends on which cats-effect version you use)

What if you don't need Env

Env is a powerful monad, but what if you're sure that you don't need it? You can still use convenient Tofu concepts to work with your own Environment (WithContext).

Usage example and a short use case description

The short story long, it is possible to use ReaderT:

import cats._
import cats.data.ReaderT
import cats.instances.option._
import tofu._
import tofu.optics._

// defining our own Env that stores some User
case class User(id: Int, name: String)
case class MyEnv(user: User)

// defining an extractor, extractor is a common lens that you can read about
// in a paragraph about lenses
implicit val extractor: Extract[MyEnv, User] = _.user

def program[F[_]: WithContext[*[_], MyEnv]](implicit u: MyEnv Extract User): F[String] =
WithContext[F, MyEnv].extract(u).ask(_.name)

// ~voilà
program[ReaderT[Option, MyEnv, *]].run(MyEnv(User(0, "Tofu"))) //> Some(Tofu): Option[String]

A bit more complicated example, that shows lenses usage only in the functions that require them:

import cats._
import cats.data.ReaderT
import cats.instances.option._
import cats.syntax.apply._
import tofu._
import tofu.optics._

// defining our own Env that stores a User and some related Metadata
case class User(id: Int, name: String)
case class Metadata(height: Double, age: Int)
case class MyEnv(user: User, md: Metadata)

// defining extractors
implicit val userExtractor: Extract[MyEnv, User] = _.user
implicit val mdExtractor: Extract[MyEnv, Metadata] = _.md

// it is possible to define a program that only has a context
def program[F[_]: Apply: WithContext[*[_], MyEnv]]: F[String] =
(name[F], age[F]).mapN { (name, age) => s"$name: $age" }

// but all the functions that were called inside a program
// have on demand and only necessary extractors
def name[F[_]: WithContext[*[_], MyEnv]](implicit u: MyEnv Extract User): F[String] =
WithContext[F, MyEnv].extract(u).ask(_.name)

def age[F[_]: WithContext[*[_], MyEnv]](implicit m: MyEnv Extract Metadata): F[Int] =
WithContext[F, MyEnv].extract(m).ask(_.age)

// ~voilà
program[ReaderT[Option, MyEnv, *]]
.run(MyEnv(User(0, "Tofu"), Metadata(60, 18))) //> Some(Tofu: 18): Option[String]

It is also possible to do define some WithContext explicitly without having a need in Env or ReaderT monads:

import cats._
import cats.instances.option._
import cats.syntax.apply._
import tofu._
import tofu.optics._

// defining our own Env that stores a User and some related Metadata
case class User(id: Int, name: String)
case class Metadata(height: Double, age: Int)
case class MyEnv(user: User, md: Metadata)

// defining extractors
implicit val userExtractor: Extract[MyEnv, User] = _.user
implicit val mdExtractor: Extract[MyEnv, Metadata] = _.md

// what if we don't need or don't know what ReaderT is
// we can define a const Context than
implicit val ctx: WithContext[Option, MyEnv] =
WithContext.const[Option, MyEnv](MyEnv(User(0, "Tofu"), Metadata(60, 18)))

// it is still possible to define a program that only has a context
def program[F[_]: Apply: WithContext[*[_], MyEnv]]: F[String] =
(name[F], age[F]).mapN { (name, age) => s"$name: $age" }

// and all the functions that were called inside a program
// have on demand and only necessary extractors
def name[F[_]: WithContext[*[_], MyEnv]](implicit u: MyEnv Extract User): F[String] =
WithContext[F, MyEnv].extract(u).ask(_.name)

def age[F[_]: WithContext[*[_], MyEnv]](implicit m: MyEnv Extract Metadata): F[Int] =
WithContext[F, MyEnv].extract(m).ask(_.age)

// ~voilà
program[Option] //> Some(Tofu: 18): Option[String]