Lean으로 하는 함수형 프로그래밍

6.2. 모나드 구성 키트🔗

ReaderT가 유용한 모나드 트랜스포머의 전부는 아닙니다. 이 절에서는 몇 가지 추가적인 트랜스포머에 대해 설명합니다. 각 모나드 트랜스포머는 다음으로 구성됩니다:

  1. 모나드를 인자로 받는 정의나 데이터타입 T. (Type u Type v) Type u Type v와 같은 타입을 가져야 하지만, 모나드보다 앞서 추가 인자를 받을 수도 있습니다.

  2. Monad m의 인스턴스에 의존하는, T m에 대한 Monad 인스턴스입니다. 이를 통해 변환된 모나드를 모나드로 사용할 수 있게 됩니다.

  3. 임의의 모나드 m에 대해, m α 타입의 액션을 T m α 타입의 액션으로 변환하는 MonadLift 인스턴스입니다. 이를 통해 밑에 있는 모나드의 액션을 변환된 모나드에서 사용할 수 있습니다.

또한, 트랜스포머의 Monad 인스턴스는, 적어도 기저 Monad 인스턴스가 그러하다면, Monad에 대한 계약을 준수해야 합니다. 또한, monadLift (pure x : m α)는 변환된 모나드에서 pure x와 동치여야 하며, monadLiftbind에 대해 분배되어야 합니다. 즉, monadLift (x >>= f : m α)(monadLift x : m α) >>= fun y => monadLift (f y)와 같아야 합니다.

많은 모나드 트랜스포머는 추가로 MonadReader와 같은 스타일의 타입 클래스를 정의하여, 해당 모나드에서 실제로 사용 가능한 효과를 기술합니다. 이는 더 큰 유연성을 제공할 수 있습니다. 즉, 인터페이스에만 의존하는 프로그램을 작성할 수 있게 해주며, 기저 모나드가 특정 트랜스포머로 구현되도록 제약하지 않습니다. 타입 클래스는 프로그램이 자신의 요구 사항을 표현하는 방법이며, 모나드 트랜스포머는 이러한 요구 사항을 충족하는 편리한 방법입니다.

6.2.1. OptionT를 사용한 실패🔗

Option 모나드로 표현되는 실패와 Except 모나드로 표현되는 예외 모두 이에 대응하는 트랜스포머를 가지고 있습니다. Option의 경우, 모나드가 원래대로라면 α 타입의 값을 담았을 자리에 대신 Option α 타입의 값을 담도록 함으로써 실패를 추가할 수 있습니다. 예를 들어, IO (Option α)는 항상 α 타입의 값을 반환하지는 않는 IO 동작을 나타냅니다. 이는 모나드 트랜스포머 OptionT의 정의를 시사합니다:

def OptionT (m : Type u Type v) (α : Type u) : Type v := m (Option α)

OptionT가 실제로 작동하는 예시로, 사용자에게 질문을 던지는 프로그램을 생각해 보겠습니다. getSomeInput 함수는 한 줄의 입력을 요청하고 양쪽 끝의 공백을 제거합니다. 결과로 트리밍된 입력이 비어 있지 않으면 이를 반환하지만, 공백이 아닌 문자가 없으면 함수는 실패합니다.

def getSomeInput : OptionT IO String := do let input ( IO.getStdin).getLine let trimmed := input.trimAscii if trimmed.isEmpty then failure else pure trimmed.copy

이 특정 애플리케이션은 사용자의 이름과 가장 좋아하는 딱정벌레 종을 추적합니다:

structure UserInfo where name : String favoriteBeetle : String

사용자에게 입력을 요청하는 것은 IO만 사용하는 함수보다 더 장황하지 않습니다:

def getUserInfo : OptionT IO UserInfo := do IO.println "What is your name?" let name getSomeInput IO.println "What is your favorite species of beetle?" let beetle getSomeInput pure name, beetle

하지만 이 함수는 단순히 IO가 아니라 OptionT IO 컨텍스트에서 실행되기 때문에, getSomeInput의 첫 번째 호출이 실패하면 전체 getUserInfo가 실패하게 되며, 제어 흐름은 딱정벌레에 관한 질문에 결코 도달하지 못합니다. 메인 함수 interact는 순수한 IO 컨텍스트에서 getUserInfo를 호출하는데, 이를 통해 내부의 Option에 대해 패턴 매칭함으로써 호출의 성공 또는 실패 여부를 확인할 수 있습니다:

def interact : IO Unit := do match getUserInfo with | none => IO.eprintln "Missing info" | some name, beetle => IO.println s!"Hello {name}, whose favorite beetle is {beetle}."

6.2.1.1. 모나드 인스턴스🔗

모나드 인스턴스를 작성하다 보면 한 가지 어려움이 드러납니다. 타입에 따르면, pure는 하위 모나드 mpuresome과 함께 사용해야 합니다. Option에 대한 bind가 첫 번째 인자를 기준으로 분기하여 none을 전파하는 것과 마찬가지로, OptionT에 대한 bind는 첫 번째 인자를 구성하는 모나드적 액션을 실행하고, 그 결과를 기준으로 분기한 다음, none을 전파해야 합니다. 이 스케치를 따라가면 다음과 같은 정의를 얻게 되지만, Lean은 이를 받아들이지 않습니다:

instance [Monad m] : Monad (OptionT m) where pure x := failed to synthesize instance of type class Pure (OptionT m) Hint: Type class instance resolution failures can be inspected with the `set_option trace.Meta.synthInstance true` command.pure Application type mismatch: The argument some x has type Option α✝ but is expected to have type α✝ in the application pure (some x)(some x) bind action next := do match ( action) with | Type mismatch none has type Option ?m.18 but is expected to have type α✝none => pure none | some v => next v

오류 메시지는 알쏭달쏭한 타입 불일치를 보여줍니다:

Application type mismatch: The argument
  some x
has type
  Option α✝
but is expected to have type
  α✝
in the application
  pure (some x)

여기서 문제는 Lean이 주변에서 사용된 pure에 대해 잘못된 Monad 인스턴스를 선택하고 있다는 것입니다. bind의 정의에서도 유사한 오류가 발생합니다. 한 가지 해결책은 타입 표기를 사용하여 Lean이 올바른 Monad 인스턴스를 찾도록 안내하는 것입니다:

instance [Monad m] : Monad (OptionT m) where pure x := (pure (some x) : m (Option _)) bind action next := (do match ( action) with | none => pure none | some v => next v : m (Option _))

이 해법은 작동하기는 하지만 우아하지 않으며 코드가 다소 지저분해집니다.

대안적인 해결책은 타입 시그니처가 Lean을 올바른 인스턴스로 안내하도록 함수를 정의하는 것입니다. 사실, OptionT는 다음과 같이 구조체로 정의될 수도 있었습니다:

structure OptionT (m : Type u Type v) (α : Type u) : Type v where run : m (Option α)

이는 문제를 해결할 것입니다. 왜냐하면 생성자 OptionT.mk와 필드 접근자 OptionT.run이 타입 클래스 추론을 올바른 인스턴스로 이끌어 주기 때문입니다. 이렇게 할 경우의 단점은 결과적인 코드가 더 복잡해진다는 것이며, 이러한 구조는 증명을 읽기 더 어렵게 만들 수 있습니다. 생성자 OptionT.mk와 필드 OptionT.run과 같은 역할을 하면서도 직접 정의와 함께 작동하는 함수를 정의하면 두 가지 방식의 장점을 모두 얻을 수 있습니다:

def OptionT.mk (x : m (Option α)) : OptionT m α := x def OptionT.run (x : OptionT m α) : m (Option α) := x

두 함수 모두 입력을 변경 없이 그대로 반환하지만, OptionT의 인터페이스를 나타내기 위한 코드와 기저 모나드 m의 인터페이스를 나타내기 위한 코드 사이의 경계를 표시합니다. 이 헬퍼들을 사용하면 Monad 인스턴스가 더 읽기 쉬워집니다:

instance [Monad m] : Monad (OptionT m) where pure x := OptionT.mk (pure (some x)) bind action next := OptionT.mk do match action with | none => pure none | some v => next v

여기서 OptionT.mk의 사용은 그 인자가 m의 인터페이스를 사용하는 코드로 간주되어야 함을 나타내며, 이는 Lean이 올바른 Monad 인스턴스를 선택할 수 있게 합니다.

모나드 인스턴스를 정의한 후에는 모나드 계약이 충족되는지 확인하는 것이 좋습니다. 첫 번째 단계는 bind (pure v) ff v와 같음을 보이는 것입니다. 단계는 다음과 같습니다:

bind (pure v) f

Unfolding the definitions of bind and pure

OptionT.mk do match pure (some v) with | none => pure none | some x => f x

Desugaring nested action syntax

OptionT.mk do let y pure (some v) match y with | none => pure none | some x => f x

Desugaring do-notation

OptionT.mk (pure (some v) >>= fun y => match y with | none => pure none | some x => f x)

Using the first monad rule for m

OptionT.mk (match some v with | none => pure none | some x => f x)

Reduce match

OptionT.mk (f v)

Definition of OptionT.mk

f v

두 번째 규칙은 bind w purew와 같다는 것을 나타냅니다. 이를 증명하기 위해 bindpure의 정의를 펼치면 다음과 같습니다:

OptionT.mk do match w with | none => pure none | some v => pure (some v)

이 패턴 매칭에서 두 경우 모두 결과는 매칭되는 패턴과 동일하며, 다만 그 주위에 pure가 감싸져 있을 뿐입니다. 다시 말해, 이는 w >>= fun y => pure y와 동등하며, 이는 m의 두 번째 모나드 규칙의 한 사례입니다.

마지막 규칙은 bind (bind v f) gbind v (fun x => bind (f x) g)와 같다는 것을 나타냅니다. bindpure의 정의를 전개한 다음 기저 모나드 m에 위임함으로써 같은 방식으로 확인할 수 있습니다.

6.2.1.2. Alternative 인스턴스🔗

OptionT를 사용하는 편리한 방법 중 하나는 Alternative 타입 클래스를 통하는 것입니다. 성공적인 반환은 이미 pure로 표시되며, AlternativefailureorElse 메서드는 여러 하위 프로그램 중에서 처음으로 성공한 결과를 반환하는 프로그램을 작성할 수 있는 방법을 제공합니다:

instance [Monad m] : Alternative (OptionT m) where failure := OptionT.mk (pure none) orElse x y := OptionT.mk do match x with | some result => pure (some result) | none => y ()

6.2.1.3. 승격(Lifting)🔗

m에서 OptionT m으로 액션을 리프팅하려면 계산 결과를 some으로 감싸기만 하면 됩니다:

instance [Monad m] : MonadLift m (OptionT m) where monadLift action := OptionT.mk do pure (some ( action))

6.2.2. 예외🔗

Except의 모나드 트랜스포머 버전은 Option의 모나드 트랜스포머 버전과 매우 유사합니다. m α 타입의 어떤 모나드 액션에 ε 타입의 예외를 추가하는 것은 α에 예외를 추가하여 m (Except ε α) 타입을 만들어냄으로써 이룰 수 있습니다:

def ExceptT (ε : Type u) (m : Type u Type v) (α : Type u) : Type v := m (Except ε α)

OptionT는 타입 검사기가 올바른 Monad 인스턴스를 찾도록 안내하는 OptionT.mkOptionT.run 함수를 제공합니다. 이 트릭은 ExceptT에도 유용합니다:

def ExceptT.mk {ε α : Type u} (x : m (Except ε α)) : ExceptT ε m α := x def ExceptT.run {ε α : Type u} (x : ExceptT ε m α) : m (Except ε α) := x

Monad 인스턴스는 ExceptT에 대해서도 OptionT에 대한 인스턴스와 매우 유사합니다. 유일한 차이점은 none 대신 특정 오류 값을 전파한다는 것입니다:

instance {ε : Type u} {m : Type u Type v} [Monad m] : Monad (ExceptT ε m) where pure x := ExceptT.mk (pure (Except.ok x)) bind result next := ExceptT.mk do match result with | .error e => pure (.error e) | .ok x => next x

ExceptT.mkExceptT.run의 타입 시그니처에는 미묘한 세부 사항이 있습니다. 바로 αε의 유니버스 레벨을 명시적으로 표기한다는 점입니다. 명시적으로 표기되지 않은 경우, Lean은 이들이 서로 다른 다형적 유니버스 변수를 갖는 더 일반적인 타입 시그니처를 생성합니다. 하지만 ExceptT의 정의는 이들이 같은 유니버스에 있을 것을 요구하는데, 이는 둘 다 m의 인자로 제공될 수 있기 때문입니다. 이로 인해 Monad 인스턴스에서 유니버스 레벨 해결기가 작동하는 해를 찾지 못하는 문제가 발생할 수 있습니다:

`ExceptT.mk`: universes `u_1`, `u_2` only occur together. This usually means there is a `max` expression in the type where none of these universes appear on their own. Note: This linter can be disabled with `set_option linter.checkUnivs false`def ExceptT.mk (x : m (Except ε α)) : ExceptT ε m α := xinstance {ε : Type u} {m : Type u Type v} [Monad m] : Monad (ExceptT ε m) stuck at solving universe constraint max ?u.26 ?u.27 =?= u while trying to unify ExceptT ε m β✝ : Type v with ExceptT.{max ?u.27 ?u.26, v} ε m β✝ : Type vstuck at solving universe constraint max ?u.17 ?u.18 =?= u while trying to unify ExceptT ε m α✝ : Type v with ExceptT.{max ?u.18 ?u.17, v} ε m α✝ : Type vwhere pure x := ExceptT.mk (pure (Except.ok x)) bind result next := ExceptT.mk do stuck at solving universe constraint max ?u.17 ?u.18 =?= u while trying to unify ExceptT ε m α✝ : Type v with ExceptT.{max ?u.18 ?u.17, v} ε m α✝ : Type vstuck at solving universe constraint max ?u.26 ?u.27 =?= u while trying to unify ExceptT ε m β✝ : Type v with ExceptT.{max ?u.27 ?u.26, v} ε m β✝ : Type vmatch ( result) with | .error e => pure (.error e) | .ok x => next x
stuck at solving universe constraint
  max ?u.26 ?u.27 =?= u
while trying to unify
  ExceptT ε m β✝ : Type v
with
  ExceptT.{max ?u.27 ?u.26, v} ε m β✝ : Type v

이런 종류의 오류 메시지는 일반적으로 제약이 충분하지 않은 유니버스 변수로 인해 발생합니다. 이를 진단하기는 까다로울 수 있지만, 좋은 첫 단계는 일부 정의에서는 재사용되는 유니버스 변수가 다른 정의에서는 재사용되지 않는 경우를 찾아보는 것입니다.

Option과 달리, Except 데이터 타입은 일반적으로 자료 구조로 사용되지 않습니다. 이는 항상 Monad 인스턴스와 함께 제어 구조로 사용됩니다. 이는 Except ε 액션을 ExceptT ε m으로 들어 올리는 것이, 기저 모나드 m의 액션을 들어 올리는 것과 마찬가지로 합당함을 의미합니다. Except 액션을 ExceptT 액션으로 끌어올리는 것은 이를 mpure로 감싸서 이루어지는데, 예외 효과만을 가진 액션은 모나드 m으로부터 오는 효과를 전혀 가질 수 없기 때문입니다:

instance [Monad m] : MonadLift (Except ε) (ExceptT ε m) where monadLift action := ExceptT.mk (pure action)

m의 액션에는 예외가 전혀 없으므로, 그 값은 Except.ok로 감싸야 합니다. FunctorMonad의 상위 클래스라는 사실을 이용하면 이를 달성할 수 있으므로, 모나드 계산의 결과에 함수를 적용하는 것은 Functor.map을 사용하여 수행할 수 있습니다:

instance [Monad m] : MonadLift m (ExceptT ε m) where monadLift action := ExceptT.mk (.ok <$> action)

6.2.2.1. 예외를 위한 타입 클래스🔗

예외 처리는 근본적으로 두 가지 연산으로 구성됩니다: 예외를 발생시키는 능력과 그로부터 복구하는 능력입니다. 지금까지는 각각 Except의 생성자와 패턴 매칭을 사용하여 이를 수행했습니다. 하지만 이 방식은 예외를 사용하는 프로그램을 예외 처리 이펙트의 특정 인코딩 하나에 종속시킵니다. 타입 클래스를 사용하여 이러한 연산을 포착하면, 예외를 사용하는 프로그램을 발생과 처리를 지원하는 모든 모나드에서 사용할 수 있습니다.

예외를 던지는 것은 예외를 인자로 받아야 하며, 모나드 동작이 요구되는 모든 문맥에서 허용되어야 합니다. 명세에서 "어떤 컨텍스트에서든"에 해당하는 부분은 m α라고 씀으로써 하나의 타입으로 나타낼 수 있습니다—임의의 타입에 대해 값을 만들어낼 방법은 없으므로, throw 연산은 반드시 프로그램의 그 부분에서 제어가 벗어나도록 만드는 무언가를 수행하고 있는 것입니다. 예외를 잡는 동작은 임의의 모나드 액션을 핸들러와 함께 받아들여야 하며, 핸들러는 예외로부터 액션의 타입으로 되돌아가는 방법을 설명해야 합니다.

class MonadExcept (ε : outParam (Type u)) (m : Type v Type w) where throw : ε m α tryCatch : m α (ε m α) m α

MonadExcept의 유니버스 레벨은 ExceptT의 유니버스 레벨과 다릅니다. ExceptT에서는 εα가 모두 같은 레벨을 가지는 반면, MonadExcept는 그런 제약을 두지 않습니다. 이는 MonadExcept가 예외 값을 m 내부에 절대 넣지 않기 때문입니다. 가장 일반적인 유니버스 시그니처는 이 정의에서 εα가 완전히 독립적이라는 사실을 인정합니다. 더 일반적이라는 것은 해당 타입 클래스가 더 다양한 타입에 대해 인스턴스화될 수 있음을 의미합니다.

MonadExcept를 사용하는 예제 프로그램은 간단한 나눗셈 서비스입니다. 이 프로그램은 두 부분으로 나뉩니다: 오류를 처리하는, 문자열 기반 사용자 인터페이스를 제공하는 프론트엔드와 실제로 나눗셈을 수행하는 백엔드입니다. 프론트엔드와 백엔드 모두 예외를 던질 수 있는데, 전자는 잘못된 형식의 입력에 대해, 후자는 0으로 나누기 오류에 대해 예외를 던집니다. 예외는 귀납적 타입입니다:

inductive Err where | divByZero | notANumber : String Err

백엔드는 0 여부를 확인하고, 가능하면 나눗셈을 수행합니다:

def divBackend [Monad m] [MonadExcept Err m] (n k : Int) : m Int := if k == 0 then throw .divByZero else pure (n / k)

프론트엔드의 헬퍼 asNumber는 전달받은 문자열이 숫자가 아니면 예외를 던집니다. 전체 프런트엔드는 입력을 Int로 변환하고 백엔드를 호출하며, 예외를 처리할 때는 친절한 문자열 오류를 반환합니다.

def asNumber [Monad m] [MonadExcept Err m] (s : String) : m Int := match s.toInt? with | none => throw (.notANumber s) | some i => pure idef divFrontend [Monad m] [MonadExcept Err m] (n k : String) : m String := tryCatch (do pure (toString ( divBackend ( asNumber n) ( asNumber k)))) fun | .divByZero => pure "Division by zero!" | .notANumber s => pure s!"Not a number: \"{s}\""

예외를 던지고 잡는 일은 흔하기 때문에 Lean은 MonadExcept를 사용하기 위한 특별한 구문을 제공합니다. +HAdd.hAdd의 축약형인 것처럼, trycatchtryCatch 메서드의 축약형으로 사용될 수 있습니다:

def divFrontend [Monad m] [MonadExcept Err m] (n k : String) : m String := try pure (toString ( divBackend ( asNumber n) ( asNumber k))) catch | .divByZero => pure "Division by zero!" | .notANumber s => pure s!"Not a number: \"{s}\""

ExceptExceptT 외에도, 언뜻 보기에 예외처럼 보이지 않을 수 있는 다른 타입들에 대해서도 유용한 MonadExcept 인스턴스가 존재합니다. 예를 들어, Option으로 인한 실패는 아무 데이터도 담지 않은 예외를 던지는 것으로 볼 수 있으므로, Option과 함께 try ...catch ... 구문을 사용할 수 있게 해 주는 MonadExcept Unit Option의 인스턴스가 존재합니다.

6.2.3. 상태🔗

모나드 액션이 시작 상태를 인자로 받고 그 결과와 함께 최종 상태를 반환하도록 함으로써, 가변 상태에 대한 시뮬레이션이 모나드에 추가됩니다. 상태 모나드의 바인드 연산자는 한 액션의 최종 상태를 다음 액션의 인자로 제공하여, 프로그램 전체에 걸쳐 상태를 이어 나갑니다. 이 패턴은 모나드 트랜스포머로도 표현할 수 있습니다:

def StateT (σ : Type u) (m : Type u Type v) (α : Type u) : Type (max u v) := σ m (α × σ)

다시 한번, 이 모나드 인스턴스는 State의 것과 매우 유사합니다. 유일한 차이점은 입력 상태와 출력 상태가 순수 코드로 전달되는 것이 아니라, 기반 모나드에서 전달되고 반환된다는 것입니다:

instance [Monad m] : Monad (StateT σ m) where pure x := fun s => pure (x, s) bind result next := fun s => do let (v, s') result s next v s'

해당 타입 클래스에는 getset 메서드가 있습니다. getset의 한 가지 단점은 상태를 갱신할 때 잘못된 상태를 set하기가 너무 쉬워진다는 것입니다. 이는 상태를 가져오고, 이를 갱신하고, 갱신된 상태를 저장하는 것이 일부 프로그램을 작성하는 자연스러운 방식이기 때문입니다. 예를 들어, 다음 프로그램은 문자열에서 발음 구별 부호가 없는 영어 모음과 자음의 개수를 셉니다:

structure LetterCounts where vowels : Nat consonants : Nat deriving Repr inductive Err where | notALetter : Char Err deriving Repr def vowels := let lowerVowels := "aeiuoy" lowerVowels ++ lowerVowels.map (·.toUpper) def consonants := let lowerConsonants := "bcdfghjklmnpqrstvwxz" lowerConsonants ++ lowerConsonants.map (·.toUpper ) def countLetters (str : String) : StateT LetterCounts (Except Err) Unit := let rec loop (chars : List Char) := do match chars with | [] => pure () | c :: cs => let st get let st' if c.isAlpha then if vowels.contains c then pure {st with vowels := st.vowels + 1} else if consonants.contains c then pure {st with consonants := st.consonants + 1} else -- modified or non-English letter pure st else throw (.notALetter c) set st' loop cs loop str.toList

set st' 대신 set st를 작성하는 실수를 저지르기가 매우 쉬울 것입니다. 대규모 프로그램에서는 이런 종류의 실수가 진단하기 어려운 버그로 이어질 수 있습니다.

get 호출에 중첩 액션을 사용하면 이 문제를 해결할 수 있겠지만, 그런 모든 문제를 해결할 수는 없습니다. 예를 들어, 어떤 함수는 다른 두 필드의 값을 기반으로 구조체의 한 필드를 업데이트할 수 있습니다. 이렇게 하려면 get에 대한 중첩 액션 호출을 두 번 별도로 수행해야 합니다. Lean 컴파일러에는 어떤 값에 대한 참조가 하나뿐일 때만 효과를 발휘하는 최적화가 포함되어 있으므로, 상태에 대한 참조를 복제하면 코드가 상당히 느려질 수 있습니다. 잠재적인 성능 문제와 잠재적인 버그는 모두 상태를 함수를 사용해 변환하는 modify를 사용함으로써 해결할 수 있습니다:

def countLetters (str : String) : StateT LetterCounts (Except Err) Unit := let rec loop (chars : List Char) := do match chars with | [] => pure () | c :: cs => if c.isAlpha then if vowels.contains c then modify fun st => {st with vowels := st.vowels + 1} else if consonants.contains c then modify fun st => {st with consonants := st.consonants + 1} else -- modified or non-English letter pure () else throw (.notALetter c) loop cs loop str.toList

이 타입 클래스는 modify와 유사한 modifyGet이라는 함수를 포함하는데, 이 함수를 사용하면 반환값을 계산하는 동시에 이전 상태를 변환하는 작업을 한 단계로 수행할 수 있습니다. 이 함수는 첫 번째 요소가 반환값이고 두 번째 요소가 새로운 상태인 쌍을 반환합니다. modifymodifyGet에서 사용된 쌍에 Unit의 생성자를 추가한 것일 뿐입니다:

def modify [MonadState σ m] (f : σ σ) : m Unit := modifyGet fun s => ((), f s)

MonadState의 정의는 다음과 같습니다:

class MonadState (σ : outParam (Type u)) (m : Type u Type v) : Type (max (u+1) v) where get : m σ set : σ m PUnit modifyGet : (σ α × σ) m α

PUnitType 대신 Type u에 있을 수 있도록 유니버스 다형성을 갖춘 Unit 타입의 버전입니다. getset을 이용하여 modifyGet의 기본 구현을 제공하는 것은 가능하겠지만, 애초에 modifyGet을 유용하게 만드는 최적화를 허용하지 않아 이 메서드를 무용지물로 만들 것입니다.

6.2.4. Of 클래스와 The 함수🔗

지금까지 MonadExcept의 예외 타입이나 MonadState의 상태 타입처럼 추가 정보를 받는 각 모나드 타입 클래스는 이러한 추가 정보의 타입을 출력 매개변수로 가지고 있습니다. 간단한 프로그램에서는 이 방식이 일반적으로 편리한데, StateT, ReaderT, ExceptT를 각각 한 번씩만 조합한 모나드는 상태 타입, 환경 타입, 예외 타입을 각각 단 하나씩만 가지기 때문입니다. 하지만 모나드가 복잡해질수록, 여러 상태나 오류 타입을 포함할 수 있습니다. 이 경우 출력 매개변수를 사용하면 동일한 do-블록 내에서 두 상태를 모두 대상으로 삼는 것이 불가능해집니다.

이런 경우를 위해, 추가 정보가 출력 매개변수가 아닌 별도의 타입 클래스가 존재합니다. 이러한 버전의 타입 클래스는 이름에 Of라는 단어를 사용합니다. 예를 들어, MonadStateOfMonadState와 비슷하지만 outParam 수식어가 없습니다.

outParam 대신, 이 클래스들은 각자의 상태, 환경, 예외 타입에 대해 semiOutParam을 사용합니다. outParam과 마찬가지로, semiOutParam도 Lean이 인스턴스 검색 과정을 시작하기 전에 알려져 있을 필요는 없습니다. 그러나 중요한 차이점이 있습니다: outParam은 인스턴스를 검색하는 동안 무시되며, 그 결과 진정한 출력이 됩니다. 검색 전에 outParam이 이미 알려져 있는 경우, Lean은 검색 결과가 이미 알려진 것과 같은지 확인하기만 합니다. 반면, 탐색이 시작되기 전에 이미 알려진 semiOutParam은 입력 매개변수와 마찬가지로 후보를 좁히는 데 사용될 수 있습니다.

상태 모나드의 상태 타입이 outParam인 경우, 각 모나드는 최대 한 종류의 상태만 가질 수 있습니다. 이는 타입 추론을 개선하기 때문에 편리합니다. 즉, 더 많은 상황에서 상태 타입을 추론할 수 있습니다. 이 또한 불편한데, StateT를 여러 번 사용하여 만든 모나드는 유용한 MonadState 인스턴스를 제공할 수 없기 때문입니다. 하지만 MonadStateOf를 사용하면 Lean은 어떤 인스턴스를 선택할지 결정할 때 상태 타입이 있을 경우 이를 고려하게 되므로, 하나의 모나드가 여러 종류의 상태를 제공할 수 있습니다. 이 방식의 단점은 상태 타입이 충분히 명시적으로 지정되지 않았을 때 그 결과로 생성되는 인스턴스가 의도한 것과 다를 수 있다는 점이며, 이는 혼란스러운 오류 메시지로 이어질 수 있습니다.

마찬가지로, 추가 정보의 타입을 암시적 인자가 아니라 explicit(명시적) 인자로 받는 버전의 타입 클래스 메서드도 있습니다. MonadStateOf의 경우, 다음과 같은 타입을 가진 getThe가 있습니다

(σ : Type u) {m : Type u Type v} [MonadStateOf σ m] m σ

그리고 modifyThe는 다음과 같은 타입을 가지고 있습니다

(σ : Type u) {m : Type u Type v} [MonadStateOf σ m] (σ σ) m PUnit

새로운 상태의 타입만으로도 어떤 주변 상태 모나드 트랜스포머를 사용할지 결정하기에 충분하므로 setThe는 존재하지 않습니다.

Lean 표준 라이브러리에는 Of가 붙지 않은 버전의 클래스 인스턴스가 Of가 붙은 버전의 인스턴스를 기반으로 정의되어 있습니다. 다시 말해, Of 버전을 구현하면 둘 다에 대한 구현을 얻게 됩니다. 일반적으로 Of 버전을 구현한 다음, 클래스의 Of가 아닌 버전을 사용해 프로그램을 작성하기 시작하고, 출력 매개변수가 불편해지면 Of 버전으로 전환하는 것이 좋은 방법입니다.

6.2.5. 트랜스포머와 Id🔗

항등 모나드 Id는 아무런 효과도 없는 모나드로, 어떤 이유로든 모나드가 필요한 맥락이지만 실제로는 아무것도 필요하지 않은 경우에 사용됩니다. Id의 또 다른 용도는 모나드 트랜스포머 스택의 맨 아래 역할을 하는 것입니다. 예를 들어, StateT σ IdState σ와 똑같이 작동합니다.

6.2.6. 연습문제🔗

6.2.6.1. 모나드 계약🔗

종이와 연필을 사용하여, 이 절의 각 모나드 트랜스포머에 대해 모나드 트랜스포머 계약의 규칙이 충족되는지 확인하십시오.

6.2.6.2. 로깅 트랜스포머🔗

WithLog의 모나드 트랜스포머 버전을 정의하십시오. 또한 이에 대응하는 타입 클래스 MonadWithLog를 정의하고, 로깅과 예외를 결합한 프로그램을 작성하십시오.

6.2.6.3. 파일 개수 세기🔗

doug의 모나드를 StateT로 수정하여 확인한 디렉터리와 파일의 개수를 세도록 하십시오. 실행이 끝나면 다음과 같은 보고서를 표시해야 합니다:

  Viewed 38 files in 5 directories.