KEMBAR78
Scala UA: Big Step To Functional Programming | PDF
Big Step To Functional
Programming*
* - beginners level
Alex Zvolinskiy
Senior Scala Developer at VidIQ
Blog: www.Fruzenshtein.com
Twitter: @Fruzenshtein
LinkedIn, Facebook, GitHub
Motivation
Conner Baker @connerbaker
Mauna Kea Observatory, Waimea, United States
Real Life Example
User Service
addUser
getUser
Offer Service
addOffer
getOffer
deleteOffer
Basic Types
type UserId = String
type Name = String
type OfferId = Int
type Message = String
final case class User(id: UserId, name: Name)
final case class Offer(id: OfferId, userId: UserId, message: Message, active: Boolean)
type UserStorage = collection.mutable.Map[UserId, User]
type OfferStorage = collection.mutable.Map[OfferId, Offer]
Standard Solution
trait UserDAO {
def addUser(user: User): Unit
def getUser(id: UserId): Option[User]
}
class UserDaoImpl(val storage: UserStorage) extends UserDAO {
def addUser(user: User): Unit = storage(user.id) = user
def getUser(id: UserId): Option[User] = storage.get(id)
}
Standard Solution
trait OfferDAO {
def addOffer(offer: Offer): Unit
def getOffer(offerId: OfferId): Option[Offer]
def deleteOffer(offerId: OfferId): Unit
}
class OfferDaoImpl(val storage: OfferStorage) extends OfferDAO {
override def addOffer(offer: Offer): Unit =
storage(offer.id) = offer
override def getOffer(offerId: OfferId): Option[Offer] =
storage.get(offerId)
override def deleteOffer(offerId: OfferId): Unit =
storage -= offerId
}
Standard Solution
val userDB = mutable.Map.empty[UserId, User]
val userDao = new UserDaoImpl(userDB)
userDao.addUser(User("uid01", "Alex"))
val user = userDao.getUser("uid01")
println(user)
for {
_ <- userDao.addUser(User("uid01", "Alex"))
user <- userDao.getUser("uid01")
} yield {
println(user)
}
Error: value flatMap is not a member of Unit
_ <- userDao.addUser(User("uid01", "Alex"))
Caution: Cats
point[M[_], A](a: A): M[A]
flatMap[M[_], A, B](fa: M[A])(f: A => M[B]): M[B]
“Another” Solution
sealed trait UserOps[A]
final case class Add(user: User) extends UserOps[Unit]
final case class Get(id: UserId) extends UserOps[Option[User]]
import cats.free.Free
type UserStorage[A] = Free[UserOps, A]
import cats.free.Free.liftF
def add(user: User): UserStorage[Unit] = liftF[UserOps, Unit](Add(user))
def get(id: UserId): UserStorage[Option[User]] = liftF[UserOps, Option[User]](Get(id))
import cats.{Id, ~>}
import scala.collection.mutable
def compiler(): UserOps ~> Id = new (UserOps ~> Id) {
val userStorage = mutable.Map.empty[UserId, User]
override def apply[A](fa: UserOps[A]): Id[A] = fa match {
case Add(user) => userStorage(user.id) = user
case Get(id) => userStorage.get(id)
}
}
“Another” Solution
import cats.{Id, ~>}
import scala.collection.mutable
def compiler(): UserOps ~> Id = new (UserOps ~> Id) {
val userStorage = mutable.Map.empty[UserId, User]
override def apply[A](fa: UserOps[A]): Id[A] = fa match {
case Add(user) => userStorage(user.id) = user
case Get(id) => userStorage.get(id)
}
}
val computations =
for {
_ <- add(User("uid01", "Alex"))
user <- get("uid01")
} yield {
println(user) //Some(User(uid01,Alex))
}
computations.foldMap(compiler)
Steps
1. Define ADT
2. Create a Free[_] for the ADT
3. Write a compiler
4. Compose operations in a program
5. Run the program
What about async?
trait UserDAO {
implicit val ec: ExecutionContext
val storage: UserStorage
def addUser(user: User): Future[Unit]
def getUser(id: UserId): Future[Option[User]]
}
class UserDaoImpl(val storage: UserStorage)
(implicit val ec: ExecutionContext) extends UserDAO {
def addUser(user: User): Future[Unit] = Future(storage(user.id) = user)
def getUser(id: UserId): Future[Option[User]] = Future(storage.get(id))
}
trait OfferDAO {
...
}
implicit val ec = ExecutionContext.global
val userDB = mutable.Map.empty[UserId, User]
val userDao = new UserDaoImpl(userDB)
val offerDB = mutable.Map.empty[OfferId, Offer]
val offerDao = new OfferDaoImpl(offerDB)
val result = for {
_ <- userDao.addUser(User("uid01", "Alex"))
_ <- offerDao.addOffer(Offer(1, "uid01", "10% discount", false))
offerAdded <- offerDao.getOffer(1)
_ <- offerDao.deleteOffer(1)
offerDeleted <- offerDao.getOffer(1)
} yield {
println(offerAdded) //Some(Offer(1,uid01,10% discount,false))
println(offerDeleted) //None
}
Await.result(result, 1.second)
“Another” Async Solution
sealed trait UserOps[A]
final case class Add(user: User) extends UserOps[Unit]
final case class Get(id: UserId) extends UserOps[Option[User]]
import cats.free.Free
type UserStorage[A] = Free[UserOps, A]
import cats.free.Free.liftF
def add(user: User): UserStorage[Unit] = liftF[UserOps, Unit](Add(user))
def get(id: UserId): UserStorage[Option[User]] = liftF[UserOps, Option[User]](Get(id))
Different part of async Solution
import cats.~>
import cats.implicits._
import scala.collection.mutable
import scala.concurrent.ExecutionContext.Implicits.global
def compiler: UserOps ~> Future = new (UserOps ~> Future) {
val userStorage = mutable.Map.empty[UserId, User]
override def apply[A](fa: UserOps[A]): Future[A] = fa match {
case Add(user) => Future(userStorage(user.id) = user)
case Get(id) => Future(userStorage.get(id))
}
}
Async Program Run
val computations =
for {
_ <- add(User("uid01", "Alex"))
user <- get("uid01")
} yield {
println(user) //Some(User(uid01,Alex))
}
import scala.concurrent.duration._
Await.result(computations.foldMap(compiler), 1.second)
What about composition?
sealed trait UserOps[A]
final case class AddUser(user: User) extends UserOps[Unit]
final case class GetUser(id: UserId) extends UserOps[Option[User]]
sealed trait OfferOps[A]
final case class AddOffer(offer: Offer) extends OfferOps[Unit]
final case class GetOffer(id: OfferId) extends OfferOps[Option[Offer]]
final case class DeleteOffer(id: OfferId) extends OfferOps[Unit]
type UserOfferApp[A] = EitherK[UserOps, OfferOps, A]
Free[_] types for ADTs
class UserOperations[F[_]](implicit I: InjectK[UserOps, F]) {
def addUser(user: User): Free[F, Unit] = Free.inject[UserOps, F](AddUser(user))
def getUser(id: UserId): Free[F, Option[User]] = Free.inject[UserOps, F](GetUser(id))
}
object UserOperations {
implicit def userOperations[F[_]](implicit I: InjectK[UserOps, F]): UserOperations[F] =
new UserOperations[F]
}
class OfferOperations[F[_]](implicit I: InjectK[OfferOps, F]) {
def addOffer(offer: Offer): Free[F, Unit] = Free.inject[OfferOps, F](AddOffer(offer))
def getOffer(id: OfferId): Free[F, Option[Offer]] = Free.inject[OfferOps, F](GetOffer(id))
def deleteOffer(id: OfferId): Free[F, Unit] = Free.inject[OfferOps, F](DeleteOffer(id))
}
object OfferOperations {
implicit def offerOperations[F[_]](implicit I: InjectK[OfferOps, F]): OfferOperations[F] =
new OfferOperations[F]
}
Interpreters
object UserOpsInterpreter extends (UserOps ~> Id) {
val userStorage = mutable.Map.empty[UserId, User]
override def apply[A](fa: UserOps[A]) = fa match {
case AddUser(user) => userStorage(user.id) = user
case GetUser(id) => userStorage.get(id)
}
}
object OfferOpsInterpreter extends (OfferOps ~> Id) {
val offerStorage = mutable.Map.empty[OfferId, Offer]
override def apply[A](fa: OfferOps[A]) = fa match {
case AddOffer(offer) => offerStorage(offer.id) = offer
case GetOffer(id) => offerStorage.get(id)
case DeleteOffer(id) => offerStorage -= id
()
}
}
val mainInterpreter: UserOfferApp ~> Id = UserOpsInterpreter or OfferOpsInterpreter
ADTs into program
def program(implicit UO: UserOperations[UserOfferApp],
OO: OfferOperations[UserOfferApp]): Free[UserOfferApp, Unit] = {
import UO._, OO._
for {
_ <- addUser(User("uid01", "Alex"))
_ <- addOffer(Offer(1, "uid01", "10% discount", true))
addedOffer <- getOffer(1)
_ <- deleteOffer(1)
deletedOffer <- getOffer(1)
} yield {
println(addedOffer) //Some(Offer(1,uid01,10% discount,true))
println(deletedOffer) //None
}
}
Run a program
import UserOperations._, OfferOperations._
program.foldMap(mainInterpreter)
Thanks
https://github.com/Fruzenshtein/ScalaUA-2019

Scala UA: Big Step To Functional Programming

  • 1.
    Big Step ToFunctional Programming* * - beginners level
  • 2.
    Alex Zvolinskiy Senior ScalaDeveloper at VidIQ Blog: www.Fruzenshtein.com Twitter: @Fruzenshtein LinkedIn, Facebook, GitHub
  • 3.
    Motivation Conner Baker @connerbaker MaunaKea Observatory, Waimea, United States
  • 4.
    Real Life Example UserService addUser getUser Offer Service addOffer getOffer deleteOffer
  • 5.
    Basic Types type UserId= String type Name = String type OfferId = Int type Message = String final case class User(id: UserId, name: Name) final case class Offer(id: OfferId, userId: UserId, message: Message, active: Boolean) type UserStorage = collection.mutable.Map[UserId, User] type OfferStorage = collection.mutable.Map[OfferId, Offer]
  • 6.
    Standard Solution trait UserDAO{ def addUser(user: User): Unit def getUser(id: UserId): Option[User] } class UserDaoImpl(val storage: UserStorage) extends UserDAO { def addUser(user: User): Unit = storage(user.id) = user def getUser(id: UserId): Option[User] = storage.get(id) }
  • 7.
    Standard Solution trait OfferDAO{ def addOffer(offer: Offer): Unit def getOffer(offerId: OfferId): Option[Offer] def deleteOffer(offerId: OfferId): Unit } class OfferDaoImpl(val storage: OfferStorage) extends OfferDAO { override def addOffer(offer: Offer): Unit = storage(offer.id) = offer override def getOffer(offerId: OfferId): Option[Offer] = storage.get(offerId) override def deleteOffer(offerId: OfferId): Unit = storage -= offerId }
  • 8.
    Standard Solution val userDB= mutable.Map.empty[UserId, User] val userDao = new UserDaoImpl(userDB) userDao.addUser(User("uid01", "Alex")) val user = userDao.getUser("uid01") println(user) for { _ <- userDao.addUser(User("uid01", "Alex")) user <- userDao.getUser("uid01") } yield { println(user) } Error: value flatMap is not a member of Unit _ <- userDao.addUser(User("uid01", "Alex"))
  • 9.
    Caution: Cats point[M[_], A](a:A): M[A] flatMap[M[_], A, B](fa: M[A])(f: A => M[B]): M[B]
  • 10.
    “Another” Solution sealed traitUserOps[A] final case class Add(user: User) extends UserOps[Unit] final case class Get(id: UserId) extends UserOps[Option[User]] import cats.free.Free type UserStorage[A] = Free[UserOps, A] import cats.free.Free.liftF def add(user: User): UserStorage[Unit] = liftF[UserOps, Unit](Add(user)) def get(id: UserId): UserStorage[Option[User]] = liftF[UserOps, Option[User]](Get(id)) import cats.{Id, ~>} import scala.collection.mutable def compiler(): UserOps ~> Id = new (UserOps ~> Id) { val userStorage = mutable.Map.empty[UserId, User] override def apply[A](fa: UserOps[A]): Id[A] = fa match { case Add(user) => userStorage(user.id) = user case Get(id) => userStorage.get(id) } }
  • 11.
    “Another” Solution import cats.{Id,~>} import scala.collection.mutable def compiler(): UserOps ~> Id = new (UserOps ~> Id) { val userStorage = mutable.Map.empty[UserId, User] override def apply[A](fa: UserOps[A]): Id[A] = fa match { case Add(user) => userStorage(user.id) = user case Get(id) => userStorage.get(id) } } val computations = for { _ <- add(User("uid01", "Alex")) user <- get("uid01") } yield { println(user) //Some(User(uid01,Alex)) } computations.foldMap(compiler)
  • 12.
    Steps 1. Define ADT 2.Create a Free[_] for the ADT 3. Write a compiler 4. Compose operations in a program 5. Run the program
  • 13.
    What about async? traitUserDAO { implicit val ec: ExecutionContext val storage: UserStorage def addUser(user: User): Future[Unit] def getUser(id: UserId): Future[Option[User]] } class UserDaoImpl(val storage: UserStorage) (implicit val ec: ExecutionContext) extends UserDAO { def addUser(user: User): Future[Unit] = Future(storage(user.id) = user) def getUser(id: UserId): Future[Option[User]] = Future(storage.get(id)) } trait OfferDAO { ... }
  • 14.
    implicit val ec= ExecutionContext.global val userDB = mutable.Map.empty[UserId, User] val userDao = new UserDaoImpl(userDB) val offerDB = mutable.Map.empty[OfferId, Offer] val offerDao = new OfferDaoImpl(offerDB) val result = for { _ <- userDao.addUser(User("uid01", "Alex")) _ <- offerDao.addOffer(Offer(1, "uid01", "10% discount", false)) offerAdded <- offerDao.getOffer(1) _ <- offerDao.deleteOffer(1) offerDeleted <- offerDao.getOffer(1) } yield { println(offerAdded) //Some(Offer(1,uid01,10% discount,false)) println(offerDeleted) //None } Await.result(result, 1.second)
  • 15.
    “Another” Async Solution sealedtrait UserOps[A] final case class Add(user: User) extends UserOps[Unit] final case class Get(id: UserId) extends UserOps[Option[User]] import cats.free.Free type UserStorage[A] = Free[UserOps, A] import cats.free.Free.liftF def add(user: User): UserStorage[Unit] = liftF[UserOps, Unit](Add(user)) def get(id: UserId): UserStorage[Option[User]] = liftF[UserOps, Option[User]](Get(id))
  • 16.
    Different part ofasync Solution import cats.~> import cats.implicits._ import scala.collection.mutable import scala.concurrent.ExecutionContext.Implicits.global def compiler: UserOps ~> Future = new (UserOps ~> Future) { val userStorage = mutable.Map.empty[UserId, User] override def apply[A](fa: UserOps[A]): Future[A] = fa match { case Add(user) => Future(userStorage(user.id) = user) case Get(id) => Future(userStorage.get(id)) } }
  • 17.
    Async Program Run valcomputations = for { _ <- add(User("uid01", "Alex")) user <- get("uid01") } yield { println(user) //Some(User(uid01,Alex)) } import scala.concurrent.duration._ Await.result(computations.foldMap(compiler), 1.second)
  • 18.
    What about composition? sealedtrait UserOps[A] final case class AddUser(user: User) extends UserOps[Unit] final case class GetUser(id: UserId) extends UserOps[Option[User]] sealed trait OfferOps[A] final case class AddOffer(offer: Offer) extends OfferOps[Unit] final case class GetOffer(id: OfferId) extends OfferOps[Option[Offer]] final case class DeleteOffer(id: OfferId) extends OfferOps[Unit] type UserOfferApp[A] = EitherK[UserOps, OfferOps, A]
  • 19.
    Free[_] types forADTs class UserOperations[F[_]](implicit I: InjectK[UserOps, F]) { def addUser(user: User): Free[F, Unit] = Free.inject[UserOps, F](AddUser(user)) def getUser(id: UserId): Free[F, Option[User]] = Free.inject[UserOps, F](GetUser(id)) } object UserOperations { implicit def userOperations[F[_]](implicit I: InjectK[UserOps, F]): UserOperations[F] = new UserOperations[F] } class OfferOperations[F[_]](implicit I: InjectK[OfferOps, F]) { def addOffer(offer: Offer): Free[F, Unit] = Free.inject[OfferOps, F](AddOffer(offer)) def getOffer(id: OfferId): Free[F, Option[Offer]] = Free.inject[OfferOps, F](GetOffer(id)) def deleteOffer(id: OfferId): Free[F, Unit] = Free.inject[OfferOps, F](DeleteOffer(id)) } object OfferOperations { implicit def offerOperations[F[_]](implicit I: InjectK[OfferOps, F]): OfferOperations[F] = new OfferOperations[F] }
  • 20.
    Interpreters object UserOpsInterpreter extends(UserOps ~> Id) { val userStorage = mutable.Map.empty[UserId, User] override def apply[A](fa: UserOps[A]) = fa match { case AddUser(user) => userStorage(user.id) = user case GetUser(id) => userStorage.get(id) } } object OfferOpsInterpreter extends (OfferOps ~> Id) { val offerStorage = mutable.Map.empty[OfferId, Offer] override def apply[A](fa: OfferOps[A]) = fa match { case AddOffer(offer) => offerStorage(offer.id) = offer case GetOffer(id) => offerStorage.get(id) case DeleteOffer(id) => offerStorage -= id () } } val mainInterpreter: UserOfferApp ~> Id = UserOpsInterpreter or OfferOpsInterpreter
  • 21.
    ADTs into program defprogram(implicit UO: UserOperations[UserOfferApp], OO: OfferOperations[UserOfferApp]): Free[UserOfferApp, Unit] = { import UO._, OO._ for { _ <- addUser(User("uid01", "Alex")) _ <- addOffer(Offer(1, "uid01", "10% discount", true)) addedOffer <- getOffer(1) _ <- deleteOffer(1) deletedOffer <- getOffer(1) } yield { println(addedOffer) //Some(Offer(1,uid01,10% discount,true)) println(deletedOffer) //None } }
  • 22.
    Run a program importUserOperations._, OfferOperations._ program.foldMap(mainInterpreter)
  • 23.