상위 질문
타임라인
채팅
관점

모나드 (함수형 프로그래밍)

위키백과, 무료 백과사전

Remove ads

함수형 프로그래밍에서 모나드(monad)는 계산을 일련의 단계로 구성하는 방법으로, 각 단계는 값뿐만 아니라 잠재적 실패, 비결정성 또는 부작용과 같은 계산에 대한 추가 정보를 생성한다. 더 형식적으로, 모나드는 두 가지 연산을 갖춘 형식 생성자 M이다. return : <A>(a : A) -> M(A)는 값을 모나드 문맥으로 올리고, bind : <A,B>(m_a : M(A), f : A -> M(B)) -> M(B)는 모나드 계산을 연결한다. 간단히 말해, 모나드는 모나드를 구현하는 다양한 형식 생성자 변형(예: Option, List 등)에 대해 함수가 추상화할 수 있도록 하는 인터페이스로 생각할 수 있다.[1][2]

모나드의 개념과 용어는 원래 범주론에서 유래했으며, 여기서 모나드는 추가 구조를 갖춘 자기 함자로 정의된다.[a][b] 1980년대 후반과 1990년대 초반에 시작된 연구는 모나드가 겉보기에는 서로 다른 컴퓨터 과학 문제를 통일된 함수형 모델로 통합할 수 있음을 입증했다. 범주론은 또한 모나드 법칙으로 알려진 몇 가지 형식적 요구 사항을 제공하며, 이는 모든 모나드에 의해 충족되어야 하며 모나드 코드를 검증하는 데 사용될 수 있다.[3][4]

모나드는 특정 종류의 계산에 대한 의미론을 명시적으로 만들기 때문에 편리한 언어 기능을 구현하는 데 사용될 수도 있다. 하스켈과 같은 일부 언어는 일반적인 모나드 구조와 공통 인스턴스를 위한 사전 빌드된 정의를 핵심 라이브러리에 제공하기도 한다.[1][5]

Remove ads

개요

요약
관점

"모나드 m의 경우, 타입 m a의 값은 모나드의 맥락 내에서 타입 a의 값에 접근할 수 있음을 나타낸다." — C. A. McCann[6]

더 정확히 말하면, 모나드는 시나리오에 특정한 이유로 값에 대한 무제한적인 접근이 부적절한 곳에서 사용될 수 있다. Maybe 모나드의 경우, 값이 존재하지 않을 수 있기 때문이다. IO 모나드의 경우, 값이 아직 알려지지 않았을 수 있기 때문인데, 예를 들어 모나드가 프롬프트가 표시된 후에만 제공될 사용자 입력을 나타낼 때 그렇다. 모든 경우에 접근이 의미 있는 시나리오는 모나드에 대해 정의된 bind 연산에 의해 포착된다. Maybe 모나드의 경우 값이 존재할 때만 바인딩되고, IO 모나드의 경우 시퀀스의 이전 연산이 수행된 후에만 바인딩된다.

모나드는 형식 생성자 M과 두 가지 연산을 정의하여 생성할 수 있다.

  • return :: a -> M a (종종 unit이라고도 함)는 타입 a의 값을 받아 모나드 값인 타입 M a로 래핑한다.
  • bind :: (M a) -> (a -> M b) -> (M b) (일반적으로 >>=로 표현됨)는 타입 M a의 모나드 값과 기본 타입 a의 값을 받는 함수 f를 받는다. Bind는 M a를 언래핑하고, f를 적용한 후, f의 결과를 모나드 값 M b로 처리할 수 있다.

(bind 연산자 대신 join 함수를 사용하는 대안적이지만 동등한 구성은 나중에 § Derivation from functors 섹션에서 찾을 수 있다.)

이러한 요소들을 사용하여 프로그래머는 여러 bind 연산자를 식으로 연결하여 함수 호출 시퀀스("파이프라인")를 구성한다. 각 함수 호출은 입력 일반형 값을 변환하고, bind 연산자는 반환된 모나드 값을 처리하여 시퀀스의 다음 단계로 공급한다.

일반적으로 바인드 연산자 >>=는 매개변수로 받은 함수에는 없는 추가 계산 단계를 수행하는 모나드 고유의 코드를 포함할 수 있다. 구성된 함수 호출 쌍 사이에서 바인드 연산자는 함수 f 내에서 접근할 수 없는 추가 정보를 모나드 값 m a에 주입하고 파이프라인을 따라 전달할 수 있다. 또한 함수를 특정 조건에서만 호출하거나 함수 호출을 특정 순서로 실행하는 등 실행 흐름을 더욱 세밀하게 제어할 수도 있다.

예시: Maybe

모나드의 한 가지 예는 Maybe 타입이다. 정의되지 않은 null 결과는 많은 절차적 언어가 특별한 도구를 제공하지 않는 특정 문제점으로, 널 오브젝트 패턴을 사용하거나 각 연산에서 유효하지 않은 값에 대한 검사를 통해 정의되지 않은 값을 처리해야 한다. 이로 인해 버그가 발생하고 오류를 우아하게 처리하는 견고한 소프트웨어를 구축하기가 더 어려워진다. Maybe 타입은 프로그래머가 결과의 두 상태를 명시적으로 정의하여 이러한 잠재적으로 정의되지 않은 결과를 처리하도록 강제한다. 즉, Just ⌑result⌑ 또는 Nothing이다. 예를 들어, 프로그래머는 중간 결과를 반환하거나, 파서가 감지한 조건을 알리고 프로그래머가 처리해야 하는 파서를 구축할 수 있다. 약간의 함수적 양념을 추가하면 이 Maybe 타입은 완전한 기능을 갖춘 모나드로 변환된다.[c]:12.3 pages 148–151

대부분의 언어에서 Maybe 모나드는 옵션 타입으로도 알려져 있는데, 이는 값이 포함되어 있는지 여부를 표시하는 타입일 뿐이다. 일반적으로 열거형의 한 형태로 표현된다. 러스트 프로그래밍 언어에서는 Option<T>라고 불리며, 이 타입의 변형은 제네릭 타입 T의 값이거나 빈 변형인 None일 수 있다.

// <T>는 제네릭 타입 "T"를 나타낸다.
enum Option<T> {
    Some(T),
    None,
}

Option<T>는 "래핑" 타입으로도 이해될 수 있으며, 이것이 모나드와의 연관성이 나타나는 지점이다. Maybe 타입을 사용하는 언어에서는 모나드 함수를 서로 구성하고 Maybe가 값을 포함하는지 여부를 테스트하는 데 도움이 되는 함수가 있다.

다음 하드코딩된 예제에서는 Maybe 타입이 실패할 수 있는 함수의 결과로 사용된다. 이 경우 0으로 나누기가 있으면 아무것도 반환하지 않는다.

fn divide(x: Decimal, y: Decimal) -> Option<Decimal> {
    if y == 0 { return None }
    else { return Some(x / y) }
}
// divide(1.0, 4.0) -> Some(0.25)를 반환
// divide(3.0, 0.0) -> None을 반환

Maybe가 값을 포함하는지 여부를 테스트하는 한 가지 방법은 if 문을 사용하는 것이다.

let m_x = divide(3.14, 0.0); // 위에 divide 함수를 참조
// m_x가 Maybe의 Just 변형이면 if 문이 m_x에서 x를 추출한다.
if let Some(x) = m_x {
    println!("answer: ", x)
} else {
    println!("division failed, divide by zero error...")
}

다른 언어는 패턴 매칭을 가질 수 있다.

let result = divide(3.0, 2.0);
match result {
    Some(x) => println!("Answer: ", x),
    None => println!("division failed; we'll get 'em next time."),
}

모나드는 Maybe를 반환하는 함수들을 결합하여 함께 사용할 수 있다. 구체적인 예시에서는 여러 Maybe 매개변수를 받아서, 다음 예시와 같이 어떤 매개변수라도 Nothing일 경우 그 값이 Nothing인 단일 Maybe를 반환하는 함수를 가질 수 있다.

fn chainable_division(maybe_x: Option<Decimal>, maybe_y: Option<Decimal>) -> Option<Decimal> {
    match (maybe_x, maybe_y) {
        (Some(x), Some(y)) => { // 두 입력 모두 Some이면 0으로 나누는지 확인하고 그에 따라 나눈다.
            if y == 0 { return None }
            else { return Some(x / y) }
        },
        _ => return None // 그렇지 않으면 None을 반환
    }
}
chainable_division(chainable_division(Some(2.0), Some(0.0)), Some(1.0)); // 내부 chainable_division 실패, 외부 chainable_division None 반환

Some 표현식을 반복하는 대신 바인드 연산자라고 불리는 것을 사용할 수 있다. (또한 "map", "flatmap", 또는 "shove"라고도 알려져 있다.[8]:2205s). 이 연산은 모나드와 모나드를 반환하는 함수를 받아서, 전달된 모나드의 내부 값에 함수를 실행하고, 함수에서 모나드를 반환한다.

// ".map"을 사용하는 러스트 예시. maybe_x는 각각 Decimal 및 String을 반환하는 2개의 함수를 통해 전달된다.
// 일반적인 함수 합성에서와 같이 서로에게 공급되는 함수의 입력과 출력은 래핑된 타입과 일치해야 한다. (즉, add_one 함수는 Decimal을 반환해야 하며, 이는 decimal_to_string 함수로 전달될 수 있다.)
let maybe_x: Option<Decimal> = Some(1.0)
let maybe_result = maybe_x.map(add_one).map(decimal_to_string)

하스켈에서는 바인드 연산자 또는 (>>=)가 함수의 합성과 유사한 보다 우아한 형태로 이러한 모나드 합성을 가능하게 한다.[d]:150–151

halve :: Int -> Maybe Int
halve x
  | even x = Just (x `div` 2)
  | odd x  = Nothing
 -- 이 코드는 x를 두 번 나눈다. x가 4의 배수가 아니면 Nothing으로 평가된다.
halve x >>= halve

>>=를 사용할 수 있게 되면서, chainable_division익명 함수(즉, 람다)의 도움으로 훨씬 더 간결하게 표현될 수 있다. 아래 표현식에서 두 개의 중첩된 람다가 바인드 연산자를 사용하여 전달된 Maybe 모나드 안의 래핑된 값에서 각각 어떻게 작동하는지 주목하라.[e]:93

 chainable_division(mx,my) =   mx >>=  ( λx ->   my >>= (λy -> Just (x / y))   )

지금까지 보여준 것은 기본적으로 모나드이지만, 더 간결하게 말하면, 다음은 다음 섹션에서 정의된 모나드에 필요한 엄격한 속성 목록이다.

모나드 타입
타입 (Maybe)[c]:148–151
단위 연산
타입 변환기 (Just(x))[e]:93
바인드 연산
모나드 함수를 위한 조합기 (>>= 또는 .flatMap())[d]:150–151

이것들이 모나드를 구성하는 데 필요한 세 가지 요소이다. 다른 모나드는 다른 논리적 프로세스를 구현할 수 있으며, 일부는 추가 속성을 가질 수도 있지만, 이 세 가지 유사한 구성 요소를 모두 가질 것이다.[1][9]

정의

위 예제에서 사용된 함수형 프로그래밍에서의 모나드에 대한 보다 일반적인 정의는 사실 범주론의 표준 정의가 아닌 클라이슬리 삼중체 ⟨T, η, μ⟩에 기반한다. 그러나 두 구성은 수학적으로 동등한 것으로 판명되었으므로 어떤 정의든 유효한 모나드를 산출할 것이다. 모든 잘 정의된 기본 타입 TU가 주어졌을 때, 모나드는 세 부분으로 구성된다.

  • 모나드 타입 M T를 구성하는 형식 생성자 M[f]
  • unit 또는 return이라고도 불리는 형 변환기는 객체 x를 모나드에 내장한다.
    unit : T → M T[g]
  • 일반적으로 bind (변수를 바인딩하는 것처럼)라고 불리고 중위 연산자 >>= 또는 flatMap이라는 메서드로 표현되는 조합기는 모나드 변수를 풀고, 이를 모나드 함수/표현식에 삽입하여 새로운 모나드 값을 생성한다.
    (>>=) : (M T, T → M U) → M U[h] 따라서 ma : M T이고 f : T → M U이면, (ma >>= f) : M U

그러나 모나드로 완전히 자격을 갖추려면 이 세 부분이 몇 가지 법칙을 준수해야 한다.

  • unitbind왼쪽 항등원이다.
    unit(x) >>= f f(x)
  • unit은 또한 bind의 오른쪽 항등원이다.
    ma >>= unit ma
  • bind는 본질적으로 결합적이다.[i]
    ma >>= λx → (f(x) >>= g) (ma >>= f) >>= g[1]

대수적으로, 이는 모든 모나드가 클라이슬리 범주라고 불리는 범주와 함자(값에서 계산으로) 범주의 모노이드를 생성하며, 모나드 합성이 모노이드의 이항 연산자[8]:2450s이고 unit이 모노이드의 항등원임을 의미한다.

사용법

모나드 패턴의 가치는 단순히 코드를 압축하고 수학적 추론에 대한 링크를 제공하는 것을 넘어선다. 개발자가 어떤 언어나 기본 프로그래밍 패러다임을 사용하든, 모나드 패턴을 따르면 순수 함수형 프로그래밍의 많은 이점을 얻을 수 있다. 특정 종류의 계산을 재화함으로써, 모나드는 그 계산 패턴의 지루한 세부 사항을 캡슐화할 뿐만 아니라, 선언적 방식으로 이를 수행하여 코드의 명확성을 향상시킨다. 모나드 값은 계산된 값뿐만 아니라 계산된 효과도 명시적으로 나타내기 때문에, 순수 표현식처럼 모나드 표현식은 참조 투명한 위치에서 그 값으로 대체될 수 있으며, 재작성에 기반한 많은 기술과 최적화를 가능하게 한다.[4]

일반적으로 프로그래머는 bind를 사용하여 모나드 함수를 시퀀스로 연결하며, 이로 인해 일부는 모나드를 "프로그래밍 가능한 세미콜론"이라고 묘사하기도 하는데, 이는 많은 명령형 프로그래밍 언어가 세미콜론을 사용하여 명령문을 구분하는 방식을 참조한다.[1][5] 그러나 모나드는 실제로 계산 순서를 정하지 않는다. 심지어 모나드를 핵심 기능으로 사용하는 언어에서도 더 간단한 함수 합성을 통해 프로그램 내의 단계를 정렬할 수 있다. 모나드의 일반적인 유용성은 추상화를 통해 프로그램의 구조를 단순화하고 관심사 분리를 개선하는 데 있다.[4][11]

모나드 구조는 또한 컴파일 타임데코레이터 패턴에 대한 고유한 수학적 변형으로 볼 수 있다. 일부 모나드는 함수에 접근할 수 없는 추가 데이터를 전달할 수 있으며, 일부는 특정 조건에서만 함수를 호출하는 등 실행에 대한 더 세밀한 제어를 행사하기도 한다. 애플리케이션 프로그래머가 도메인 논리를 구현하면서 상용구 코드를 미리 개발된 모듈로 오프로드할 수 있게 해주기 때문에, 모나드는 관점 지향 프로그래밍을 위한 도구로도 간주될 수 있다.[12]

모나드의 또 다른 주목할 만한 용도는 입출력 또는 가변 상태와 같은 부작용을 순수 함수형 코드에서 격리하는 것이다. 순수 함수형 언어도 모나드 없이도 이러한 "불순한" 계산을 구현할 수 있는데, 특히 복잡한 함수 합성 및 연속 전달 스타일 (CPS)의 조합을 통해서 가능하다.[2] 그러나 모나드를 사용하면 이러한 많은 스캐폴딩을 추상화할 수 있으며, 본질적으로 CPS 코드의 각 반복 패턴을 별개의 모나드로 묶는 방식이다.[4]

언어가 기본적으로 모나드를 지원하지 않더라도 패턴을 구현하는 것은 여전히 가능하며, 종종 큰 어려움 없이 구현할 수 있다. 범주론에서 프로그래밍 용어로 번역될 때, 모나드 구조는 제네릭 개념이며 제한된 다형성에 상응하는 기능을 지원하는 모든 언어에서 직접 정의할 수 있다. 개념이 기본 타입에서 작동하면서도 운영 세부 사항에 대해 불가지론적일 수 있는 능력은 강력하지만, 모나드의 고유한 기능과 엄격한 동작은 다른 개념과 구별된다.[13]

Remove ads

응용

특정 모나드에 대한 논의는 주어진 모나드가 특정 계산 형식을 나타내므로 좁은 구현 문제를 해결하는 데 중점을 둔다. 그러나 일부 상황에서는 애플리케이션이 핵심 논리 내에서 적절한 모나드를 사용하여 고수준 목표를 달성할 수도 있다.

다음은 디자인의 핵심에 모나드를 사용하는 몇 가지 응용 프로그램이다.

  • 파섹 파서 라이브러리는 모나드를 사용하여 간단한 구문 분석 규칙을 더 복잡한 규칙으로 결합하며, 특히 작은 도메인 특화 언어에 유용하다.[14]
  • xmonad지퍼 자료 구조를 중심으로 하는 타일링 창 관리자이며, 지퍼 자료 구조 자체는 구분된 연속체의 특정 경우로 모나드적으로 처리될 수 있다.[15]
  • 마이크로소프트LINQ는 함수형 프로그래밍 개념, 특히 모나드 방식으로 쿼리를 구성하는 핵심 연산자를 강하게 영향을 받은 닷넷 프레임워크질의 언어를 제공한다.[16]
  • ZipperFS는 지퍼 구조를 주로 사용하여 기능을 구현하는 단순하고 실험적인 파일 시스템이다.[17]
  • 리액티브 확장 프레임워크는 본질적으로 데이터 스트림에 대한 (코)모나드 인터페이스를 제공하여 옵서버 패턴을 구현한다.[18]
Remove ads

역사

요약
관점

프로그래밍에서 "모나드"라는 용어는 순수 함수형 경향이 있는 APLJ 프로그래밍 언어에서 유래했다. 그러나 이들 언어에서 "모나드"는 단일 매개변수를 취하는 함수(두 매개변수를 취하는 함수는 "다이어드" 등으로 불림)의 약어일 뿐이다.[19]

수학자 로제 고드망은 1950년대 후반에 모나드 개념(이를 "표준 구성"이라고 명명)을 처음으로 정립했지만, 널리 퍼지게 된 "모나드"라는 용어는 범주론자인 손더스 매클레인에 의해 대중화되었다. 그러나 위에서 bind를 사용하여 정의된 형태는 원래 1965년 수학자 하인리히 클라이슬리가 모든 모나드가 두 (공변) 함자 사이의 수반으로 특징지어질 수 있음을 증명하기 위해 기술했다.[20]

1980년대부터 모나드 패턴에 대한 모호한 개념이 컴퓨터 과학 커뮤니티에서 나타나기 시작했다. 프로그래밍 언어 연구원인 필립 와들러에 따르면, 컴퓨터 과학자 존 C. 레이놀즈는 1970년대와 1980년대 초반에 연속 전달 스타일의 가치, 형식 의미론의 풍부한 원천으로서의 범주론, 그리고 값과 계산 사이의 타입 구별에 대해 논하면서 모나드의 여러 측면을 예상했다.[4] 1990년까지 활발히 설계되었던 연구 언어 오팔도 I/O를 모나드 타입에 효과적으로 기반했지만, 당시에는 그 연결이 인식되지 않았다.[21]

컴퓨터 과학자 유제니오 모지는 1989년 컨퍼런스 논문에서 범주론의 모나드를 함수형 프로그래밍에 명시적으로 연결한 최초의 인물이었고,[22] 이어서 1991년에는 더욱 정교한 저널 논문을 발표했다. 이전 연구에서 여러 컴퓨터 과학자들은 람다 대수에 대한 의미론을 제공하기 위해 범주론을 사용하는 것을 발전시켰다. 모지의 핵심 통찰력은 실제 프로그램이 단순히 값에서 다른 값으로의 함수가 아니라, 그 값에 대한 계산을 형성하는 변환이라는 것이었다. 이를 범주론적 용어로 형식화하면 모나드가 이러한 계산을 나타내는 구조라는 결론에 이르게 된다.[3]

필립 와들러사이먼 페이튼 존스를 포함한 다른 여러 사람들이 이 아이디어를 대중화하고 발전시켰으며, 이들 모두는 하스켈의 사양에 참여했다. 특히 하스켈은 v1.2까지 I/O를 느긋한 계산법과 조화시키기 위해 문제가 있는 "느긋한 스트림" 모델을 사용하다가, 더 유연한 모나드 인터페이스로 전환했다.[23] 하스켈 커뮤니티는 함수형 프로그래밍의 많은 문제에 모나드를 적용했으며, 2010년대에 하스켈을 연구하는 학자들은 결국 모나드가 어플리커티브 함자라는 것을 인식했다.[24][j] 그리고 모나드와 화살 모두 모노이드이다.[26]

처음에는 모나드를 사용한 프로그래밍이 주로 하스켈과 그 파생 언어에 국한되었지만, 함수형 프로그래밍이 다른 패러다임에 영향을 미치면서 많은 언어가 모나드 패턴(이름은 아닐지라도 정신적으로)을 통합했다. 현재 스킴, , 파이썬, 래킷, 클로저, 스칼라, F#에서 형식화가 존재하며, 새로운 ML 표준에 대해서도 고려되었다.

분석

요약
관점

모나드 패턴의 한 가지 이점은 계산 합성에 대한 수학적 정밀성을 제공한다는 것이다. 모나드 법칙을 사용하여 인스턴스의 유효성을 확인할 수 있을 뿐만 아니라, (함자와 같은) 관련 구조의 기능도 서브타이핑을 통해 사용할 수 있다.

모나드 법칙 검증

Maybe 예제로 돌아가서, 그 구성 요소들은 모나드를 구성한다고 선언되었지만, 모나드 법칙을 만족한다는 증명은 주어지지 않았다.

이것은 일반 법칙의 한쪽에 Maybe의 세부 사항을 대입한 다음, 대수적으로 등식의 사슬을 구성하여 다른 쪽에 도달함으로써 해결될 수 있다.

법칙 1:  eta(a) >>= f(x)  ⇔  (Just a) >>= f(x)  ⇔  f(a)
법칙 2:  ma >>= eta(x)           ⇔  ma
        if ma is (Just a) then
            eta(a)              ⇔ Just a
        else                        or
            Nothing             ⇔ Nothing
        end if
법칙 3:  (ma >>= f(x)) >>= g(y)                       ⇔  ma >>= (f(x) >>= g(y))
        if (ma >>= f(x)) is (Just b) then               if ma is (Just a) then
            g(ma >>= f(x))                                (f(x) >>= g(y)) a
        else                                            else
            Nothing                                         Nothing
        end if                                          end if
if ma is (Just a) and f(a) is (Just b) then
                       (g ∘ f) a
                   else if ma is (Just a) and f(a) is Nothing then
                       Nothing
                   else
                       Nothing
                   end if

함자에서 파생

컴퓨터 과학에서는 더 드물지만, 함자와 두 가지 추가적인 자연 변환을 가진 모나드를 직접적으로 범주론에서 정의할 수 있다.[k] 따라서 시작하려면 구조가 map이라는 고차 함수 (또는 "함수형")를 가져야 함자의 자격을 갖춘다.

map : (a → b) → (ma → mb)

그러나 이것이 항상 큰 문제는 아니다. 특히 모나드가 기존 함자에서 파생될 경우 모나드는 map을 자동으로 상속받기 때문이다. (역사적인 이유로, 이 map은 하스켈에서 fmap이라고 불린다.)

모나드의 첫 번째 변환은 사실 클라이슬리 삼중체와 동일한 unit이지만, 구조의 계층을 면밀히 따르면 unit은 모나드와 기본 함자 사이의 중간 구조인 어플리커티브 함자를 특징짓는 것으로 밝혀진다. 어플리커티브 컨텍스트에서 unit은 때때로 pure라고 불리지만 여전히 동일한 함수이다. 이 구성에서 다른 점은 unit이 만족해야 하는 법칙이다. bind가 정의되지 않았으므로 제약 조건은 map의 관점에서 주어진다.

(unit ∘ φ) x ↔ ((map φ) ∘ unit) x ↔ x[27]

어플리커티브 함자에서 모나드로의 마지막 도약은 두 번째 변환인 join 함수(범주론에서는 일반적으로 μ라고 불리는 자연 변환)와 함께 이루어지며, 이는 중첩된 모나드 적용을 "평탄화"한다.

join(mma) : M (M T) → M T

특성 함수로서 join 또한 모나드 법칙에 대한 세 가지 변형을 만족해야 한다.

(join ∘ (map join)) mmma ↔ (join ∘ join) mmma ↔ ma
(join ∘ (map unit)) ma ↔ (join ∘ unit) ma ↔ ma
(join ∘ (map map φ)) mma ↔ ((map φ) ∘ join) mma ↔ mb

개발자가 직접 모나드를 정의하든 클라이슬리 삼중체를 정의하든, 기본 구조는 동일하며 형식은 서로 쉽게 파생될 수 있다.

(map φ) ma ↔ ma >>= (unit ∘ φ)
join(mma) ↔ mma >>= id
ma >>= f ↔ (join ∘ (map f)) ma[28]

또 다른 예시: List

리스트 모나드는 더 간단한 함자에서 모나드를 파생시키는 것이 얼마나 유용한지를 자연스럽게 보여준다. 많은 언어에서 리스트 구조는 일부 기본 기능과 함께 미리 정의되어 있으므로, List 형식 생성자와 추가 연산자(중위 표기법의 경우 ++로 표시됨)는 여기에서 이미 주어진 것으로 가정한다.

일반 값을 리스트에 임베딩하는 것은 대부분의 언어에서 사소하다.

unit(x)  =  [x]

여기서부터 리스트 캄프리헨션을 사용하여 함수를 반복적으로 적용하는 것이 bind 및 리스트를 완전한 모나드로 변환하는 쉬운 선택처럼 보일 수 있다. 이 접근 방식의 어려움은 bind가 모나드 함수를 기대하며, 이 경우 자체가 리스트를 출력한다는 것이다. 더 많은 함수가 적용될수록 중첩된 리스트 레이어가 축적되어 기본적인 컴프리헨션 이상이 필요하다.

그러나 전체 리스트에 간단한 함수를 적용하는 절차, 즉 map은 간단하다.

(map φ) xlist  =  [ φ(x1), φ(x2), ..., φ(xn) ]

이제 이 두 절차는 이미 List를 어플리커티브 함자로 승격시킨다. 모나드로 완전히 자격을 갖추려면 반복되는 구조를 평탄화하는 올바른 join 개념만 필요하지만, 리스트의 경우 이는 외부 리스트를 풀어서 값을 포함하는 내부 리스트를 추가하는 것을 의미한다.

join(xlistlist)  =  join([xlist1, xlist2, ..., xlistn])
                 =  xlist1 ++ xlist2 ++ ... ++ xlistn

결과적으로 생성된 모나드는 리스트일 뿐만 아니라 함수가 적용될 때마다 자동으로 크기가 조정되고 자체적으로 응축되는 리스트이다. bind는 이제 공식만으로도 파생될 수 있으며, List 값을 모나드 함수의 파이프라인을 통해 공급하는 데 사용될 수 있다.

Thumb
List 모나드는 복소수 근과 같은 다가 함수의 사용을 크게 단순화할 수 있다.[29]
(xlist >>= f)  =  join ∘ (map f) xlist

이 모나드 리스트의 한 가지 응용 프로그램은 비결정적 계산을 나타내는 것이다. List는 알고리즘의 모든 실행 경로에 대한 결과를 보유한 다음, 각 단계에서 자체적으로 응축하여 어떤 경로가 어떤 결과로 이어졌는지 "잊을" 수 있다 (때로는 결정적이고 완전한 알고리즘과 중요한 구별). 또 다른 이점은 검사를 모나드에 내장할 수 있다는 것이다. 특정 경로는 파이프라인의 함수를 다시 작성할 필요 없이 실패 지점에서 투명하게 가지치기될 수 있다.[28]

List가 빛을 발하는 두 번째 상황은 다가 함수를 구성하는 것이다. 예를 들어, 어떤 수의 n번째 복소근n개의 서로 다른 복소수를 생성해야 하지만, 그 결과에 대해 또 다른 m번째 근이 취해지면 최종 m•n개의 값은 m•n번째 근의 출력과 동일해야 한다. List는 이 문제를 완전히 자동화하여 각 단계의 결과를 평탄하고 수학적으로 올바른 리스트로 압축한다.[29]

Remove ads

기법

요약
관점

모나드는 단순히 프로그램 논리를 구성하는 것을 넘어 흥미로운 기법들을 제공한다. 모나드는 유용한 문법적 기능의 토대를 마련할 수 있으며, 그 고수준 및 수학적 특성은 상당한 추상화를 가능하게 한다.

신택틱 슈거 do-notation

bind를 공개적으로 사용하는 것이 종종 의미가 있지만, 많은 프로그래머는 명령문과 유사한 구문(하스켈에서는 do-notation, OCaml에서는 perform-notation, F#에서는 계산 표현식,[30] 스칼라에서는 for comprehension)을 선호한다. 이는 모나드 파이프라인을 코드 블록으로 위장하는 단순한 신택틱 슈거일 뿐이며, 컴파일러는 이러한 표현식을 기본 함수형 코드로 조용히 변환한다.

Maybe에서 add 함수를 하스켈로 번역하면 이 기능을 확인할 수 있다. 하스켈에서 add의 비모나드 버전은 다음과 같다.

add mx my =
    case mx of
        Nothing -> Nothing
        Just x  -> case my of
                       Nothing -> Nothing
                       Just y  -> Just (x + y)

모나드 하스켈에서 returnunit의 표준 이름이며, 람다 표현식은 명시적으로 처리해야 하지만, 이러한 기술적인 문제에도 불구하고 Maybe 모나드는 더 깔끔한 정의를 제공한다.

add mx my =
    mx >>= (\x ->
        my >>= (\y ->
            return (x + y)))

하지만 do-표기법을 사용하면 이 과정을 더욱 직관적인 순서로 간결화할 수 있다.

add mx my = do
    x <- mx
    y <- my
    return (x + y)

두 번째 예는 Maybe가 완전히 다른 언어인 F#에서 어떻게 사용될 수 있는지를 보여준다. 계산 표현식을 사용하면, 정의되지 않은 피연산자나 0으로 나누기 연산에 대해 None을 반환하는 "안전한 나누기" 함수를 다음과 같이 작성할 수 있다.

let readNum () =
  let s = Console.ReadLine()
  let succ,v = Int32.TryParse(s)
  if (succ) then Some(v) else None

let secure_div =
  maybe {
    let! x = readNum()
    let! y = readNum()
    if (y = 0)
    then None
    else return (x / y)
  }

빌드 시 컴파일러는 이 함수를 내부적으로 더 밀집된 bind 호출 체인으로 "디슈거링"할 것이다.

maybe.Delay(fun () ->
  maybe.Bind(readNum(), fun x ->
    maybe.Bind(readNum(), fun y ->
      if (y=0) then None else maybe.Return(x / y))))

마지막 예시로, 일반 모나드 법칙 자체도 do-표기법으로 표현할 수 있다.

do { x <- return v; f x }            ==  do { f v }
do { x <- m; return x }              ==  do { m }
do { y <- do { x <- m; f x }; g y }  ==  do { x <- m; y <- f x; g y }

일반 인터페이스

모든 모나드는 모나드 법칙을 만족하는 특정 구현이 필요하지만, 다른 구조와의 관계나 언어 내의 표준 관용구와 같은 다른 측면은 모든 모나드에서 공유된다. 결과적으로 언어 또는 라이브러리는 함수 원형, 서브타이핑 관계 및 기타 일반적인 사실을 포함하는 일반적인 Monad 인터페이스를 제공할 수 있다. 개발을 시작하고 새로운 모나드가 슈퍼타입(함자와 같은)의 기능을 상속받도록 보장하는 것 외에도, 모나드의 설계를 인터페이스와 비교하여 확인하는 것은 품질 관리의 또 다른 계층을 추가한다.

연산자

모나드 코드는 종종 연산자를 현명하게 사용함으로써 더욱 단순화될 수 있다. map 함수형은 특히 유용할 수 있는데, 이는 임시 모나드 함수뿐만 아니라 다양한 경우에 작동하기 때문이다. 모나드 함수가 미리 정의된 연산자와 유사하게 작동해야 하는 한, map을 사용하여 간단한 연산자를 즉시 모나드 연산자로 "올릴" 수 있다.[l] 이 기법을 사용하면 Maybe 예제의 add 정의를 다음으로 요약할 수 있다.

add(mx,my)  =  map (+)

이 과정은 addMaybe뿐만 아니라 전체 Monad 인터페이스에 대해 정의함으로써 한 단계 더 나아갈 수 있다. 이렇게 하면 구조 인터페이스와 일치하고 자체 map을 구현하는 모든 새로운 모나드도 add의 올림 버전을 즉시 상속받을 것이다. 함수에 필요한 유일한 변경 사항은 타입 시그니처를 일반화하는 것이다.

add : (Monad Number, Monad Number)  →  Monad Number[31]

분석에 유용한 또 다른 모나드 연산자는 모나드 합성(여기서는 중위 >=>로 표현됨)인데, 이는 모나드 함수를 보다 수학적인 스타일로 연결할 수 있도록 한다.

(f >=> g)(x)  =  f(x) >>= g

이 연산자를 사용하면 모나드 법칙을 함수만으로 작성할 수 있으며, 결합성 및 항등원의 존재와의 대응을 강조한다.

(unit >=> g)     ↔  g
(f >=> unit)     ↔  f
(f >=> g) >=> h  ↔  f >=> (g >=> h)[1]

결과적으로, 위 내용은 하스켈의 "do" 블록의 의미를 보여준다.

do
 _p <- f(x)
 _q <- g(_p)
 h(_q)          ↔ ( f >=> g >=> h )(x)
Remove ads

더 많은 예시

요약
관점

항등 모나드

가장 간단한 모나드는 항등 모나드인데, 이는 모나드 법칙을 만족하기 위해 일반 값과 함수에 주석을 달 뿐이다.

newtype Id T  =  T
unit(x)    =  x
(x >>= f)  =  f(x)

Identity는 재귀적인 모나드 트랜스포머기저 사례를 제공하는 것과 같이 유효한 사용 사례를 가지고 있다. 또한 명령형 스타일 블록 내에서 기본적인 변수 할당을 수행하는 데 사용될 수도 있다.[m]

컬렉션

적절한 append를 가진 모든 컬렉션은 이미 모노이드이지만, List만이 잘 정의된 join을 가지고 모나드의 자격을 갖춘 컬렉션은 아닌 것으로 밝혀졌다. append에 특수 속성을 부과하는 것만으로 List를 다른 모나드 컬렉션으로 변형시킬 수도 있다.[n][o]

자세한 정보 컬렉션, 모노이드 속성 ...

IO 모나드 (하스켈)

앞서 언급했듯이, 순수 코드는 관리되지 않는 부작용을 가져서는 안 되지만, 프로그램이 효과를 명시적으로 설명하고 관리하는 것을 배제하지는 않는다. 이 아이디어는 하스켈의 IO 모나드의 핵심이다. 여기서 타입 IO a의 객체는 세상에서 수행될 동작을 설명하는 것으로 볼 수 있으며, 선택적으로 타입 a의 세상에 대한 정보를 제공한다. 세상에 대한 정보를 제공하지 않는 동작은 타입 IO ()를 가지며, 더미 값 ()를 "제공"한다. 프로그래머가 IO 값을 함수에 바인딩할 때, 함수는 이전 동작(사용자 입력, 파일 등)이 제공한 세상에 대한 정보를 기반으로 수행될 다음 동작을 계산한다.[23] 가장 중요하게, IO 모나드의 값은 다른 IO 모나드를 계산하는 함수에만 바인딩될 수 있기 때문에, 바인드 함수는 동작의 결과를 수행할 다음 동작을 계산할 함수에만 제공될 수 있는 동작 시퀀스의 규율을 부과한다. 이는 수행할 필요가 없는 동작은 절대 수행되지 않으며, 수행해야 할 동작은 잘 정의된 시퀀스를 갖는다는 것을 의미한다.

예를 들어, 하스켈에는 파일 시스템에 대한 여러 함수가 있는데, 파일이 존재하는지 확인하는 함수와 파일을 삭제하는 함수가 있다. 두 함수의 타입 시그니처는 다음과 같다.

doesFileExist :: FilePath -> IO Bool
removeFile :: FilePath -> IO ()

첫 번째 함수는 주어진 파일이 실제로 존재하는지에 관심이 있으며, 그 결과 IO 모나드 내에서 불리언 값을 출력한다. 두 번째 함수는 파일 시스템에서 작동하는 것에만 관심이 있으므로 출력하는 IO 컨테이너는 비어 있다.

IO는 파일 I/O에만 국한되지 않는다. 사용자 I/O도 허용하며, 명령형 구문 설탕과 함께 일반적인 "Hello, World!" 프로그램을 모방할 수 있다.

main :: IO ()
main = do
  putStrLn "Hello, world!"
  putStrLn "What is your name, user?"
  name <- getLine
  putStrLn ("Nice to meet you, " ++ name ++ "!")

탈설탕화하면 다음과 같은 모나드 파이프라인으로 변환된다 (하스켈에서 >>는 모나드 효과만 중요하고 기본 결과는 버릴 수 있을 때의 bind 변형일 뿐이다).

main :: IO ()
main =
  putStrLn "Hello, world!" >>
  putStrLn "What is your name, user?" >>
  getLine >>= (\name ->
    putStrLn ("Nice to meet you, " ++ name ++ "!"))

Writer 모나드 (자바스크립트)

또 다른 일반적인 상황은 로그 파일을 유지하거나 프로그램의 진행 상황을 보고하는 것이다. 때로는 프로그래머가 나중에 프로파일링 또는 디버깅을 위해 더 구체적이고 기술적인 데이터를 로깅하기를 원할 수 있다. Writer 모나드는 단계별로 누적되는 보조 출력을 생성함으로써 이러한 작업을 처리할 수 있다.

모나드 패턴이 주로 함수형 언어에만 국한되지 않는다는 것을 보여주기 위해, 이 예제에서는 자바스크립트Writer 모나드를 구현한다. 먼저, 배열(중첩된 꼬리 포함)을 사용하면 Writer 타입을 연결 리스트로 구성할 수 있다. 기본 출력 값은 배열의 0번 위치에 있고, 1번 위치는 보조 노트 체인을 암묵적으로 보유한다.

const writer = value => [value, []];

unit을 정의하는 것도 매우 간단하다.

const unit = value => [value, []];

unit만 있으면 디버깅 노트를 포함하는 Writer 객체를 출력하는 간단한 함수를 정의할 수 있다.

const squared = x => [x * x, [`${x} was squared.`]];
const halved = x => [x / 2, [`${x} was halved.`]];

진정한 모나드는 여전히 bind를 필요로 하지만, Writer의 경우 이는 단순히 함수의 출력을 모나드의 연결 리스트에 연결하는 것에 해당한다.

const bind = (writer, transform) => {
    const [value, log] = writer;
    const [result, updates] = transform(value);
    return [result, log.concat(updates)];
};

이제 샘플 함수는 bind를 사용하여 연결될 수 있지만, 모나드 합성 버전(여기서는 pipelog라고 함)을 정의하면 이러한 함수를 더욱 간결하게 적용할 수 있다.

const pipelog = (writer, ...transforms) =>
    transforms.reduce(bind, writer);

최종 결과는 계산 단계를 거치는 것과 나중에 감사하기 위해 로깅하는 것 사이의 깔끔한 관심사 분리이다.

pipelog(unit(4), squared, halved);
// 결과 Writer 객체 = [8, ['4 was squared.', '16 was halved.']]

환경 모나드

환경 모나드(리더 모나드 및 함수 모나드라고도 함)를 사용하면 계산이 공유 환경의 값에 따라 달라질 수 있다. 모나드 타입 생성자는 타입 T를 함수 타입 E → T에 매핑하며, 여기서 E는 공유 환경의 타입이다. 모나드 함수는 다음과 같다.

다음 모나드 연산은 유용하다.

ask 연산은 현재 컨텍스트를 검색하는 데 사용되며, local은 수정된 하위 컨텍스트에서 계산을 실행한다. 상태 모나드와 마찬가지로 환경 모나드의 계산은 단순히 환경 값을 제공하고 이를 모나드의 인스턴스에 적용하여 호출할 수 있다.

형식적으로, 환경 모나드의 값은 추가적인 익명 인수를 가진 함수와 동일하다. returnbindSKI 조합자 계산법에서 각각 KS 조합자와 동일하다.

상태 모나드

상태 모나드는 프로그래머가 모든 타입의 상태 정보를 계산에 첨부할 수 있도록 한다. 모든 값 타입이 주어졌을 때, 상태 모나드의 해당 타입은 상태를 받아들인 다음 (타입 s의) 새로운 상태와 함께 (타입 t의) 반환 값을 출력하는 함수이다. 이는 환경 모나드와 유사하지만, 새로운 상태를 반환하므로 변경 가능한 환경을 모델링할 수 있다는 점이 다르다.

type State s t = s -> (t, s)

이 모나드가 상태 정보의 타입인 타입 매개변수를 취한다는 점에 유의하라. 모나드 연산은 다음과 같이 정의된다.

-- "return"은 상태를 변경하지 않고 주어진 값을 생성한다.
return x = \s -> (x, s)
-- "bind"는 m을 수정하여 결과를 f에 적용한다.
m >>= f = \r -> let (x, s) = m r in (f x) s

유용한 상태 연산은 다음과 같다.

get = \s -> (s, s) -- 계산의 이 지점에서 상태를 검사한다.
put s = \_ -> ((), s) -- 상태를 바꾼다.
modify f = \s -> ((), f s) -- 상태를 업데이트한다.

다른 연산은 주어진 초기 상태에 상태 모나드를 적용한다.

runState :: State s a -> s -> (a, s)
runState t s = t s

상태 모나드의 do-블록은 상태 데이터를 검사하고 업데이트할 수 있는 일련의 연산이다.

비공식적으로, 상태 타입 S의 상태 모나드는 반환 값의 타입 T 타입의 함수로 매핑하며, 여기서 S는 기본 상태이다. returnbind 함수는 다음과 같다.

.

범주론적 관점에서, 상태 모나드는 곱 함자와 지수 함자 사이의 수반에서 파생되며, 이는 정의상 모든 데카르트 닫힌 범주에 존재한다.

연속 모나드

반환 타입 R을 가진 연속 모나드[p]는 타입 T를 함수 타입 에 매핑한다. 이는 연속 전달 스타일을 모델링하는 데 사용된다. return 및 bind 함수는 다음과 같다.

Call-with-current-continuation 함수는 다음과 같이 정의된다.

프로그램 로깅

다음 코드는 의사 코드이다. 두 함수 foobar가 있다고 가정해 보자. 타입은 다음과 같다.

foo : int -> int
bar : int -> int

즉, 두 함수 모두 정수를 받아 다른 정수를 반환한다. 그러면 함수를 다음과 같이 연속적으로 적용할 수 있다.

foo (bar x)

결과는 xbar를 적용한 결과에 foo를 적용한 결과이다.

그러나 프로그램을 디버깅 중이고 foobar에 로깅 메시지를 추가하고 싶다고 가정해 보자. 그래서 타입을 다음과 같이 변경한다.

foo : int -> int * string
bar : int -> int * string

그래서 두 함수 모두 튜플을 반환하며, 적용 결과는 정수이고, 적용된 함수와 이전에 적용된 모든 함수에 대한 정보가 포함된 로깅 메시지는 문자열이다.

불행히도, 이는 foobar를 더 이상 구성할 수 없다는 것을 의미한다. 입력 타입 int가 출력 타입 int * string과 호환되지 않기 때문이다. 그리고 각 함수의 타입을 int * string -> int * string으로 수정하여 구성 가능성을 다시 얻을 수 있지만, 이는 튜플에서 정수를 추출하기 위해 각 함수에 상용구 코드를 추가해야 하며, 이러한 함수의 수가 증가함에 따라 지루해질 것이다.

대신, 이 상용구를 추상화하는 도우미 함수를 정의해 보자.

bind : int * string -> (int -> int * string) -> int * string

bind는 정수와 문자열 튜플을 받은 다음, 정수에서 정수와 문자열 튜플로 매핑하는 함수(예: foo)를 받는다. 그 출력은 입력 정수와 문자열 튜플 내의 정수에 입력 함수를 적용한 결과인 정수와 문자열 튜플이다. 이러한 방식으로, bind에서 튜플에서 정수를 추출하는 상용구 코드를 한 번만 작성하면 된다.

이제 우리는 구성 가능성을 어느 정도 회복했다. 예를 들어:

bind (bind (x,s) bar) foo

여기서 (x,s)는 정수와 문자열 튜플이다.[q]

이점을 더욱 명확히 하기 위해 bind의 별칭으로 중위 연산자를 정의해 보자.

(>>=) : int * string -> (int -> int * string) -> int * string

그래서 t >>= fbind t f와 같다.

그러면 위 예시는 다음과 같이 된다.

((x,s) >>= bar) >>= foo

마지막으로, 빈 로깅 메시지를 생성할 때마다 (x, "")를 작성하지 않도록 새 함수를 정의한다. 여기서 ""는 빈 문자열이다.

return : int -> int * string

이는 위에서 설명한 튜플로 x를 래핑한다.

그 결과는 로깅 메시지를 위한 파이프라인이다.

((return x) >>= bar) >>= foo

이를 통해 barfoox에 미치는 영향을 더 쉽게 로깅할 수 있다.

int * string은 의사 코드화된 모나드 값을 나타낸다.[q] bindreturn은 같은 이름의 해당 함수와 유사하다. 사실, int * string, bind, return은 모나드를 형성한다.

가산 모나드

가산 모나드는 추가적인 닫힌, 결합적인 이항 연산자 mplusmplus 아래의 항등 요소 mzero가 부여된 모나드이다. Maybe 모나드는 mzero로서 NothingOR 연산자의 변형으로서 mplus를 가진 가산 모나드로 간주될 수 있다. List 또한 가산 모나드로, 빈 리스트 []mzero 역할을 하고 연결 연산자 ++mplus 역할을 한다.

직관적으로 mzero는 기본 타입에서 값이 없는 모나드 래퍼를 나타내지만, 모나드 함수에 바인딩될 때마다 mzero를 반환하는 bind흡수원 역할을 하므로 "1"이 아닌 "0"으로 간주된다. 이 속성은 양면적이며, 어떤 값이 모나드 영 함수에 바인딩될 때도 bindmzero를 반환할 것이다.

범주론적 용어로, 가산 모나드는 bind(모든 모나드가 그렇듯이)를 가진 모나드 함수에 대한 모노이드로서 한 번, 그리고 mplus를 통한 모나드 값에 대한 모노이드로서 다시 한 번 자격을 갖춘다.[32][r]

자유 모나드

때로는 모나드의 일반적인 윤곽이 유용할 수 있지만, 어떤 모나드가 다른 모나드보다 더 낫다고 추천할 만한 간단한 패턴이 없을 때가 있다. 이것이 자유 모나드가 등장하는 곳이다. 모나드 범주에서 자유 대상으로서, 모나드 법칙을 넘어서는 특정 제약 없이 모나드 구조를 나타낼 수 있다. 자유 모노이드가 평가 없이 요소를 연결하는 것처럼, 자유 모나드는 타입 시스템을 만족시키기 위해 마커와 함께 계산을 연결할 수 있지만, 그 자체로는 더 깊은 의미를 부여하지 않는다.

예를 들어, JustNothing 마커를 통해 전적으로 작동함으로써 Maybe 모나드는 사실 자유 모나드이다. 반면에 List 모나드는 리스트에 대한 추가적이고 구체적인 사실(append와 같은)을 정의에 포함하기 때문에 자유 모나드가 아니다. 마지막 예시는 추상적인 자유 모나드이다.

data Free f a
  = Pure a
  | Free (f (Free f a))

unit :: a -> Free f a
unit x = Pure x

bind :: Functor f => Free f a -> (a -> Free f b) -> Free f b
bind (Pure x) f = f x
bind (Free x) f = Free (fmap (\y -> bind y f) x)

그러나 자유 모나드는 이 예시처럼 연결 리스트에 국한되지 않으며, 트리와 같은 다른 구조를 중심으로 구축될 수도 있다.

자유 모나드를 의도적으로 사용하는 것이 처음에는 비실용적으로 보일 수 있지만, 그 형식적인 특성은 구문 문제에 특히 적합하다. 자유 모나드는 나중에 의미론을 남겨두고 구문과 타입을 추적하는 데 사용될 수 있으며, 그 결과 파서와 인터프리터에서 사용되었다.[33] 다른 사람들은 이를 더 동적이고 운영적인 문제에도 적용했으며, 예를 들어 언어 내에서 반복자를 제공하는 데 사용했다.[34]

코모나드

추가 속성을 가진 모나드를 생성하는 것 외에도, 주어진 모나드에 대해 코모나드를 정의할 수 있다. 개념적으로, 모나드가 기본 값에서 구축된 계산을 나타낸다면, 코모나드는 값으로 다시 환원되는 감소로 볼 수 있다. 모나드 코드는 어떤 의미에서는 완전히 "압축 해제"될 수 없다. 값이 모나드 내부에 래핑되면, 어떤 부작용과 함께 그 안에 격리된 상태로 유지된다 (순수 함수형 프로그래밍에서는 좋은 점이다). 그러나 때로는 문제가 문맥 데이터를 소비하는 것에 더 가깝고, 코모나드는 이를 명시적으로 모델링할 수 있다.

기술적으로, 코모나드는 모나드의 범주론적 쌍대이며, 이는 느슨하게 말하면 타입 시그니처의 방향이 역전된 동일한 필수 구성 요소를 가질 것이라는 의미이다. bind-중심 모나드 정의에서 시작하여, 코모나드는 다음으로 구성된다.

  • 고차 타입 W T를 표시하는 타입 생성자 W
  • unit의 쌍대이며, 여기서 counit이라고 불리는 것은 코모나드에서 기본 값을 추출한다.
counit(wa) : W T → T
  • 감소 함수 체인을 확장하는 bind의 역전(=>>로도 표현됨)이다.
(wa =>> f) : (W U, W U → T) → W T[s]

extendcounit은 또한 모나드 법칙의 쌍대도 만족해야 한다.

counit ∘ ( (wa =>> f) → wb )  ↔  f(wa) → b
wa =>> counit  ↔  wa
wa ( (=>> f(wx = wa)) → wb (=>> g(wy = wb)) → wc )( wa (=>> f(wx = wa)) → wb ) (=>> g(wy = wb)) → wc

모나드와 마찬가지로 코모나드도 join의 쌍대를 사용하여 함자에서 파생될 수 있다.

  • duplicate는 이미 코모나드인 값을 또 다른 코모나드 구조 층으로 래핑한다.
duplicate(wa) : W T → W (W T)

그러나 extend와 같은 연산은 역전되지만, 코모나드는 그에 작용하는 함수를 역전시키지 않으며, 결과적으로 코모나드는 여전히 map을 가진 함자이지 공함자는 아니다. duplicate, counit, map을 사용한 대체 정의도 자체 코모나드 법칙을 준수해야 한다.

((map duplicate) ∘ duplicate) wa  ↔  (duplicate ∘ duplicate) wa  ↔  wwwa
((map counit) ∘ duplicate)    wa  ↔  (counit ∘ duplicate)    wa  ↔  wa
((map map φ) ∘ duplicate)     wa  ↔  (duplicate ∘ (map φ))   wa  ↔  wwb

모나드와 마찬가지로 두 형식은 자동으로 변환될 수 있다.

(map φ) wa    ↔  wa =>> (φ ∘ counit) wx
duplicate wa  ↔  wa =>> wx
wa =>> f(wx)  ↔  ((map f) ∘ duplicate) wa

간단한 예시는 곱 코모나드이며, 입력 값과 공유 환경 데이터를 기반으로 값을 출력한다. 사실, Product 코모나드는 Writer 모나드의 쌍대이며 Reader 모나드와 실질적으로 동일하다 (둘 다 아래에서 논의됨). ProductReader는 수용하는 함수 시그니처와 값 래핑 또는 언래핑을 통해 해당 함수를 보완하는 방식에서만 다르다.

덜 자명한 예는 스트림 코모나드인데, 이는 데이터 스트림을 나타내고 extend를 사용하여 들어오는 신호에 필터를 첨부하는 데 사용될 수 있다. 사실, 모나드만큼 인기가 있지는 않지만, 연구자들은 코모나드가 스트림 프로세싱데이터플로 프로그래밍 모델링에 특히 유용하다는 것을 발견했다.[35][36]

그러나 엄격한 정의 때문에 모나드와 코모나드 사이에서 객체를 단순히 앞뒤로 이동할 수는 없다. 더 높은 추상화로서 화살은 두 구조를 모두 포괄할 수 있지만, 모나드 및 코모나드 코드를 결합하는 더 세밀한 방법을 찾는 것은 활발한 연구 분야이다.[37][38]

Remove ads

같이 보기

계산을 모델링하기 위한 대안:

  • 이펙트 시스템 (특히 대수적 이펙트 핸들러)은 부작용을 타입으로 설명하는 다른 방법이다.
  • 고유성 타입은 함수형 언어에서 부작용을 처리하는 세 번째 접근 방식이다.

관련 디자인 개념:

  • 관점 지향 프로그래밍은 모듈성과 단순성을 향상시키기 위해 보조적인 장부 정리 코드를 분리하는 것을 강조한다.
  • 제어 반전은 포괄적인 프레임워크에서 특정 함수를 호출하는 추상적인 원리이다.
  • 타입 클래스는 하스켈에서 모나드 및 기타 구조를 구현하는 데 사용되는 특정 언어 기능이다.
  • 데코레이터 패턴은 객체 지향 프로그래밍에서 유사한 이점을 얻기 위한 더 구체적이고 즉흥적인 방법이다.

모나드의 일반화:

  • 어플리커티브 함자unit과 이를 map과 연결하는 법칙만 유지함으로써 모나드에서 일반화된다.
  • 화살은 추가 구조를 사용하여 일반 함수와 모나드를 단일 인터페이스로 통합한다.
  • 모나드 트랜스포머는 서로 다른 모나드에 작용하여 모듈식으로 결합한다.
Remove ads

내용주

  1. 더 형식적으로, 모나드는 자기 함자 범주에서의 모노이드이다.
  2. 프로그래밍에서 여러 자유 변수에 대한 함수가 흔하다는 사실 때문에, 이 문서에 설명된 모나드는 기술적으로 범주론자들이 강한 모나드라고 부를 것이다.[3]
  3. Maybe에 대한 구체적인 동기는 (Hutton 2016)에서 찾을 수 있다.[7]
  4. 하튼은 실패할 수 있는 타입 a와 실패할 수 있는 a→b 매핑이 주어졌을 때 실패할 수 있는 결과 b를 생성하는 bind를 추상화한다. (하튼, 2016)[7]
  5. (Hutton 2016)은 Just가 성공을, Nothing이 실패를 나타낼 수 있다고 언급한다.[7]
  6. 의미적으로, M은 자명하지 않으며 모든 잘 타입화된 값의 범주에 대한 자기 함자를 나타낸다:
  7. 프로그래밍 용어로는 (매개변수 다형성) 함수이지만, unit (범주론에서는 종종 η라고 불림)은 수학적으로 함자들 사이를 매핑하는 자연 변환이다:
  8. 반면에 bind는 범주론에서 자연 변환이 아니라 매핑(값에서 계산으로)을 계산 간의 사상으로 올리는 확장 이다:
  9. 엄밀히 말하면, bind람다 대수 내의 적용에 해당하며 수학적 적용에 해당하지 않기 때문에 모든 컨텍스트에서 형식적으로 결합적이지 않을 수 있다. 엄격한 람다 대수에서는 bind를 평가하려면 왼쪽에서 입력을 계속 받을 수 있도록 먼저 오른쪽 항(두 모나드 값을 바인딩할 때) 또는 바인드 자체(두 모나드 함수 사이)를 익명 함수로 래핑해야 할 수 있다.[10]
  10. GHC 버전 7.10.1부터, 하스켈은 모나드를 사용하는 기존 모듈에 7줄의 코드를 삽입하도록 요구하는 하스켈의 2014년 어플리커티브 모나드 제안(AMP)을 시행하기 시작했다.[25]
  11. 이 자연 변환들은 일반적으로 사상 η, μ로 표시된다. 즉, η, μ는 각각 unit과 join을 나타낸다.
  12. 하스켈과 같은 일부 언어는 다른 컨텍스트에서 map에 대한 lift라는 별칭과 다른 매개변수 개수에 대한 여러 버전을 제공하는데, 이 세부 사항은 여기서는 무시한다.
  13. 범주론에서 Identity 모나드는 임의의 함자와 그 역함자 사이의 수반에서 발생하는 것으로 볼 수도 있다.
  14. 범주론은 이러한 컬렉션 모나드를 자유 함자집합 범주에서 모노이드 범주로의 다른 함자 사이의 수반으로 본다.
  15. 여기서 프로그래머의 과제는 적절한 모노이드를 구성하거나 라이브러리에서 모노이드를 선택하는 것이다.
  16. 독자는 McCann의 스레드[6]를 따라가서 아래의 타입들과 비교해 볼 수 있다.
  17. 이 경우, bind는 이전에 integer만 있던 자리에 string을 삽입했다. 즉, 프로그래머는 튜플 (x,s)(의사 코드 위 §에서 int * string으로 표시됨)인 수반을 구성했다.
  18. 대수적으로, 두 (비교환적) 모노이드 측면 간의 관계는 준반환의 관계와 유사하며, 일부 가산 모나드는 그러한 자격을 갖춘다. 그러나 모든 가산 모나드가 준반환의 분배법칙을 충족하지는 않는다.[32]
  19. 하스켈에서 extend는 실제로 입력이 바뀐 채로 정의되지만, 이 문서에서는 커링을 사용하지 않으므로 bind의 정확한 쌍대로 정의된다.
Remove ads

각주

외부 링크

Loading related searches...

Wikiwand - on

Seamless Wikipedia browsing. On steroids.

Remove ads