Skip to content

Instantly share code, notes, and snippets.

Embed
What would you like to do?
Tagless final is a flexible and powerful design pattern in functional programming, this gist tries to give a brief explanation of it.
import scala.concurrent.{ExecutionContext, Future}
import Monad.syntax._
import scalaz.reactive.tmp.lambda_tickets.{HttpImplementation, Id, MockedImplementation}
object tickets {
import scala.concurrent.ExecutionContext.Implicits.global
// Q: How many implementations does this have? A: mathematically infinite
// Q: How many implementations differ from the one you intended? A: mathematically infinite
def foo1(a: Int): Int = ???
// Q: How many implementations does this have? A: 1, the identity function
// Q: How many implementations differ from the one you intended? A: 0
// Q: Why? A: Because we know nothing about A
def foo2[A](a: A): A = a
// Q: How many implementations does this have? A: 0
// Q: Why? A: Because the result is within context F[_], which we know nothing about
def foo3[F[_]](a: Int): F[Int] = ???
// Q: How many implementations does this have? A: 2, identity and the function provided by the type class
// Q: How many implementations differ from the one you intended? A: 1, but it is very obvious
// Making a polymorphic function removes all knowledge about the implementation, adding type classes adds
// enough knowledge to do something useful, and actually, the only correct thing
trait SecondPossibleComputation[A] { def somethingElse(a: A): A }
def foo4[A](a: A)(implicit andDo: SecondPossibleComputation[A]): A =
andDo.somethingElse(a)
// We can do the same for controlling the flow of a computation, in here the Monad instance is giving us
// enough knowledge to be able to write a useful computation
def foo[F[_]](a: Int)(implicit F: Monad[F]): F[Int] =
F.flatMap(F.pure(5))(a => F.pure(a + 1))
// We can leverage this technique of "only adding enough knowledge" to restrict our business domain logic,
// in the process decoupling the declaration of your program from the implementation, giving you a lot of
// flexibility and correctness
// Q: How many implementations does this have? A: Infinite if you build ticket with bad data, but the
// correct implementation is quiet obvious!
// Exercise: Try to think why, and how the type classes provide you with the means to write the correct implementation
// Exercise: Try to remove Monad from the implicits and try to figure out why it doesn't compile,
// If you feel lost remove one of the services from the implicits and think again
def reserveTicket[F[_]](id: String)(implicit ticketsService: TicketsService[F], concertService: ConcertsService[F], F: Monad[F]): F[Ticket] =
for {
lock <- ticketsService.reserveTicket(id)
concert <- concertService.getConcert(id)
} yield Ticket(lock.id, concert.name, lock.serialNumber)
// Different type class instances help us execute the same complex programs in different ways.
type Id[+A] = A
type HttpImplementation[+A] = Future[A]
type MockedImplementation[+A] = Id[A]
// Network call for production
val ticket1: Future[Ticket] = reserveTicket[HttpImplementation]("id")
// Mocked version for whatever else
implicit val mocks: TicketMocks = TicketMocks(
(TicketLock("id1", 1234), Concert("carbon based lifeforms")),
(TicketLock("id2", 1235), Concert("solar fields"))
)
val ticket2: Ticket = reserveTicket[MockedImplementation]("id")
// The functions within the typeclasses parametrized with F[_] (like TicketsService or ConcertsService) form
// a "domain specific language" (DSL) which we like to call a "free algebra", because it provides statements
// which can be combined, and which are "free of interpretation", that means depending on the typeclass instance
// the program executes with different effects but with the same logic, i.e. one may use http calls to other services,
// one might read a file, one may just provide predefined data for the functions.
}
case class TicketLock(id: String, serialNumber: Int)
case class Concert(name: String)
case class Ticket(id: String, concertName: String, serialNumber: Int)
case class TicketMocks(test1: (TicketLock, Concert), test2: (TicketLock, Concert))
trait TicketsService[F[_]] {
def reserveTicket(ticketId: String): F[TicketLock]
}
object TicketsService {
implicit def httpTickerService(implicit ec: ExecutionContext): TicketsService[HttpImplementation] =
new TicketsService[HttpImplementation] {
override def reserveTicket(ticketId: String): HttpImplementation[TicketLock] =
Future { ??? } // network call
}
implicit def mockTicketService(implicit mocks: TicketMocks): TicketsService[MockedImplementation] =
new TicketsService[MockedImplementation] {
override def reserveTicket(ticketId: String): MockedImplementation[TicketLock] =
if(ticketId == "ticket1") mocks.test1._1
else mocks.test2._1
}
}
trait ConcertsService[F[_]] {
def getConcert(ticketId: String): F[Concert]
}
object ConcertsService {
implicit def httpConcertsService(implicit ec: ExecutionContext): ConcertsService[HttpImplementation] =
new ConcertsService[HttpImplementation] {
override def getConcert(ticketId: String): HttpImplementation[Concert] =
Future { ??? } // network call
}
implicit def mockedConcertsService(implicit mocks: TicketMocks): ConcertsService[MockedImplementation] =
new ConcertsService[MockedImplementation] {
override def getConcert(ticketId: String): MockedImplementation[Concert] =
if(ticketId == "ticket1") mocks.test1._2
else mocks.test2._2
}
}
trait Monad[F[_]] {
def pure[A](a: A): F[A]
def flatMap[A, B](fa: F[A])(f: A => F[B]): F[B]
def map[A, B](fa: F[A])(f: A => B): F[B] =
flatMap(fa)(a => pure(f(a)))
}
object Monad {
implicit def futureMonad(implicit ec: ExecutionContext): Monad[Future] =
new Monad[Future] {
override def pure[A](a: A): Future[A] =
Future.successful(a)
override def flatMap[A, B](fa: Future[A])(f: A => Future[B]): Future[B] =
fa.flatMap(f)
}
implicit def idMonad: Monad[Id] =
new Monad[Id] {
override def pure[A](a: A): Id[A] = a
override def flatMap[A, B](fa: Id[A])(f: A => Id[B]): Id[B] =
f(fa)
}
object syntax {
implicit class MonadOps[F[_], A](fa: F[A])(implicit F: Monad[F]) {
def flatMap[B](f: A => F[B]): F[B] = F.flatMap(fa)(f)
def map[B](f: A => B): F[B] = F.map(fa)(f)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
You can’t perform that action at this time.