Towards Functional Programming 🦄
through Hexagonal Architecture 🎯
Habla Computing + CodelyTV
Acercándonos a la Programación Funcional 🦄
a través de la Arquitectura Hexagonal 🎯
Habla Computing + CodelyTV
Text
#SCBCN18
Software Crafters Barcelona - VI Edition
Functional Programming intro based on Hexagonal Architecture
Who we are
@JavierCane@juanshac
Session goal
🤔

Rethinking time
Functional Programming intro based on Hexagonal Architecture
Reviewing different technics to solve the same problems with more modularity
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
🗺 Design principles > 🎯 Hexagonal Architecture
🛀 Clean Architectures
🤯 Ports & adapters
🚫
🗺 Design principles > 🎯 Hexagonal Architecture
Layers & dependency rule
Infrastructure
Application
Domain
🗺 Design principles > 🎯 Hexagonal Architecture
Layered Architecture
Presentation
Domain
Database
🗺 Design principles > 🎯 Hexagonal Architecture
Layered vs Hexagonal
Presentation
Domain
Database
Infrastructure
Application
Domain
🗺 Design principles > 🎯 Hexagonal Architecture
CONTROLLER
REPOS
MODELS
SERVICES
APPLICATION SERVICE
D
A
I
Request flow
IMPLEMENTATION
Acceptance test
Unit test Integration test
🗺 Design principles > 🎯 Hexagonal Architecture
Acceptance test
Unit test Integration test
CONTROLLER
REPOS
MODELS
SERVICES
APPLICATION SERVICE
D
A
I
Request flow
IMPLEMENTATION
🗺 Design principles > 🎯 Hexagonal Architecture
Port
Adapter
VideoPostController
VideoRepository
VideoVideoCreator
D
A
I
DoobieMySqlVideoRepo
Acceptance test
Unit test Integration test
Request flow example
🗺 Design principles > 🎯 Hexagonal Architecture
VideoPostController
VideoRepository
VideoVideoCreator
D
A
I
DoobieMySqlVideoRepo
Acceptance test
Unit test Integration test
Port
Adapter
🗺 Design principles > 🎯 Hexagonal Architecture
Request flow example
Domain Events, CQRS, and Event Sourcing as an optional further step
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
What is a functional architecture?
DSL1
DSL2
DSLN
…
🗺 Design principles > 🦄 Functional Architectures
A FUNCTIONAL LANGUAGE IS A DOMAIN-
SPECIFIC LANGUAGE FOR DEFINING
DOMAIN-SPECIFIC LANGUAGES
The DSLs of your application
• Infrastructure DSLs
• HTTP
• Databases
• Messaging
• Application-specific DSLs
• Use cases
• Repos
• …
HTTP
SERVICE DSL
SQL
REPO DSL
🗺 Design principles > 🦄 Functional Architectures
Are hexagonal and functional architectures like apples and oranges?
HTTP
SERVICE DSL
SQL
REPO DSL
CONTROLLER
SERVICE IMPL.
ORM
DB
ADAPTER/INTERP.
PORT/DSL
🗺 Design principles > 🦄 Functional Architectures
There are no exceptions • Everything is either a port or an adapter
• Every adapter implements a given port
• Adapters are implemented on top of other ports
VideoPostController
VideoRepositoryVideoRepoC
DoobieMySqlVideoRepo
VideoCreator
HTTP
🗺 Design principles > 🦄 Functional Architectures
🎯
🦄
VideoPostController
VideoRepository
VideoCreator
DoobieMySqlVideoRepo
VideoPostController
VideoRepositoryVideoRepoC
DoobieMySqlVideoRepo
VideoCreator
HTTP
🤔 Rethinking time
🗺 Design principles
🗺 Design principles > 🤔 Rethinking time
Final goals
• Decouple from infrastructure
• Testing easiness (test doubles when testing out use cases)
• Change tolerance
• ¡Same goals!
🗺 Design principles > 🤔 Rethinking time
Differences: More decoupling and indirection levels
• Controllers:
• Consistency (APIs for all DSLs) vs. Specific needs
• Conclusion: We need industry standards. PHP-FIG as example to follow
• Application Services:
• Consistency (APIs for all DSLs) vs. Specific needs
• Reuse use case APIs in clients (e.g. user clients)
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
🎯 OOP
👤 Domain models
final case class Video(
id: VideoId,
title: VideoTitle,
quality: VideoQuality
)
👤 Domain models > 🎯 OOP > Video Entity
object Video {
def apply(id: String, title: String, quality: Int): Video =
Video(
VideoId(id),
VideoTitle(title),
VideoQuality(quality)
)
}
final case class Video(
id: VideoId,
title: VideoTitle,
quality: VideoQuality
)
👤 Domain models > 🎯 OOP > Video Entity
object Video {
def apply(id: String, title: String, quality: Int): Video =
Video(
VideoId(id),
VideoTitle(title),
VideoQuality(quality)
)
}
final case class Video(
id: VideoId,
title: VideoTitle,
quality: VideoQuality
)
👤 Domain models > 🎯 OOP > Video Entity
object VideoQuality {
val maxQuality = 50
val minQuality = 0
}
final case class VideoQuality(value: Int) {
require(value <= maxQuality, s"Video quality value greater than $maxQuality")
require(value >= minQuality, s"Video quality value less than $minQuality")
def increase(): VideoQuality = {
if (value >= maxQuality) this
else copy(value + 1)
}
}
OOP: Data+Behaviour
👤 Domain models > 🎯 OOP > VideoQuality Value Object
object VideoQuality {
val maxQuality = 50
val minQuality = 0
}
final case class VideoQuality(value: Int) {
require(value <= maxQuality, s"Video quality value greater than $maxQuality")
require(value >= minQuality, s"Video quality value less than $minQuality")
def increase(): VideoQuality = {
if (value >= maxQuality) this
else copy(value + 1)
}
}
Behaviour
Simpler API
+ domain semantics
+ immutability
👤 Domain models > 🎯 OOP > VideoQuality Value Object
case class Video(
id: VideoId,
title: VideoTitle,
quality: VideoQuality
) {
def increaseQuality(): Video =
copy(quality = quality.increase())
}
VideoQualityIncreaser Video
increaseQuality()
Behaviour
Rich domain model - Tell don’t ask
👤 Domain models > 🎯 OOP > Video Entity
public final class Video {
private VideoId id;
private VideoQuality quality;
// …
public VideoQuality getQuality() {
return quality;
}
public void setQuality(
VideoQuality quality
) {
this.quality = quality;
}
}
VideoQualityIncreaser Video
getQuality()
increaseQuality()
setQuality()
Behaviour
👤 Domain models > 🎯 OOP > Video Entity
setQuality()
getQuality()
increaseQuality()
…Increaser Video
Behaviour
…Increaser Video
Behaviour
👤 Domain models > 🎯 OOP > Video Entity
increaseQuality()
🦄 Functional Programming
👤 Domain models
…Increaser Video
Behaviour
👤 Domain models > 🦄 FP > Video Entity
increaseQuality()
…Increaser
increaseQuality()
Behaviour Video
…
def increaser[V: VideoBehaviour](
video: V): V =
video.increaseQuality()
Increaser
increaseQuality()
LOGIC
VideoBehaviour
Rich domain layer - Tell don’t ask
Do not expose quality property
👤 Domain models > 🦄 FP > Video EntityTYPE CLASSES: API
trait VideoBehaviour[T]{
val maxQuality: Int
val minQuality: Int
def increaseQuality(thing: T): T
}
+Less coupling!
PlainVideo
👤 Domain models > 🦄 FP > Video Entitycase class PlainVideo(
id: VideoId,
title: VideoTitle,
quality: VideoQuality
)
copy(quality=…)
quality
increase
:VideoImpl
VideoBeh
INSTANTIATION
implicit object VideoImpl
extends VideoBehaviour[PlainVideo]{
val maxQuality = 100
val minQuality = 10
def increaseQuality(video: PlainVideo) =
video.copy(quality =
video.quality.increase)
}
Video
Behaviour
👤 Domain models > 🦄 FP > Video Entitycase class Video(
id: VideoId,
title: VideoTitle,
quality: VideoQuality){
def increaseQuality(): Video =
copy(quality = quality.increase())
}
increaseQuality()
:VideoImpl
implicit object VideoImpl
extends VideoBehaviour[Video]{
val maxQuality = Video.maxQuality
def increaseQuality(video: Video) =
video.increaseQuality
}
VideoBeh
INSTANTIATION
👤 Domain models > 🦄 FP > Video Entity
COMPOSITION
def plainVideoIncreaser(video: PlainVideo): PlainVideo =
increaser[PlainVideo](video)
def videoIncreaser(video: Video): Video =
increaser[Video](video)
🤔 Rethinking time
👤 Domain models
& Implementation > 👤 Domain models > 🤔 Rethinking time
Differences on Domain Modelling goals
• 🦄 FP Type classes: APIs with steroids
• Goal: Coupling to behaviours (not to state)
• Decouple behaviour and data
• Support rich domain layers
• 🎯 OOP: High cohesion (behaviour close to related data)
• Modularity because abstractions => More indirection levels (complexity)
• Goal: Rich domain models (data+behaviour)
INSTANTIATION
TYPECLASS: API
COMPOSITION
LOGIC
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
🎯 Hexagonal Architecture
🏗 Layers implementations
final class VideoPostController(creator: VideoCreator) {
def post(id: String, title: String): StandardRoute = {
creator.create(VideoId(id), VideoTitle(title))
complete(HttpResponse(Created))
}
}
VideoPostController - HTTP API Controller
VideoPostController
final class VideoPostController(creator: VideoCreator) {
def post(id: String, title: String): StandardRoute = {
creator.create(VideoId(id), VideoTitle(title))
complete(HttpResponse(Created))
}
}
VideoPostController - HTTP API Controller
VideoPostController
final class VideoCreator(
repository: VideoRepository,
publisher: MessagePublisher
) {
def create(id: VideoId, title: VideoTitle): Unit = {
val video = Video(id, title)
repository.save(video)
publisher.publish(VideoCreated(video))
}
}
VideoCreator - Application Service/Use case
VideoPostController VideoCreator
final class VideoCreator(
repository: VideoRepository,
publisher: MessagePublisher
) {
def create(id: VideoId, title: VideoTitle): Unit = {
val video = Video(id, title)
repository.save(video)
publisher.publish(VideoCreated(video))
}
}
VideoCreator - Application Service/Use case
VideoPostController VideoCreator
trait VideoRepository {
def all(): Future[Seq[Video]]
def save(video: Video): Future[Unit]
}
VideoRepository - Domain contract/Port
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepository - Infrastructure implementation/Adapter
VideoPostController VideoRepositoryVideoCreator
DoobieMySqlVideoRepo
final class DoobieMySqlVideoRepository(db: DoobieDbConnection)
(implicit ec: ExecutionContext) extends VideoRepository {
override def all(): Future[Seq[Video]] =
db.read(sql”SELECT…”.query[Video].to[Seq])
override def save(video: Video): Future[Unit] =
sql"INSERT INTO…”.update.run
.transact(db.transactor)
.unsafeToFuture().map(_ => ())
}
🦄 Functional Architectures
🏗 Layers implementations
trait VideoRepository {
def all(): Future[Seq[Video]]
def save(video: Video):
Future[Unit]
}
Domain contract/PortVideoRepository
?
trait VideoRepository {
def all(): cats.IO[Seq[Video]]
def save(video: Video):
cats.IO[Unit]
}
trait VideoRepository {
def all(): Seq[Video]
def save(video: Video): Unit
}
trait VideoRepository {
def all(): Seq[Video]
def save(video: Video): Unit
}
trait VideoRepository {
def all(): Future[Seq[Video]]
def save(video: Video):
Future[Unit]
}
trait VideoRepository {
def all(): cats.IO[Seq[Video]]
def save(video: Video):
cats.IO[Unit]
}
Domain contract/Port
infrastructure leaks
VideoRepository
?
trait VideoRepository[P[_]] {
def all(): P[Seq[Video]]
def save(video: Video): P[Unit]
}
TYPE (CONSTRUCTOR) CLASSES
VideoRepository
?
Domain contract/Port
Infrastructure implementation/Adapter
import scala.concurent.Future
final class DoobieMySqlVideoRepoFuture(
db: DoobieDbConnection[Future])(implicit
ec: ExecutionContext) extends VideoRepository[Future] {
override def all(): Future[Seq[Video]] =
db.read(sql"SELECT ...".query[Video].to[Seq])
override def save(video: Video): Future[Unit] =
sql"INSERT INTO ... ".update.run
.transact(db.transactor)
.map(_ => ())
}
VideoRepository
DoobieVideoRepoFuture
INSTANTIATION
Infrastructure implementation/Adapter
import scala.concurent.Future
final class DoobieMySqlVideoRepoFuture(
db: DoobieDbConnection[Future])(implicit
ec: ExecutionContext) extends VideoRepository[Future] {
override def all(): Future[Seq[Video]] =
db.read(sql"SELECT ...".query[Video].to[Seq])
override def save(video: Video): Future[Unit] =
sql"INSERT INTO ... ".update.run
.transact(db.transactor)
.map(_ => ())
}
VideoRepository
DoobieVideoRepoFuture
INSTANTIATION
Infrastructure implementation/AdapterVideoRepository
DoobieVideoRepoIO
import cats.effect.IO
final class DoobieMySqlVideoRepoIO(
db: DoobieDbConnection[IO]) extends VideoRepository[IO] {
override def all(): IO[Seq[Video]] =
db.read(sql"SELECT ...".query[Video].to[Seq])
override def save(video: Video): IO[Unit] =
sql"INSERT INTO ... ".update.run
.transact(db.transactor)
.map(_ => ())
}
INSTANTIATION
Infrastructure implementation/AdapterVideoRepository
DoobieVideoRepoIO
import cats.effect.IO
final class DoobieMySqlVideoRepoIO(
db: DoobieDbConnection[IO]) extends VideoRepository[IO] {
override def all(): IO[Seq[Video]] =
db.read(sql"SELECT ...".query[Video].to[Seq])
override def save(video: Video): IO[Unit] =
sql"INSERT INTO ... ".update.run
.transact(db.transactor)
.map(_ => ())
}
INSTANTIATION
Infrastructure implementation/AdapterVideoRepository
DoobieVideoRepo (GENERIC) INSTANTIATION
final case class DoobieMySqlVideoRepo[P[_]: Monad]()(
db: DoobieDbConnection[P]) extends VideoRepository[P] {
override def all(): P[Seq[Video]] =
db.read(sql"SELECT …”.query[Video].to[Seq])
override def save(video: Video): P[Unit] =
sql"INSERT …”.update.run
.transact(db.transactor)
.map(_ => ())
}
Infrastructure implementation/AdapterVideoRepository
DoobieVideoRepo (GENERIC) INSTANTIATION
final case class DoobieMySqlVideoRepo[P[_]: Monad]()(
db: DoobieDbConnection[P]) extends VideoRepository[P] {
override def all(): P[Seq[Video]] =
db.read(sql"SELECT …”.query[Video].to[Seq])
override def save(video: Video): P[Unit] =
sql"INSERT …”.update.run
.transact(db.transactor)
.map(_ => ())
}
🤔 Rethinking time
🏗 Layers implementations
Differences on layers implementation
• Same goal: Decouple from infrastructure
• 🦄 FP:
• One step forward to avoid infrastructure leaks
• Again, through type classes: a better API
• More complexity: monads, applicatives, etc.
& Implementation > 🏗 Layers > 🤔 Rethinking time
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
👤
Domain
models
Contents
🗺Design principles
&Implementation details
🦄
Functional
Architectures
🎯
Hexagonal
Architecture
🏗
Layers
implementation
Functional Programming intro based on Hexagonal Architecture
⚡
Error
Handling
🎯 OOP
⚡Error Handling
object VideoQuality {
val maxQuality = 50
val minQuality = 0
}
final case class VideoQuality(value: Int) {
require(value <= maxQuality, s"Video quality value greater than $maxQuality")
require(value >= minQuality, s"Video quality value less than $minQuality")
def increase(): VideoQuality = {
if (value >= maxQuality) this
else copy(value + 1)
}
}
VO: Raise exceptions
for invalid states
⚡Error Handling > 🎯 OOP > Raise exceptions
final class VideoFinder(repository: VideoRepository)
(implicit ec: ExecutionContext) {
def find(id: VideoId): Future[Video] =
repository.search(id).map { videoOption =>
if (videoOption.isEmpty) throw VideoNotFound(id)
else videoOption.get
}
}
Imperativeness
⚡Error Handling > 🎯 OOP > Raise exceptions
trait VideoRepository {
def search(id: VideoId): Future[Option[Video]]
}
final class VideoGetController extends ApiController
{
protected function exceptions(): array
{
return [
VideoNotFound::class => Response::HTTP_NOT_FOUND,
];
}
// …
}
HTTP Mapping
⚡Error Handling > 🎯 OOP > Handle exceptions
🦄 Functional Architectures
⚡Error Handling
Imperativeness
⚡Error Handling > 🦄 FP > Raise errors
Declarativeness
+
final case class VideoFinderRepo[P[_]]()(
implicit repository: VideoRepository[P]
) extends VideoFinder[P] {
def find(id: VideoId)(implicit
E: MonadError[P, VideoFinderError]): P[Video] =
repository.search(id) flatMap { maybeVideo =>
maybeVideo.fold(E.raiseError[Video](VideoNotFound(id))){
video => video.pure[P]
}
}
}
⚡Error Handling > 🦄 FP > Raise errors
final case class VideoFinderRepo[P[_]]()(
implicit repository: VideoRepository[P]
) extends VideoFinder[P] {
def find(id: VideoId)(implicit
E: MonadError[P, VideoFinderError]): P[Video] =
repository.search(id) flatMap { maybeVideo =>
maybeVideo.fold(E.raiseError[Video](VideoNotFound(id))){
video => video.pure[P]
}
}
}
A functional signature doesn’t hide anything!
⚡Error Handling > 🦄 FP > Raise errors
TYPE CLASSES
trait MonadError[F[_], E] {
def raiseError[A](e: E): F[A]
def handleErrorWith[A](fa: F[A])(f: E => F[A]): F[A]
}
⚡Error Handling > 🦄 FP > Raise errors
final case class VideoFinderRepo[P[_]: Monad]()(
implicit repository: VideoRepository[P]
) extends VideoFinder[P] {
def find(id: VideoId)(implicit
E: MonadError[P, VideoFinderError]): P[Video] =
repository.search(id) flatMap { maybeVideo =>
maybeVideo.fold(E.raiseError[Video](VideoNotFound(id))){
video => video.pure[P]
}
}
}
⚡Error Handling > 🦄 FP > Raise errors
final case class VideoFinderRepo[P[_]: Monad]()(
implicit repository: VideoRepository[P]
) extends VideoFinder[P] {
def find(id: VideoId)(implicit
E: MonadError[P, VideoFinderError]): P[Video] =
repository.search(id) flatMap { maybeVideo =>
maybeVideo.fold(E.raiseError[Video](VideoNotFound(id))){
video => video.pure[P]
}
}
}
Imperativeness
⚡Error Handling > 🦄 FP > Raise errors
final case class VideoFinderRepo[P[_]: Monad]()(
implicit repository: VideoRepository[P]
) extends VideoFinder[P] {
def find(id: VideoId)(implicit
E: MonadError[P, VideoFinderError]): P[Video] =
val maybeVideo = repository.search(id) ;
if (videoOption.isEmpty) throw VideoNotFound(id)
else videoOption.get
}
Monadic programming is
imperative programming with steroids!
object FutureBasedController{
case class VideoController(
videoFinder: VideoFinder[Future]
)(implicit ec: ExecutionContext) extends Http4sDsl[Future] {
val service = HttpService[Future] {
case GET -> Root / "videos" / id =>
videoFinder.find(VideoId(id)) flatMap {
case video =>
Ok(video.asJson)
} recoverWith {
case error: VideoNotFound =>
NotFound(error.asJson)
}
}
}
}
⚡Error Handling > 🦄 FP > Raise errors
Future-based
HTTP Mapping
LOGIC
case class VideoController[P[_]: Effect](
videoFinder: VideoFinder[P]
) extends Http4sDsl[P] {
val service = HttpService[P] {
case GET -> Root / "videos" / id =>
videoFinder.find(VideoId(id)) flatMap {
case video =>
Ok(video.asJson)
} handleErrorWith {
case error: VideoNotFound =>
NotFound(error.asJson)
}
}
}
⚡Error Handling > 🦄 FP > Raise errors
Generic
HTTP Mapping
MonadError
API
LOGIC
🤔 Rethinking time
⚡Error Handling
Differences on Error handling
• 🦄 FP:
• FP tell no lies (or, rather, doesn’t hide anything): more robust contracts using
type system
• Declarative: more abstract about how the error will be raised (exceptions,
Option, Either, etc)
& Implementation > ⚡Error Handling > 🤔 Rethinking time
🍕Takeaways
🎯
🦄
VideoPostController
VideoRepository
VideoCreator
DoobieMySqlVideoRepo
VideoPostController
VideoRepositoryVideoRepoC
DoobieMySqlVideoRepo
VideoCreator
HTTP
🍕Takeaways
⚡ Exceptions
🏗 Monolithic interfaces
👤 Rich domain models
🍕Takeaways
⚡ Exceptions
🏗 Monolithic interfaces
👤 Rich domain models
🦄 TypeClasses
⚛ APIs with steroids
🧐 FP as a subsuming paradigm
ℹ More info
References
ℹ More info
👉Formación FP presencial 👈
hablapps.com
👉Cursos en vídeo online 👈
codely.tv/pro/cursos

Towards Functional Programming through Hexagonal Architecture