Created
November 4, 2020 13:00
-
-
Save hejfelix/e4c38c0f716f39e026c01739c032564f to your computer and use it in GitHub Desktop.
Context functions in Scala 3 with tests
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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