ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • 함수형 프로그래밍, 커링, 모노이드, 펑터..
    Development/Architecture 2026. 2. 18. 18:56
    반응형

    함수형 프로그래밍

     순수 함수(Pure function)와 불변 데이터(Immutable data)를 중심으로 프로그래밍을 구성하는 패러다임.

    • 순수 함수 : 같은 입력에 대해서 항상 같은 출력을 반환하고, 외부 상태를 변경하지 않는 함수다.
    • 불변성 : 데이터를 생성한 후에는 변경이 되지 않고, 변경이 필요하면 새로운 데이터를 만든다.
    • 일급 함수(First-Class Function) : 함수를 일반 변수처럼 취급하여 변수 할당, 인자 전달 및 반환값으로 사용 가능하도록 하는 특성이다.
    • 고차 함수(Higher-Order Function) : 함수를 인자로 받거나 함수를 반환하는 함수다.
    • 함수 합성(Function Composition) : 함수들을 조합하여 다양한 함수를 만들 수 있다.
    // 작은 단위 함수들
    val removeSpaces: (String) -> String = { it.replace(" ", "") }
    val toUpperCase: (String) -> String = { it.uppercase() }
    val addPrefix: (String) -> String = { "PREFIX_$it" }
    
    // 합성 유틸리티
    infix fun <A, B, C> ((A) -> B).then(next: (B) -> C): (A) -> C =
        { a -> next(this(a)) }
    
    // 함수 합성
    val processKey = removeSpaces then toUpperCase then addPrefix
    
    println(processKey("hello world"))  // PREFIX_HELLOWORLD

     

     

    커링(Currying) : 다중 인자를 갖는 함수를 단일 인자를 갖는 함수 체인으로 변환하는 방법이다.

    // 커링: 여러 인자를 받는 함수를 한 인자씩 받는 함수 체인으로 변환
    fun <A, B, C> ((A, B) -> C).curried(): (A) -> (B) -> C =
        { a -> { b -> this(a, b) } }
    
    val multiply = { a: Int, b: Int -> a * b }
    val curriedMultiply = multiply.curried()
    
    val double = curriedMultiply(2)   // b만 받으면 되는 함수
    val triple = curriedMultiply(3)
    
    println(double(5))   // 10
    println(triple(5))   // 15

     


    모노이드(Monoid)

    • 아래 세 가지 조건을 만족하면 모노이드다.
      • 결합 법칙(associativity) : (a * b) * c == a * (b * c)
      • 항등원(identity element) : a * e == a, e * a == a
      • 닫힌 연산(closure) : 같은 타입끼리 연산하면 같은 타입이 나옴.
    • 결합 법칙이 성립하기 때문에 데이터를 여러 덩어리로 나누어 각각 계산한 뒤에 마지막에 합쳐도 결과가 동일하다.
      • 안전한 병렬 처리의 추상화가 가능하여 대규모 병렬 처리(Map Reduce), 분산 집계 등의 핵심 원리가 된다.
    / 모노이드 인터페이스
    interface Monoid<T> {
        val empty: T                       // 항등원
        fun combine(a: T, b: T): T        // 결합 연산
    }
    
    // 정수 덧셈 모노이드: 항등원 = 0, 연산 = +
    object IntAddMonoid : Monoid<Int> {
        override val empty: Int = 0
        override fun combine(a: Int, b: Int): Int = a + b
    }
    
    // 문자열 모노이드: 항등원 = "", 연산 = 연결
    object StringMonoid : Monoid<String> {
        override val empty: String = ""
        override fun combine(a: String, b: String): String = a + b
    }
    
    // 리스트 모노이드: 항등원 = emptyList(), 연산 = +
    class ListMonoid<T> : Monoid<List<T>> {
        override val empty: List<T> = emptyList()
        override fun combine(a: List<T>, b: List<T>): List<T> = a + b
    }
    • 교환 법칙은 성립하지 않는다. 위 문자열 모노이드가 그 예시다. ("Hello" 와 "World"는 연산의 순사가 달라지면 결과가 달라진다.)
      • 범주론에서 살펴 보자면, 추상화의 대상이 순서가 중요한 결합까지 모두 포함하기 때문이다.
      • 원소 그 자체를 바라보는 것이 아니라, 객체와 객체를 변환하는 사상(morphism)으로 이해해야 한다.

    펑터(Functor)

    • 펑터는 두 카테고리 사이의 구조를 보존하는 매핑을 의미한다.
    • 두 카테고리 C와 D가 있을 때, 펑터 F : C -> D는 두 가지 매핑으로 구성된다.
      • 대상 매핑 (Object Mapping) : 카테고리 C의 객체 A를 카테고리 D의 객체 F(A)로 보낸다.
      • 사상 매핑 (Morphism Mapping) : 카테고리 C의 사상 f : A -> B 를 카테고리 D의 사상 F(f) : F(A) -> F(B)로 보낸다.
    • 펑터는 다음의 성질을 만족해야 한다.
      • 항등 사상 보존 : F(id_A) = id_F(A)
        • 원래의 카테고리에서 아무것도 안 하는 화살표는 옮겨진 카테고리에서도 아무것도 하지 않아야 한다.
      • 사상 합성 보존 : F(g * f) = F(g) * F(f)
        • f를 가고나서 g를 가는 경로를 한꺼번에 옮긴 것은, 각각을 옮겨서 연결한 것과 같아야 한다.
    // 펑터 인터페이스
    interface Functor<out A> {
        fun <B> map(f: (A) -> B): Functor<B>
    }
    
    // Maybe — null-safe한 값을 담는 컨텍스트
    sealed class Maybe<out A> : Functor<A> {
        data class Just<A>(val value: A) : Maybe<A>()
        data object None : Maybe<Nothing>()
    
        override fun <B> map(f: (A) -> B): Maybe<B> = when (this) {
            is Just -> Just(f(value))    // 값이 있으면 변환
            is None -> None              // 없으면 그대로 None
        }
    }

     

    반응형

    댓글

Designed by Tistory.