Higher-Kinded Type: 타입을 추상화하는 타입 생성자
// Kind-0: 구체적인 타입
val x: Int = 42
val y: String = "hello"
// Kind-1: 타입 생성자 (타입 파라미터 1개)
val list: List[Int] = List(1, 2, 3)
val opt: Option[String] = Some("hello")
// Kind-2: 고차 타입 생성자 (타입 파라미터를 받는 타입 생성자)
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// List, Option 등의 Functor 인스턴스
implicit val listFunctor: Functor[List] = new Functor[List] {
def map[A, B](fa: List[A])(f: A => B): List[B] = fa.map(f)
}
implicit val optionFunctor: Functor[Option] = new Functor[Option] {
def map[A, B](fa: Option[A])(f: A => B): Option[B] = fa.map(f)
}
// Java에는 Higher-Kinded Types가 없음
// 인터페이스로 유사하게 구현 가능하지만 제약적
interface Functor<F> {
<A, B> F map(F fa, Function<A, B> f);
}
// 하지만 F의 타입 파라미터를 표현할 방법이 없음
// Monad 타입 클래스 정의
trait Monad[M[_]] {
def pure[A](a: A): M[A]
def flatMap[A, B](ma: M[A])(f: A => M[B]): M[B]
// map은 flatMap으로 구현 가능
def map[A, B](ma: M[A])(f: A => B): M[B] =
flatMap(ma)(a => pure(f(a)))
}
// Option Monad
implicit val optionMonad: Monad[Option] = new Monad[Option] {
def pure[A](a: A): Option[A] = Some(a)
def flatMap[A, B](ma: Option[A])(f: A => Option[B]): Option[B] = ma.flatMap(f)
}
// List Monad
implicit val listMonad: Monad[List] = new Monad[List] {
def pure[A](a: A): List[A] = List(a)
def flatMap[A, B](ma: List[A])(f: A => List[B]): List[B] = ma.flatMap(f)
}
// Monad를 사용하는 제네릭 함수
def sequence[M[_]: Monad, A](list: List[M[A]]): M[List[A]] = {
val monad = implicitly[Monad[M]]
list.foldRight(monad.pure(List.empty[A])) { (ma, acc) =>
monad.flatMap(ma)(a => monad.map(acc)(a :: _))
}
}
// 사용 예제
val opts = List(Some(1), Some(2), Some(3))
println(sequence(opts)) // Some(List(1, 2, 3))
val optsWithNone = List(Some(1), None, Some(3))
println(sequence(optsWithNone)) // None
// Functor는 다음 법칙을 만족해야 함
trait FunctorLaws[F[_]] {
def functor: Functor[F]
// Law 1: Identity
// map(fa)(x => x) == fa
def identityLaw[A](fa: F[A]): Boolean = {
functor.map(fa)(identity) == fa
}
// Law 2: Composition
// map(map(fa)(f))(g) == map(fa)(f andThen g)
def compositionLaw[A, B, C](fa: F[A], f: A => B, g: B => C): Boolean = {
functor.map(functor.map(fa)(f))(g) == functor.map(fa)(f andThen g)
}
}
// 외부 클래스 인스턴스에 의존하는 내부 타입
class Graph {
class Node(val id: Int) {
def connectTo(other: Node): Unit = {
println(s"Connecting $id to ${other.id}")
}
}
val nodes = scala.collection.mutable.Set[Node]()
def newNode(id: Int): Node = {
val node = new Node(id)
nodes += node
node
}
}
val graph1 = new Graph
val graph2 = new Graph
val node1 = graph1.newNode(1)
val node2 = graph1.newNode(2)
val node3 = graph2.newNode(3)
node1.connectTo(node2) // ✅ 같은 Graph의 Node
// node1.connectTo(node3) // ❌ 컴파일 에러: 다른 Graph의 Node
// Java의 Inner Class와 차이
public class Graph {
class Node {
int id;
Node(int id) { this.id = id; }
void connectTo(Node other) {
// Java는 다른 Graph의 Node도 허용 (타입 안전성 낮음)
}
}
}
// Type Projection: 외부 인스턴스 독립적인 타입
class Outer {
class Inner
}
val outer1 = new Outer
val outer2 = new Outer
// Path-dependent type
val inner1: outer1.Inner = new outer1.Inner
// val inner2: outer1.Inner = new outer2.Inner // ❌ 타입 불일치
// Type projection
val inner3: Outer#Inner = new outer1.Inner
val inner4: Outer#Inner = new outer2.Inner // ✅ 가능
// 데이터베이스 스키마를 타입으로 표현
trait Database {
type Table
type Column
def createTable(name: String): Table
def addColumn(table: Table, name: String): Column
}
class PostgreSQL extends Database {
case class PGTable(name: String)
case class PGColumn(table: PGTable, name: String)
type Table = PGTable
type Column = PGColumn
def createTable(name: String): Table = PGTable(name)
def addColumn(table: Table, name: String): Column = PGColumn(table, name)
}
class MySQL extends Database {
case class MySQLTable(name: String)
case class MySQLColumn(table: MySQLTable, name: String)
type Table = MySQLTable
type Column = MySQLColumn
def createTable(name: String): Table = MySQLTable(name)
def addColumn(table: Table, name: String): Column = MySQLColumn(table, name)
}
// 타입 안전성 보장
val pg = new PostgreSQL
val mysql = new MySQL
val pgTable = pg.createTable("users")
val pgColumn = pg.addColumn(pgTable, "id") // ✅ 가능
// val mixedColumn = mysql.addColumn(pgTable, "id") // ❌ 컴파일 에러
// Self Type: 믹스인될 타입 제약
trait User {
def username: String
}
trait Tweeter {
this: User => // Self type annotation
def tweet(msg: String): Unit = {
println(s"$username tweets: $msg")
}
}
// ✅ User와 함께 믹스인
class TwitterUser(val username: String) extends User with Tweeter
val user = new TwitterUser("alice")
user.tweet("Hello Scala!") // alice tweets: Hello Scala!
// ❌ User 없이 Tweeter만 사용 불가
// class InvalidTweeter extends Tweeter // 컴파일 에러
// 컴포넌트 정의
trait UserRepositoryComponent {
val userRepository: UserRepository
trait UserRepository {
def findById(id: Int): Option[User]
}
}
trait UserServiceComponent {
this: UserRepositoryComponent => // Self type
val userService: UserService
trait UserService {
def getUser(id: Int): Option[String] = {
userRepository.findById(id).map(_.username)
}
}
}
// 구현
trait UserRepositoryComponentImpl extends UserRepositoryComponent {
val userRepository: UserRepository = new UserRepositoryImpl
class UserRepositoryImpl extends UserRepository {
private val db = Map(1 -> User("alice"), 2 -> User("bob"))
def findById(id: Int): Option[User] = db.get(id)
}
}
trait UserServiceComponentImpl extends UserServiceComponent {
this: UserRepositoryComponent =>
val userService: UserService = new UserServiceImpl
class UserServiceImpl extends UserService
}
// 조합
object Application extends UserRepositoryComponentImpl with UserServiceComponentImpl {
def main(args: Array[String]): Unit = {
println(userService.getUser(1)) // Some(alice)
}
}
case class User(username: String)
// Either는 타입 파라미터가 2개
// Either[+A, +B]
// Functor는 타입 파라미터가 1개인 타입 생성자 필요
trait Functor[F[_]] {
def map[A, B](fa: F[A])(f: A => B): F[B]
}
// Either를 Functor로 만들려면?
// implicit val eitherFunctor: Functor[Either] = ??? // ❌ Kind 불일치
// Type Lambda: 타입 생성자를 부분 적용
type StringOr[A] = Either[String, A]
implicit val stringOrFunctor: Functor[StringOr] = new Functor[StringOr] {
def map[A, B](fa: StringOr[A])(f: A => B): StringOr[B] = fa.map(f)
}
// 또는 익명 타입 람다 (Scala 2.12)
implicit def eitherFunctor[E]: Functor[({type L[A] = Either[E, A]})#L] =
new Functor[({type L[A] = Either[E, A]})#L] {
def map[A, B](fa: Either[E, A])(f: A => B): Either[E, B] = fa.map(f)
}
// Scala 3: 더 간결한 Type Lambda 문법
implicit def eitherFunctor[E]: Functor[[A] =>> Either[E, A]] =
new Functor[[A] =>> Either[E, A]] {
def map[A, B](fa: Either[E, A])(f: A => B): Either[E, B] = fa.map(f)
}
// sbt 설정에 추가
// addCompilerPlugin("org.typelevel" % "kind-projector" % "0.13.2" cross CrossVersion.full)
// Kind Projector 문법
implicit def eitherFunctor[E]: Functor[Either[E, *]] =
new Functor[Either[E, *]] {
def map[A, B](fa: Either[E, A])(f: A => B): Either[E, B] = fa.map(f)
}
// Lambda 문법
implicit def eitherFunctor2[E]: Functor[Lambda[A => Either[E, A]]] =
new Functor[Lambda[A => Either[E, A]]] {
def map[A, B](fa: Either[E, A])(f: A => B): Either[E, B] = fa.map(f)
}
// Java의 와일드카드와 유사
def printList(list: List[_]): Unit = {
println(s"List with ${list.size} elements")
}
printList(List(1, 2, 3)) // ✅ List[Int]
printList(List("a", "b")) // ✅ List[String]
printList(List(true, false)) // ✅ List[Boolean]
// Upper bound wildcard
def processAnimals(list: List[_ <: Animal]): Unit = {
list.foreach(animal => println(animal.name))
}
// forSome 키워드 (Scala 2.13에서 deprecated)
type IntList = List[T] forSome { type T <: Int }
// 대신 와일드카드 사용 권장
type IntList2 = List[_ <: Int]
// 다양한 타입의 값을 저장하는 캐시
class TypeSafeCache {
private val cache = scala.collection.mutable.Map[String, Any]()
def put[T](key: String, value: T): Unit = {
cache(key) = value
}
def get[T](key: String): Option[T] = {
cache.get(key).map(_.asInstanceOf[T])
}
}
val cache = new TypeSafeCache
cache.put("count", 42)
cache.put("name", "Alice")
val count: Option[Int] = cache.get[Int]("count")
val name: Option[String] = cache.get[String]("name")
println(count) // Some(42)
println(name) // Some(Alice)
// Free Monad: 순수한 데이터 구조로 프로그램 표현
sealed trait Free[F[_], A]
case class Pure[F[_], A](a: A) extends Free[F, A]
case class Suspend[F[_], A](fa: F[Free[F, A]]) extends Free[F, A]
// Functor 제약
implicit def freeMonad[F[_]: Functor]: Monad[Free[F, *]] = new Monad[Free[F, *]] {
def pure[A](a: A): Free[F, A] = Pure(a)
def flatMap[A, B](fa: Free[F, A])(f: A => Free[F, B]): Free[F, B] = fa match {
case Pure(a) => f(a)
case Suspend(ffa) =>
val functor = implicitly[Functor[F]]
Suspend(functor.map(ffa)(_.flatMap(f)))
}
}
// 인터프리터
def interpret[F[_]: Functor, A](free: Free[F, A], interpreter: F ~> Id): A = free match {
case Pure(a) => a
case Suspend(ffa) =>
val functor = implicitly[Functor[F]]
interpret(functor.map(ffa)(identity), interpreter)
}
// Natural Transformation
trait ~>[F[_], G[_]] {
def apply[A](fa: F[A]): G[A]
}
type Id[A] = A
// Tagless Final: Higher-Kinded Types로 효과 추상화
trait Console[F[_]] {
def putStrLn(line: String): F[Unit]
def getStrLn: F[String]
}
trait FileSystem[F[_]] {
def readFile(path: String): F[String]
def writeFile(path: String, content: String): F[Unit]
}
// 프로그램 정의 (효과 추상화)
def program[F[_]: Monad](implicit C: Console[F], FS: FileSystem[F]): F[Unit] = {
val monad = implicitly[Monad[F]]
for {
_ <- C.putStrLn("Enter filename:")
filename <- C.getStrLn
content <- FS.readFile(filename)
_ <- C.putStrLn(s"File content: $content")
} yield ()
}
// IO Monad 구현
case class IO[A](run: () => A) {
def flatMap[B](f: A => IO[B]): IO[B] = IO(() => f(run()).run())
def map[B](f: A => B): IO[B] = IO(() => f(run()))
}
implicit val ioMonad: Monad[IO] = new Monad[IO] {
def pure[A](a: A): IO[A] = IO(() => a)
def flatMap[A, B](fa: IO[A])(f: A => IO[B]): IO[B] = fa.flatMap(f)
}
// Console 인터프리터
implicit val consoleIO: Console[IO] = new Console[IO] {
def putStrLn(line: String): IO[Unit] = IO(() => println(line))
def getStrLn: IO[String] = IO(() => scala.io.StdIn.readLine())
}
// FileSystem 인터프리터
implicit val fileSystemIO: FileSystem[IO] = new FileSystem[IO] {
def readFile(path: String): IO[String] = IO(() => scala.io.Source.fromFile(path).mkString)
def writeFile(path: String, content: String): IO[Unit] =
IO(() => java.nio.file.Files.writeString(java.nio.file.Paths.get(path), content))
}
// 프로그램 실행
// program[IO].run()
기능 | Scala | Java |
---|---|---|
Higher-Kinded Types | ✅ | ❌ |
Path-Dependent Types | ✅ | ❌ (Inner Class는 있지만 제약적) |
Self Types | ✅ | ❌ |
Type Lambdas | ✅ | ❌ |
Existential Types | ✅ (deprecated) | ✅ (Wildcards) |
extends
: 상속 관계Applicative 타입 클래스를 구현하세요:
trait Applicative[F[_]] extends Functor[F] {
def pure[A](a: A): F[A]
def ap[A, B](ff: F[A => B])(fa: F[A]): F[B]
}
// 인스턴스 구현
implicit val optionApplicative: Applicative[Option] = ???
implicit val listApplicative: Applicative[List] = ???
// 테스트
val f: Option[Int => String] = Some(_.toString)
val x: Option[Int] = Some(42)
assert(optionApplicative.ap(f)(x) == Some("42"))
State Monad를 구현하세요:
case class State[S, A](run: S => (S, A)) {
def map[B](f: A => B): State[S, B] = ???
def flatMap[B](f: A => State[S, B]): State[S, B] = ???
}
object State {
def get[S]: State[S, S] = ???
def put[S](s: S): State[S, Unit] = ???
def modify[S](f: S => S): State[S, Unit] = ???
}
// 테스트: 스택 시뮬레이션
type Stack = List[Int]
val program: State[Stack, Int] = for {
_ <- State.modify[Stack](1 :: _)
_ <- State.modify[Stack](2 :: _)
top <- State.get[Stack].map(_.head)
} yield top
assert(program.run(Nil) == (List(2, 1), 2))
이번 챕터에서 학습한 내용:
다음 챕터 예고: Chapter 12에서는 동시성과 Future를 활용한 비동기 프로그래밍을 학습합니다.
[A] =>> F[A]
(더 간결)[A] => F[A] => G[A]
?=> T
문법