Skip to content

Instantly share code, notes, and snippets.

@hejfelix
Created November 4, 2020 13:00
Show Gist options
  • Save hejfelix/e4c38c0f716f39e026c01739c032564f to your computer and use it in GitHub Desktop.
Save hejfelix/e4c38c0f716f39e026c01739c032564f to your computer and use it in GitHub Desktop.
Context functions in Scala 3 with tests
import cats.Applicative
import cats.data.Kleisli
/**
* This highly modular service
* allows the call-site to pick and choose
* between the different functions while still
* sharing code for e.g. configuration/context.
*
* The opaque types `Name` and `AccountNumber` guarantee
* a highly specific and well-documented interface for each function.
*
*/
object AccountService {
opaque type Name = String
object Name {
def apply(s:String):Name = s
}
opaque type AccountNumber = Long
object AccountNumber {
def apply(l:Long):AccountNumber = l
}
case class AccountConfig(name: Name, accountNumber: AccountNumber)
type ContextFunction[F[_], T] =
Applicative[F] ?=> Kleisli[F, AccountConfig, T]
def getContext[F[_]]: ContextFunction[F, AccountConfig] =
Kleisli.ask
def accountNumber[F[_]]: ContextFunction[F, AccountNumber] =
Kleisli.ask.map(_.accountNumber)
def name[F[_]]: ContextFunction[F, Name] =
Kleisli.ask.map(_.name)
}
import cats.{Applicative, Id}
import org.junit.Test
import org.junit.Assert._
import cats.implicits._
class AccountServiceTest {
import AccountService._
given Applicative[Id] = new {
override def pure[T](t:T):Id[T] = t
override def ap[A,B](ff:Id[A => B])(a:Id[A]):Id[B] = ff(a)
}
private val testName: Name = Name("Felix")
private val testAccountNumber: AccountNumber = AccountNumber(1337L)
private val testAccount =
AccountService.AccountConfig(testName, testAccountNumber)
@Test def accountServiceName =
assertEquals(AccountService.name[Id](testAccount),testName)
@Test def accountServiceNumber =
assertEquals(AccountService.accountNumber[Id](testAccount),testAccountNumber)
@Test def accountConfig =
assertEquals(AccountService.getContext[Id](testAccount),testAccount)
}
import AccountService.{AccountConfig, AccountNumber, ContextFunction, Name}
import cats.{Applicative, FlatMap, Id, Monad}
import cats.data.{Kleisli, Reader}
import cats.implicits._
trait MainProgram[F[_]: Monad]{
type CF[T] = ContextFunction[F,T]
/**
* We define a Dsl with the functions that WE need
* at _the call site_. The burden here is reversed, i.e. we
* don't get a lot of functions that we don't need.
*
* For testing, this means we don't need to mock out
* unneeded functions.
*/
trait Dsl {
val getName: CF[Name]
val getAccountNumber: CF[AccountNumber]
}
val defaultDsl:Dsl = new Dsl {
val getName = AccountService.name
val getAccountNumber = AccountService.accountNumber
}
val program = (dsl:Dsl) =>
import dsl._
for
name <- getName
an <- getAccountNumber
yield (an, name)
def main(args: Array[String]): Unit =
val accountConfig = AccountConfig(Name("Felix"), AccountNumber(1337l))
val run = program(defaultDsl)
println(run(accountConfig))
}
object Main extends MainProgram[Option]
import AccountService.{AccountNumber, Name}
import cats.data.Kleisli
import cats.{Id, Monad}
import org.junit.Test
import org.junit.Assert._
import cats.implicits._
import scala.language.implicitConversions
class ProgramTest {
given Monad[Id] = new {
override def pure[T](t:T):Id[T] = t
override def flatMap[A,B](fa:Id[A])(f: A => Id[B]):Id[B] = f(fa)
override def tailRecM[A, B](a: A)(f: A => Id[Either[A, B]]): Id[B] = ???
}
object TestMain extends MainProgram[Id]
private val testName: Name = Name("Felix")
private val testAccountNumber: AccountNumber = AccountNumber(1337L)
private val testAccount =
AccountService.AccountConfig(testName, testAccountNumber)
val dsl:TestMain.Dsl = new {
val getAccountNumber = testAccountNumber.pure
val getName = testName.pure
}
@Test def testProgram = assertEquals(TestMain.program(dsl)(testAccount), (testAccountNumber,testName))
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment