잠정 정의

잠정 정의 (Provisional Definitions)

때로는 의존 타입으로 프로그래밍할 때, 타입 검사기가 요구하는 타입과 우리가 작성한 프로그램의 타입이 다를 수 있어요 (정규 형식이 같지 않다는 점에서), 하지만 그럼에도 증명 가능하게 같을 수 있어요. 예를 들어 parity 함수를 떠올려보세요:

data Parity : Nat -> Type where
   Even : Parity (n + n)
   Odd  : Parity (S (n + n))

이것을 다음과 같이 구현하고 싶어요:

parity : (n:Nat) -> Parity n
parity Z     = Even {n=Z}
parity (S Z) = Odd {n=Z}
parity (S (S k)) with (parity k)
  parity (S (S (j + j)))     | Even = Even {n=S j}
  parity (S (S (S (j + j)))) | Odd  = Odd {n=S j}

이것은 단순히 0은 짝수, 1은 홀수, 그리고 재귀적으로 k+2의 parity는 k의 parity와 같다고 진술해요. n의 값을 evenodd로 명시적으로 표시하는 것은 타입 추론을 돕는 데 필요해요.

안타깝게도 타입 검사기는 이것을 거부해요:

viewsbroken.idr:12:10:When elaborating right hand side of ViewsBroken.parity:
Type mismatch between
    Parity (plus (S j) (S j))
and
    Parity (S (S (plus j j)))

Specifically:
    Type mismatch between
        plus (S j) (S j)
    and
        S (S (plus j j))

타입 검사기는 (j+1)+(j+1)2+j+j가 같은 값으로 정규화되지 않는다고 알려줘요. 이것은 plus가 첫 번째 인자에 대해 재귀적으로 정의되고, 두 번째 값에서는 두 번째 인자에 후임자 기호가 있기 때문에 축소에 도움이 되지 않기 때문이에요. 이 값들은 분명히 같아요 — 이 문제를 고치도록 프로그램을 어떻게 다시 쓸까요?

잠정 정의 (Provisional definitions)

잠정 정의는 증명 세부 사항을 더 나중 시점까지 미룰 수 있게 해서 이 문제를 도와줘요. 유용한 주된 두 가지 이유가 있어요.

  • 프로토타이핑할 때, 증명의 모든 세부 사항을 끝내기 전에 프로그램을 테스트할 수 있는 것이 유용함.

  • 프로그램을 읽을 때, 독자가 밑바탕의 알고리즘에서 주의를 분산시키지 않도록 증명 세부 사항을 미루는 것이 훨씬 명확한 경우가 많음.

잠정 정의는 오른쪽을 = 대신 ?=로 도입한다는 점을 제외하면 일반 정의와 같은 방식으로 작성돼요. 우리는 parity를 다음과 같이 정의해요:

parity : (n:Nat) -> Parity n
parity Z = Even {n=Z}
parity (S Z) = Odd {n=Z}
parity (S (S k)) with (parity k)
  parity (S (S (j + j))) | Even ?= Even {n=S j}
  parity (S (S (S (j + j)))) | Odd ?= Odd {n=S j}

이 형태로 작성하면, 타입 오류를 보고하는 대신 Idris는 타입 오류를 고칠 정리를 나타내는 홀을 삽입해요. Idris는 모듈과 함수 이름에서 생성된 이름을 가진 두 개의 증명 의무(proof obligations)가 있다고 알려줘요:

*views> :m
Global holes:
        [views.parity_lemma_2,views.parity_lemma_1]

첫 번째는 다음 타입을 가져요:

*views> :p views.parity_lemma_1

---------------------------------- (views.parity_lemma_1) --------
{hole0} : (j : Nat) -> (Parity (plus (S j) (S j))) -> Parity (S (S (plus j j)))

-views.parity_lemma_1>

두 인자는 패턴 매치에서 범위에 있는 변수 j와, 잠정 정의의 오른쪽에서 준 값인 value이에요. 우리의 목표는 이 값을 사용할 수 있도록 타입을 다시 쓰는 것이에요. 프렐류드의 다음 정리를 사용해 이를 달성할 수 있어요:

plusSuccRightSucc : (left : Nat) -> (right : Nat) ->
  S (left + right) = left + (S right)

plus의 정의를 펼치기 위해 다시 compute를 사용해야 해요:

-views.parity_lemma_1> compute


---------------------------------- (views.parity_lemma_1) --------
{hole0} : (j : Nat) -> (Parity (S (plus j (S j)))) -> Parity (S (S (plus j j)))

intros를 적용한 후:

-views.parity_lemma_1> intros

  j : Nat
  value : Parity (S (plus j (S j)))
---------------------------------- (views.parity_lemma_1) --------
{hole2} : Parity (S (S (plus j j)))

그 다음 plusSuccRightSucc 다시 쓰기 규칙을 jj에 대해 대칭적으로 적용해요:

-views.parity_lemma_1> rewrite sym (plusSuccRightSucc j j)

  j : Nat
  value : Parity (S (plus j (S j)))
---------------------------------- (views.parity_lemma_1) --------
{hole3} : Parity (S (plus j (S j)))

sym은 다시 쓰기의 순서를 뒤집는 라이브러리에 정의된 함수예요:

sym : l = r -> r = l
sym Refl = Refl

전제에서 value를 찾는 사소한(tivial) 택틱을 사용해 이 증명을 완성할 수 있어요. 두 번째 보조 정리의 증명은 정확히 같은 방식으로 진행돼요.

이제 The with rule — matching intermediate values 절의 natToBin 함수를 프롬프트에서 테스트할 수 있어요. 숫자 42는 이진수로 101010이에요. 이진 숫자는 뒤집혀 있어요:

*views> show (natToBin 42)
"[False, True, False, True, False, True]" : String

불신의 정지 (Suspension of Disbelief)

Idris는 프로그램을 컴파일하기 전에 증명이 완성되기를 요구해요 (프롬프트에서의 평가는 증명 세부 사항 없이도 가능하지만). 때로는, 특히 프로토타이핑할 때, 이것을 하지 않는 것이 더 쉽게 느껴질 수 있어요. 프로그램에 대해 증명을 시도하기 전에 테스트하는 것이 이로울 수도 있어요 — 테스트가 오류를 찾으면, 무엇인가를 증명하는 데 시간을 낭비하지 않는 편이 낫다는 것을 알게 되니까요!

따라서 Idris는 잘못된 타입의 값을 사용할 수 있게 해주는 내장 강제 함수(coercion function)를 제공해요:

believe_me : a -> b

분명히 이것은 극도로 주의해서 사용해야 해요. 프로토타이핑할 때 유용하고, 외부 코드(어쩌면 외부 C 라이브러리)의 속성을 단언할 때도 적절할 수 있어요. 이것을 사용한 views.parity_lemma_1의 "증명"은 다음과 같아요:

views.parity_lemma_2 = proof {
    intro;
    intro;
    exact believe_me value;
}

exact 택틱은 증명에 대한 정확한 값을 제공할 수 있게 해줘요. 이 경우 우리가 준 값이 정확하다고 단언해요.

예: 이진수 (Example: Binary numbers)

앞서 Parity 뷰를 사용해 이진수로의 변환을 구현했어요. 여기서는 같은 뷰를 사용해 검증된 이진수 변환을 구현하는 방법을 보여줄게요. 이진수를 그 Nat 등가물로 인덱싱하는 것으로 시작해요. 이것은 표현(이 경우 Binary)을 의미(이 경우 Nat)와 연결하는 흔한 패턴이에요:

data Binary : Nat -> Type where
   BEnd : Binary Z
   BO : Binary n -> Binary (n + n)
   BI : Binary n -> Binary (S (n + n))

BOBI는 이진수를 인자로 취하고, 사실상 그것을 한 비트 왼쪽으로 시프트해 새 최하위 비트로 0 또는 1을 더해요. 인덱스 n + n 또는 S (n + n)은 이 왼쪽 시프트 후 덧셈이 숫자의 의미에 가질 결과를 진술해요. 이것은 최하위 비트가 맨 앞에 있는 표현을 만들 거예요.

이제 Nat를 이진수로 변환하는 함수는, 타입에서 결과 이진수가 원래 Nat의 충실한 표현이라고 진술할 거예요:

natToBin : (n:Nat) -> Binary n

Parity 뷰는 정의를 꽤 간단하게 만들어요 — 숫자를 반으로 나누는 것이 결국 효과적으로 오른쪽 시프트니까요 — 비록 Odd 경우에는 잠정 정의를 사용해야 하지만요:

natToBin : (n:Nat) -> Binary n
natToBin Z = BEnd
natToBin (S k) with (parity k)
   natToBin (S (j + j))     | Even  = BI (natToBin j)
   natToBin (S (S (j + j))) | Odd  ?= BO (natToBin (S j))

Odd 경우의 문제는 parity의 정의에서와 같으며, 증명도 같은 방식으로 진행돼요:

natToBin_lemma_1 = proof {
    intro;
    intro;
    rewrite sym (plusSuccRightSucc j j);
    trivial;
}

마무리하려면, 사용자로부터 정수를 읽어 이진수로 출력하는 메인 프로그램을 구현할게요.

main : IO ()
main = do putStr "Enter a number: "
          x <- getLine
          print (natToBin (fromInteger (cast x)))

물론 이것이 작동하려면 Binary n에 대한 Show 구현이 필요해요:

Show (Binary n) where
    show (BO x) = show x ++ "0"
    show (BI x) = show x ++ "1"
    show BEnd = ""

출처: 문서