๐ Functional Programming Conference LiftIO 2021
๐ ์ฐ์๋ ์ฐ์ฐ์ผ๋ก ๊พธ๋ ค์ง ๊ฐ๋ฐํ์ ํจ์ํ Scala ํ์ฉ๊ธฐโ
Github Repoโ
package purecoincidence
object HigherLevel extends App {
def factA(n: Int): Int = {
var r = 1
for {
i <- 1 to n
} r *= i
r
}
def factB(n: Int): Int =
if (n <= 1) 1
else n * factB(n - 1)
def factC(n: Int): Int =
(1 to n).product
println((
factA(5),
factB(5),
factC(5)
)) // => (120,120,120)
}
FP ํน์งโ
- ๊ธฐ๊ณ ์ค์ฌ ๊ด์ ๊ณผ ์ฌ๋ ์ค์ฌ ๊ด์ ์ ์ฐจ์ด
- ์์ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๋ ํจ์๋ฅผ ํฉ์ฑํด ํฐ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ๊ธฐ ์ฝ๋ค.
- ์ํ๋ ๋ถ์ํจ๊ณผ์์ด ์์ ํจ์์ ์ ๋ ฅ๊ณผ ์ถ๋ ฅ๋ง์ ๋ค๋ฃฌ๋ค๋ฉด, ๊ทธ๋งํผ ์ถ๋ก ์ด๋ ํ ์คํธ๊ฐ ์ฝ๋ค.
- ์๊ฐ๊ณผ ์ํ๋ฅผ ๊ณ ๋ คํ์ง ์์๋ ๋๋ค.
- ๋ฉํฐ์ค๋ ๋ ์ฒ๋ฆฌ๊ฐ ํจ์ฌ ์ฌ์์ง๋ค.
๊ณ ์ฐจํจ์โ
- ์ด๋ค ํจ์๊ฐ, ๋ค๋ฅธ ํจ์๋ฅผ ์ธ์๋ก ๋ฐ๊ฑฐ๋, ํธ์ถ ๊ฒฐ๊ณผ๋ก ๋ณดํต์ ๊ฐ์ด ์๋ ํจ์๋ฅผ ๋ฐํํ๊ฑฐ๋, ๋๋, ๊ทธ ๋ ๋ค๋ฅผ ํ๋ค.
- ํจ์๋ฅผ ํ๋ฒํ ๊ฐ์ผ๋ก ์ทจ๊ธํ ์ ์์ด์ผ ๊ฐ๋ฅ.
package purecoincidence
object HigherOrderFunction {
def multiple(n: Int): Int => Int =
x => x * n
def pow(r: Int): Int => Int =
x => List.fill(r)(x).product
val triple: Int => Int = multiple(3)
val square: Int => Int = pow(2)
def main(args: Array[String]) =
println((
List(1, 2, 3, 4).map(triple compose square),
List(1, 2, 3, 4).map(square).map(triple)
)) // => List(3, 12, 27, 48), ...
}
์์ ํจ์โ
๊ฐ์ ์ธ์๋ฅผ ๋๊ฒจ ํธ์ถํ๋ฉด, ์ธ์ ๋ ๊ฒฐ๊ณผ๊ฐ์ด ๊ฐ๊ฒ ๋์จ๋ค.
ํจ์๋ฅผ ํธ์ถํด๋, ๋ณ๋์ ๋ถ์ ํจ๊ณผ๊ฐ ๋ฐ์ํ์ง ์๋๋ค.
- Impure
// Impure ํ๋ค.
package purecoincidence
object Impurity extends App {
var current: Int = 0
def impure(n: Int): Int = {
current += n
current
}
val i = impure(2)
println((
i + i,
impure(2) + i,
impure(2) + impure(2)
)) // (4,6,14)
}
- pure
package purecoincidence
object Purity extends App {
def pure(a: Int, b: Int): Int =
2*a*a + 3*b // 2a^2 + 3b
val p = pure(2, 3) // => 17
println((
p + p,
pure(2, 3) + p,
pure(2, 3) + pure(2, 3),
)) // => (34,34,34)
}
package purecoincidence
object PurelyLong extends App {
def pure(a: Int, b: Int): Int =
2*a*a + 3*b // 2a^2 + 3b
def expressionA(a: Int, b: Int): Int =
4*a*a + pure(a, b) + 6*b
def expressionB(a: Int, b: Int): Int =
3 * pure(a, b)
println((
expressionA(3, 4),
expressionB(3, 4)
)) // => (90,90)
}
(๋ชจ๋ ธ์ด๋)monoidโ
- empty, combine
- ๊ฒฐํฉ๋ฒ์น, ํญ๋ฑ์
์ต์ ๋ชจ๋๋โ
- pure, flatMap
- ์ต์ ๋ชจ๋๋ - ๊ฐ ์ ๋ฌด, ์๋ฌ ์ ๋ฌด๋ฅผ ๋ค๋ฃจ๊ธฐ ๋ฌด์์ด ํธํ๊ฐ.
FP ์ค๋ฌด์์ ์ฐ๋ ค ํ๋ค๋ฉด... ์ด๋ ต์ฃ โ
์์ฌ, ๋๋ฃ๋ฅผ ์ค๋, ์ดํด๊ด๊ณ์ ๋ค์๊ฐ FP์ ๊ด์ฌ ์๊ธฐ๋ ์ด๋ ต์ง ์๊ฒ ๋? ์ด๋ฏธ ์ ๊ฐ์ถฐ์ง ๊ธฐ์กด ํ๋ก์ ํธ์ ํฐ ๋ณํ๋ฅผ ๊ฐ์ ธ์ค๊ธฐ๋ ์ด๋ ต๊ณ , ํฐ ๊ท๋ชจ์ ํ์ฌ์์ ์ ๊ท ํ๋ก ์ ํธ๋ฅผ ๋งก๋ ๊ฒ์ ์ฝ์ง ์์ ์ผ.
์ค์นผ๋ผ ๋ผ์ด๋ธ๋ฌ๋ฆฌโ
๋ช
๋ นํ๊ณผ ํจ์ํ์ ํผํฉํด์ ์ฌ์ฉํ ์ ์๋ค. Cats
ํจ์ํ ์น ์๋น์ค ๋ผ์ด๋ธ๋ฌ๋ฆฌ http4s
JSON์ circe (ํจ์๋ฅผ ์กฐ๋ฆฝํด์ ์ธ์ฝ๋ ๋์ฝ๋๋ฅผ ์ฌ์ฉํ ์ ์์)
doobie cats๊ธฐ๋ฐ์ผ๋ก ๋ง๋ RDB ์ ๊ทผ (Connection IO ๋ชจ๋๋ ์ฌ์ฉ)
๋น์ ์ ์ ๋ฌด์ FP ๋์ โ
๋ช ๋ นํ ์ธ์ด์์ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ ๊ฐ๋ฅโ
ํจ์๋ก ๊ฐ์ผ๋ก ๋ค๋ฃจ๊ธฐ, ๊ณ ์ฐจํจ์ ํ์ฉ. ํจ์ ์กฐํฉ vavr.io (functional library for Java) ํจ๊ณผ(effect)๋ฅผ ์ผ์ผํค๋ ํ๋ก์์ ธ์ ์์ํจ์๋ฅผ ๋ถ๋ฆฌ
ํจ์ํ ์ธ์ด ๋ฅผ ์ง์ ์ ์ผ๋ก ์ฐ๊ธฐ ์ํ ์กฐ๊ฑดโ
- ์ง์์ ์ธ ๊ด์ฌ๊ณผ ๋ฐ๋จ
- ๋ด๊ฒ ๊ทธ๋ฅ ์ค์์น ์์ ๊ฑธ๋ค์ ํฌ๊ธฐํ ์ฉ๊ธฐ
- ์ถฉ๋ถํ ์์ ๊ฐ
- ๊ฐ์ธ ํ๋ก์ ํธ๋ก ์ ํ ํ๋ จ
- ์ค๋ฌด ํ๋ก์ ํธ์ ๋จ์ ํ ์คํธ ์ฝ๋์ ์ ์ง์ ๋ฐ์ํด์ ๊ฒฝํ์น
ํ์์ ์ณ๋ฐ๋ฅธ ์ถ๋ฐ์ โ
- ์ ํฌ ํ๋ก์ ํธ์ ํจ์ํ ํ๋ก๊ทธ๋๋ฐ์ ๋์
ํ๋ฉด ์ด๋จ๊น์? (์ ๋ X)
- ์๋ก์ด ์๋์ ๊ฐ์น๋ฅผ ์ฆ๋ช ํด์ผ ํ๊ณ , ํ๋ ๋ณํ๋ฅผ ๋ฆฌ๋ํด์ผ ํ๋ค.
- ๊ทธ๋ฅ ์กฐ์ฉํ ๋ฌต๋ฌตํ ์ ์ง๋ฅธ๋ค. ๋๋, ์ ์ฉํ๊ณ ๋์ ํต๋ณด
- ์ง๋ฅด๊ณ ๋์, ์ ์๋๋ ์ง๋ฅผ ๋ฌผ์ด๋ณด์.
- ๋น๋นํ์ง ๋ชป ํ ์ด์ ๊ฐ ์๋ค.
๐ ํจ์ํ ๋๋ฉ์ธ ์ฃผ๋ ์ค๊ณ ๊ตฌํโ
- ์์ : ๊ฒฐ์ ์์คํ ๋ ๊ฐ์ง ํ ์ธ ์ ํ ๋น์จ ํผ์ผํธ ์ ์ก ํ ์ธ
Table Driven Design์ ๋ฌธ์ โ
๋์ ๋ณต์ก๋ ์ธ์ง ๋ถํ๊ฐ ๋์ด๋จ
enum DiscountType:
case Percentage
case FixedAmount
class Discount(
var discountType: DiscountType,
var percentage: Option[Int],
var fixedAmount: Option[BigDecimal],
var fixedAmountCurrency: Option[Currency]
)
C(Discount) = C(discountType) * C(percentage) *
C(fixedAmount) * C(fixedAmountCurrency)
= C(DiscountType) C(Option[Int]) *
C(Option[BigDecimal]) * C(Option[Currency])
= 2 * 2 * 2 * 2
= 16
// Discount๊ฐ ๊ฐ์ง ์ ์๋ ๊ฒฝ์ฐ์ ์๋ 16๊ฐ
- ์๋ชป๋ ๊ฐ์ ์ ๊ทผ ๊ฐ๋ฅ
- ์ ์ง๋ณด์ํ ๋ ์๊ธฐ๋ ์ผ
- ํน์ ํ ์ธ ์ ํ์ ์๋ก์ด ํ๋
- ๊ณ์ฐ ๊ณผ์ ์ ๋ฐ์ํ๋๊ฑธ ๊น๋จน์๋ค
- ์๋ก์ด ํ ์ธ ์ ํ ์ถ๊ฐ
- else if ๋ฌธ ์ถ๊ฐ.
- ํน์ ํ ์ธ ์ ํ์ ์๋ก์ด ํ๋
๋์์ ์๋ฃํ (ADT)โ
- ADT๋ฅผ ์ด์ฉํ ๋๋ฉ์ธ ๋ชจ๋ธ๋ง
enum Discount:
case Percentage(percent: Int)
case Fixed(amount: BigDecimal, currency: Currency)
ํ ์ธ
- ๋น์จ: ํผ์ผํธ(์ ์)
- ์ ์ก: ์ก์(์ค์), ํตํ(ํตํ์ฝ๋)
def discount(subtotal: Money, d: Discount): Money =
d match {
case Percentage(percent) => // amount ์ ๊ทผ ๋ถ๊ฐ
case Fixed(amount, currency) => // percent ์ ๊ทผ ๋ถ๊ฐ
}
ADT๋ก ๋ชจ๋ธ๋ง ์ ์ฅ์
- ์ ํจํ ์ํ๋ง ํฌํ ๊ฐ๋ฅ
- ๋ณต์ก๋ ๊ฐ์
ADT ์ผ์ด์ค์ ๋ณต์ก๋
- ๋ ๊ฐ์ง ๊ฒฝ์ฐ์๋ง ์ ๊ฒฝ์ ์ธ ์ ์๋ค.
enum Discount:
case Percentage(percent: Int)
case Fixed(amount: BigDecimal, currency: Currency)
C(Discount) = C(Percentage) + C(Fixed)
= C(percent) + (C(amount) * C(currency))
= C(Int) + (C(BigDecimal) * C(Currency))
= 1 + (1 * 1)
= 2
ํ ์ด๋ธ ์ฃผ๋ VS. ํจ์ํ
- ํ ์ด๋ธ ์ฃผ๋
enum DiscountType:
case Percentage
case FixedAmount
class Discount(
var discountType: DiscountType,
var percentage: Option[Int],
var fixedAmount: Option[BigDecimal],
var fixedAmountCurrency: Option[Currency]
)
- ํจ์ํ
enum Discount:
case Percentage(percent: Int)
case Fixed(amount: BigDecimal, currency: Currency)
EITHER ํ์ ์ ์ด์ฉํ ๋๋ฉ์ธ ์์ธ ์ฒ๋ฆฌ
Either ADT
enum Either[+A, +B]:
case Left(value: A)
case Right(value: B)
๋ฌ์คํธ์ Result๋ ๊ฐ์ ๊ตฌ์กฐ
pub enum Result<T, E> {
Ok(T),
Err(E),
}
EITHER ํ์ ์ ์ฐํธํฅ
case class LengthError()
def oddLength(str: String): Either[LengthError, Int] =
str.length match
case n if n % 2 == 0 => Right(n)
case n => Left(LengthError())
var result =
for
s <- oddLength("scala") // Right(5)
h <- oddLength("haskell") // Right(7)
yield s + h
assert(result == Right(12))
var result =
for
s <- oddLength("scala") // Right(5)
j <- oddLength("java") // Left(LengthError())
h <- oddLength("haskell") // ํ๊ฐํ์ง ์์
yield s + h
assert(result == Left(LengthError()))
๋ค ์ข์๋ฐ ์ค๋ฌด์์ ์ธ ์ ์๋?
- ์ค๋ฅ DB ์ค์ ๊ด๋ฆฌ ์์กด์ฑ ๊ด๋ฆฌ
- ์ด๊ฒ๋ค์ด ๊ฐ๋ฅํ๊ฐ?
์์ ํจ์ํ ํ๋ก๊ทธ๋จ์ ํน์งโ
์ ์ฐจ์
- ๋ถ๋ถ์ : ๋ชจ๋ ์ ๋ ฅ์ ์ฒ์งํ์ง ์์(์์ธ)
- ๋น๊ฒฐ์ ์ : ๊ฐ์ ์ ๋ ฅ, ๋ค๋ฅธ ์ถ๋ ฅ
- ๋น์์: ๋ถ์ํจ๊ณผ, ์คํ ์ค ๊ฐ์ด ๋ณํจ
ํจ์ํ
- ์ ์ฒด์ : ๋ชจ๋ ์ ๋ ฅ์ ๋ํ ์ถ๋ ฅ์ด ์กด์ฌํจ
- ๊ฒฐ์ ์ : ์ ๋ ฅ๊ฐ์ด ๊ฐ์ผ๋ฉด ์ถ๋ ฅ๊ฐ๋ ๊ฐ์
- ์์ํจ: ๋ถ์ํจ๊ณผ๊ฐ ์์
์์ ํจ์ํ ์ดํํธ๊ฐ ๊ฐ๋ฅํ๊ฐ์? ๊ฐ๋ฅ: ํจ์ํ ์ดํํธ ๋์์๋ฆฌ
ํ๋ก๊ทธ๋จ์ ๋ช ์ธ์ ์คํ์ ๋ถ๋ฆฌํ๋ค. ํจ์๋ฅผ ์ด์ฉํด ์์ ํ๋ก๊ทธ๋จ์ ํฐ ํ๋ก๊ทธ๋จ์ผ๋ก ์กฐ๋ฆฝํ๋ค.
๋น์ทํ ์ฌ๋ก
- ํ์ด์ฌ ์คํฌ๋ฆฝํธ์ ์ธํฐํ๋ฆฌํฐ
- Abstract Syntax Tree(AST)
- Continuation-Passing Style (CPS)
์: console ADT
enum Console[+A]:
case Return(value: () => A)
case PrintLine(line: String, rest: Console[A])
case ReadLine(rest: String => Console[A])
console (CONT.1) ํ๋ก๊ทธ๋จ
val example1 = Console[Unit] =
PrintLine("์๋
ํ์ธ์, ์ด๋ฆ์ด ๋ฌด์์ธ๊ฐ์?",
ReadLine(name =>
PrintLine(s"${name}๋, ๋ฐ๊ฐ์์.",
Return(() => ()))))
console (count.2)
์ธํฐํ๋ฆฌํฐ
ํจํด ๋งค์นญ์ ํตํด ์คํ
def interpret[A](program: Console[A]): A = program match {
case Return(value) =>
value()
case PrintLine(line, next) =>
println(line)
interpret(next)
case ReadLine(next) =>
interpret(next(scala.io.StdIn.readLine()))
}
ZIOโ
- ์์ ํจ์ํ ์ดํํธ
- ๊ฐ๋ ฅํ ํ์ ์์ ์ฑ
- ๋์์ฑ, ์คํธ๋ฆฌ๋ฐ
- ํ๋ฐํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ์ํ๊ณ์ ์ปค๋ฎค๋ํฐ
- ์ค์นผ๋ผ์ Spring
- ์ค์นผ๋ผ์ ๊ฐ์ฒด์งํฅ์ ์ธ ๋ฉด ํ์ฉ
- Cats๋ฑ ๋ค๋ฅธ ํจ์ํ ๋ผ์ด๋ธ๋ฌ๋ฆฌ ํธํ
์ค๋ฅ ์ฒ๋ฆฌโ
def findUserByName(name: String): IO[DBError, Option[user]]
def pureValidation(user: User): Either[DomainError, Unit]
def flakyApiCall(x: Int): IO[NetworkError, Remote]
def prog: ZIO[Has[Clock], AppError, Remote] =
for
user <- findUserByName("guersam")
.catchAll(e => IO.fail(AppError.Unexpected(e)))
.someOrFail(AppError.NotFound("User not found"))
_ <- IO.from(pureValidation(user))
.mapError(e => AppError.FromDomainError(e.msg))
๊ฐ์ฒด์งํฅ ์ค๊ณ ๊ธฐ๋ฒ ์ฌ์ฌ์ฉโ
- FP in small, OOP in large
- ์ธํฐํ์ด์ค์ ๊ตฌํ ๋ถ๋ฆฌ
trait UserRepo:
def getById(id: UserId): IO[DBError, User]
case class UserRepoLive(log: Logging, db: Database)
extends UserRepo:
// ...
case class TestUserLive(ref: Ref[Map[UserId, User]])
extends UserRepo:
// ...
์ ๋ฆฌโ
- ๋์์ ์๋ฃํ์ ์ด์ฉํ ๋๋ฉ์ธ ๋ชจ๋ธ๋ง
- ํ ์ด๋ธ ์ฃผ๋ ์ค๊ณ์ ํจ์ํ ์ค๊ณ
- ํต์ ๊ฐ๋ฅํ ๋ชจ๋ธ ๋ณต์ก๋ ๊ด๋ฆฌ
- ZIO๋ก ์ค์ ๋ก ์ ์ฉํ ํ๋ก๊ทธ๋จ ๋ง๋ค๊ธฐ
- ํจ์ํ ์ดํํธ ์์คํ
- ๋ช ์ธ์ ์คํ์ ๋ถ๋ฆฌ
- ZIO
- ์ค๋ฅ ์ฒ๋ฆฌ
- ์์กด์ฑ ๊ด๋ฆฌ
- ํจ์ํ ์ดํํธ ์์คํ