함수형 프로그래밍에서 수학 법칙을 통해 어떻게 모듈성을 달성합니까?


11

내가 읽어 이 질문에 기능적인 프로그래머가 자신의 프로그램이 제대로 작동하는지 확인 증명을 사용하는 경향이있다. 이것은 단위 테스트보다 훨씬 쉽고 빠르지 만 OOP / 단위 테스트 배경에서 나온 것입니다.

설명해 주시고 예를 들어 주시겠습니까?


7
"이것은 단위 테스트보다 훨씬 쉽고 빠릅니다." 예, 소리가납니다. 실제로는 대부분의 소프트웨어에서 실제로 불가능합니다. 제목이 모듈성을 언급하면서 왜 검증에 대해 이야기하고 있습니까?
Euphoric

@Euphoric OOP의 단위 테스트에서는 검증을위한 테스트를 작성합니다. 소프트웨어의 일부가 올바르게 작동하는지 검증하고 문제가 분리되었는지 검증합니다 (예 : 모듈성 및 재사용 성).
leeand00

2
@Euphoric 돌연변이와 상속을 남용하고 결함이있는 유형 시스템 (예 :)을 가진 언어로 작업하는 경우에만 해당됩니다 null.
Doval

@ leeand00 "확인"이라는 용어를 잘못 사용하고 있다고 생각합니다. 모듈성 및 재사용 가능성은 소프트웨어 검증으로 직접 확인되지 않습니다 (물론 모듈성이 부족하면 소프트웨어를 유지 관리 및 재사용하기가 더 어려워서 버그가 발생하고 검증 프로세스가 실패 할 수 있음).
Andres F.

모듈 방식으로 작성된 소프트웨어의 일부를 확인하는 것이 훨씬 쉽습니다. 따라서 함수가 일부 함수에 대해 올바르게 작동하고 다른 함수에 대해서는 단위 테스트를 작성할 수 있다는 실제 증거가 있습니다.
grizwako

답변:


22

부작용, 무제한 상속 및 null모든 유형의 구성원 이기 때문에 OOP 세계에서는 증명이 훨씬 어렵습니다 . 대부분의 증거는 모든 가능성을 다룬다는 것을 증명하기 위해 유도 원리에 의존하며,이 세 가지가 모두 증명하기 어렵게 만듭니다.

정수 값을 포함하는 이진 트리를 구현한다고 가정 해 봅시다 (구문을 더 단순하게 유지하기 위해 일반적인 프로그래밍을하지는 않지만 아무것도 변경하지는 않습니다). 표준 ML에서는 다음과 같이 정의합니다. 이:

datatype tree = Empty | Node of (tree * int * tree)

이것은 tree값이 정확히 두 가지 종류 (또는 클래스의 OOP 개념과 혼동되지 않는 클래스)- Empty정보를 전달하지 않는 Node값과 처음과 마지막을 갖는 3 튜플 을 전달 하는 값으로 불리는 새로운 유형을 도입합니다. 요소는 tree이며 중간 요소는입니다 int. OOP에서이 선언에 가장 가까운 근사값은 다음과 같습니다.

public class Tree {
    private Tree() {} // Prevent external subclassing

    public static final class Empty extends Tree {}

    public static final class Node extends Tree {
        public final Tree leftChild;
        public final int value;
        public final Tree rightChild;

        public Node(Tree leftChild, int value, Tree rightChild) {
            this.leftChild = leftChild;
            this.value = value;
            this.rightChild = rightChild;
        }
    }
}

주의해야 할 점은 Tree 유형의 변수는 결코 될 수 없다는 것 null입니다.

이제 트리의 높이 (또는 깊이)를 계산하는 함수를 작성 max하고 더 큰 두 숫자를 반환 하는 함수에 액세스 할 수 있다고 가정 합니다.

fun height(Empty) =
        0
 |  height(Node (leftChild, value, rightChild)) =
        1 + max( height(leftChild), height(rightChild) )

우리는 height기능을 사례별로 Empty정의했습니다 Node. 나무에 대한 정의와 나무에 대한 정의가 있습니다. 컴파일러는 존재하는 트리 클래스 수를 알고 있으며 두 경우를 모두 정의하지 않으면 경고를 표시합니다. 식 Node (leftChild, value, rightChild)함수 서명에서이 변수 3 튜플의 값을 결합 leftChild, valuerightChild각각 우리 기능 정의에이를 참조 할 수 있도록. OOP 언어로 다음과 같은 지역 변수를 선언 한 것과 비슷합니다.

Tree leftChild = tuple.getFirst();
int value = tuple.getSecond();
Tree rightChild = tuple.getThird();

height올바르게 구현했다는 것을 어떻게 증명할 수 있습니까? 우리가 사용할 수있는 구조 유도 로 구성 : 그 증명 1. height우리의 (들) 기본 경우 올바른 tree(유형 Empty재귀 호출하는 것을 가정) 2. height올바른지, 그 증명 height(비 기본 케이스에 대한 올바른들 ) (나무가 실제로이면 Node).

1 단계에서 인수가 Empty트리 인 경우 함수가 항상 0을 반환 함을 알 수 있습니다 . 이것은 나무의 높이를 정의하면 정확합니다.

2 단계의 경우 함수는를 반환합니다 1 + max( height(leftChild), height(rightChild) ). 재귀 호출이 실제로 어린이의 키를 반환한다고 가정하면, 이것이 또한 올바른 것임을 알 수 있습니다.

그리고 그 증거를 완성합니다. 1 단계와 2 단계는 모든 가능성을 소진합니다. 그러나 우리에게는 돌연변이가없고, 널이 없으며, 정확히 두 종류의 나무가 있습니다. 이 세 가지 조건을 제거하면 실용적이지 않더라도 증거가 더 복잡해집니다.


편집 : 이 답변이 맨 위로 올라 갔으므로 증거의 간단한 예를 추가하고 구조적 유도를 좀 더 철저히 다루고 싶습니다. 위의 우리는 것을 증명 하는 경우 height반환 , 반환 값이 올바른지. 그러나 항상 값을 반환한다는 것을 증명하지는 못했습니다. 구조적 유도를 사용하여이를 증명할 수도 있습니다 (또는 다른 자산). 다시 2 단계에서 재귀 호출이 모두 직계 자식에 대해 작동하는 한 재귀 호출의 속성이 보류되는 것으로 가정 할 수 있습니다. 나무.

함수는 두 가지 상황에서 값을 반환하지 못할 수 있습니다. 예외가 발생하는 경우와 영원히 반복되는 경우입니다. 먼저 예외가 발생하지 않으면 함수가 종료된다는 것을 증명해 봅시다.

  1. 예외가 발생하지 않으면 기본 사례 ( Empty)에 대해 함수가 종료됨을 증명하십시오 . 무조건 0을 반환하기 때문에 종료됩니다.

  2. 기본이 아닌 경우 ( Node) 에서 함수가 종료되는지 증명하십시오 . 여기 +에는 max,, 및 세 가지 함수 호출이 있습니다 height. 우리는 알고 +max언어의 표준 라이브러리의 그들이 있기 때문에있는 거 부분을 종료하고 그들은 방법 것으로 정의하고있다. 앞에서 언급했듯이 재귀 호출이 즉각적인 하위 트리에서 작동하는 한 재귀 호출에 대해 입증하려는 속성이 true라고 가정 할 수 있으므로 호출도 height종료됩니다.

이것으로 증명이 끝납니다. 단위 테스트로는 종료를 증명할 수 없습니다. 이제 남은 것은 height예외를 발생시키지 않는 것을 보여주는 것입니다 .

  1. height기본 사례 ( Empty) 에서 예외가 발생하지 않음을 증명하십시오 . 0을 반환하면 예외를 throw 할 수 없으므로 끝났습니다.
  2. height기본이 아닌 경우 ( Node) 에서 예외가 발생하지 않는다는 것을 증명하십시오 . 우리가 예외를 알고 +있고 max던지지 않는다고 다시 가정하십시오 . 그리고 구조적 유도는 재귀 호출이 (나무의 직계 자식에 대한 작업 때문에) 발생하지 않을 것이라고 가정합니다. 이 함수는 재귀이지만 꼬리 재귀는 아닙니다 . 우리는 스택을 날려 버릴 수 있습니다! 시도한 증거로 버그가 발견되었습니다. 꼬리 재귀변경 height하여 수정할 수 있습니다 .

나는 이것이 증거가 무섭거나 복잡 할 필요가 없다는 것을 보여주기를 바란다. 실제로 코드를 작성할 때마다 비공식적으로 머리에 증거를 만들었습니다. 그렇지 않으면 함수를 구현했다고 확신 할 수 없습니다. null, 불필요한 돌연변이 및 무제한 상속을 피함으로써 직관이 상당히 쉽게 수정하십시오. 이러한 제한은 생각만큼 가혹하지 않습니다.

  • null 언어 결함이며 그것을 없애는 것은 무조건적으로 좋습니다.
  • 돌연변이는 때로는 피할 수없고 필요하지만 생각보다 훨씬 덜 필요합니다. 특히 지속적인 데이터 구조가있을 때 더욱 그렇습니다.
  • 유한 한 수의 클래스 (기능적 의미) / 서브 클래스 (OOP 의미)와 무제한 수 를 갖는 것은 단일 답변에 비해 너무 큰 주제 입니다. 정확성과 확장의 유연성의 가능성에 대한 디자인 트레이드가 있다고해도 충분합니다.

8
  1. 모든 것이 불변 인 경우 코드에 대해 추론하기가 훨씬 쉽다 . 결과적으로 루프는 재귀로 더 자주 작성됩니다. 일반적으로 재귀 솔루션의 정확성을 확인하는 것이 더 쉽습니다. 종종 이러한 솔루션은 문제의 수학적 정의와 매우 유사하게 읽 힙니다.

    그러나 대부분의 경우 실제 공식적인 정확성 증명을 수행하려는 동기는 거의 없습니다. 증명은 어렵고 시간이 많이 걸리며 ROI가 낮습니다.

  2. 일부 기능적 언어 (예 : ML 제품군)는 C 표현형 시스템을 훨씬 더 완벽하게 보장 할 수있는 매우 표현적인 유형 시스템을 가지고 있습니다 (그러나 일반 언어와 같은 일부 아이디어는 주류 언어에서도 공통적으로 사용됨). 프로그램이 유형 검사를 통과하면 일종의 자동 증거입니다. 경우에 따라 일부 오류를 감지 할 수 있습니다 (예 : 재귀에서 기본 사례를 잊어 버리거나 패턴 일치에서 특정 사례를 처리하지 못함).

    반면에, 이러한 유형의 시스템은 매우 그들을 유지하기 위해 제한 유지되어야한다 decidable . 따라서 어떤 의미에서 우리는 유연성을 포기함으로써 정적 인 보장을 얻습니다. 이러한 제한은“ 하스켈에서 해결 된 문제에 대한 모나드 솔루션 ”라인을 따라 복잡한 학술 논문이 존재하는 이유 입니다.

    나는 매우 자유로운 언어와 매우 제한된 언어를 모두 즐기고 있으며 각자 어려움이 있습니다. 그러나 하나가 더 나은 경우는 아니며, 각각 다른 종류의 작업에 더 편리합니다.

그런 다음 증명과 단위 테스트를 서로 바꿔서 사용할 수 없다는 점을 지적해야합니다. 둘 다 프로그램의 정확성에 한계를 둘 수 있습니다.

  • 테스트는 정확성에 상한선을 둔다 : 테스트가 실패하면 프로그램이 부정확하고, 테스트가 실패하지 않으면 프로그램이 테스트 된 사례를 처리 할 것이라고 확신하지만, 아직 발견되지 않은 버그가있을 수있다.

    int factorial(int n) {
      if (n <= 1) return 1;
      if (n == 2) return 2;
      if (n == 3) return 6;
      return -1;
    }
    
    assert(factorial(0) == 1);
    assert(factorial(1) == 1);
    assert(factorial(3) == 6);
    // oops, we forgot to test that it handles n > 3…
    
  • 증거는 정확성에 하한선을 두었습니다. 특정 속성을 증명하는 것이 불가능할 수 있습니다. 예를 들어 함수가 항상 숫자를 반환한다는 것을 쉽게 증명할 수 있습니다 (유형 시스템의 기능). 그러나 숫자가 항상임을 증명하는 것은 불가능할 수 있습니다 < 10.

    int factorial(int n) {
      return n;  // FIXME this is just a placeholder to make it compile
    }
    
    // type system says this will be OK…
    

1
"특정 속성을 증명하는 것이 불가능할 수도 있지만 ... 숫자가 항상 <10임을 증명하는 것은 불가능합니다." 프로그램의 정확성이 10보다 작은 숫자에 의존하는 경우이를 증명할 수 있어야합니다. 그것은 그 실제의 타입 시스템이 있지만, 당신이 할 수있는 - 할 수없는 (이상 유효하지 않은 프로그램의 톤을 배제하지 않고).
Doval

@Doval 예. 그러나 형식 시스템은 증명 용 시스템의 예일뿐입니다. 타입 시스템은 눈에 띄게 제한되어 있으며 특정 진술의 진실을 평가할 수 없습니다. 사람은 훨씬 더 복잡한 교정을 수행 할 수 있습니다,하지만 여전히 제한됩니다 것을 그가 증명할 수 있습니다. 여전히 넘어 질 수없는 한계가 있습니다. 더 먼 거리입니다.
amon

1
동의, 나는 단지 예제가 약간 오도 된 것이라고 생각한다.
Doval

2
Idris와 같이 의존적으로 유형이 지정된 언어에서는 10보다 낮은 값을 반환 할 수도 있습니다.
Ingo

2
@Doval이 제기하는 우려를 해결하는 더 좋은 방법은 일부 문제를 결정할 수 없거나 (예 : 정지 문제), 증명하는 데 너무 많은 시간이 필요하거나, 결과를 증명하기 위해 새로운 수학이 발견되어야한다는 것입니다. 내 개인적인 견해는 무언가가 사실로 판명되면 그것을 단위 테스트 할 필요 가 없다는 것을 분명히해야한다는 것입니다 . 증명은 이미 상한과 하한을 설정합니다. 증명과 테스트가 상호 교환되지 않는 이유는 증명이 너무 어려워서 또는 불가능한 일이기 때문입니다. 또한 코드가 변경 될 때 테스트를 자동화 할 수 있습니다.
Thomas Eding

7

경고 문구는 다음과 같습니다.

간단히 말해서, 다른 사람들이 여기에서 쓰는 것은 사실이지만, 간단히 말하면, 고급 유형 시스템, 불변성 및 참조 투명성이 정확성에 많은 영향을 미친다는 것은 기능적인 세계에서 테스트를 수행하지 않은 경우가 아닙니다. 반대로 !

테스트 케이스를 자동으로 무작위로 생성하는 Quickcheck와 같은 도구가 있기 때문입니다. 함수가 준수해야하는 법률을 명시한 다음 빠른 검사를 통해 수백 가지의 임의 테스트 사례에 대해 이러한 법률을 확인합니다.

보시다시피, 이것은 소수의 테스트 사례에서 사소한 평등 검사보다 약간 높은 수준입니다.

다음은 AVL 트리 구현의 예입니다.

--- A generator for arbitrary Trees with integer keys and string values
aTree = arbitrary :: Gen (Tree Int String)


--- After insertion, a lookup with the same key yields the inserted value        
p_insert = forAll aTree (\t -> 
             forAll arbitrary (\k ->
               forAll arbitrary (\v ->
                lookup (insert t k v) k == Just v)))

--- After deletion of a key, lookup results in Nothing
p_delete = forAll aTree (\t ->
            not (null t) ==> forAll (elements (keys t)) (\k ->
                lookup (delete t k) k == Nothing))

두 번째 법칙 (또는 속성)은 다음과 같이 읽을 수 있습니다. 모든 임의의 나무 t의 경우 다음을 보유합니다. t비어 있지 않은 경우 k해당 트리의 모든 키 에 대해 k삭제 결과 인 트리에서 해당 키 를 찾습니다. kfrom t결과는 Nothing(찾을 수 없음을 나타냄)입니다.

기존 키 삭제를위한 올바른 기능을 확인합니다. 존재하지 않는 키의 삭제에 적용되는 법률은 무엇입니까? 우리는 결과 트리가 삭제 한 트리와 동일하게되기를 원합니다. 우리는 이것을 쉽게 표현할 수 있습니다 :

p_delete_nonexistant = forAll aTree (\t ->
                          forAll arbitrary (\k -> 
                              k `notElem` keys t ==> delete t k == t))

이런 식으로 테스트는 정말 재미있다. 또한 빠른 검사 속성을 읽는 방법을 배우면 머신 테스트 가능 사양으로 사용 됩니다.


4

나는 "수학적 법칙을 통한 모듈화 달성"이라는 의미로 연결된 답변이 무엇을 의미하는지 이해하지 못하지만 의미가 무엇인지 생각합니다.

Functor를 확인하십시오 .

Functor 클래스는 다음과 같이 정의됩니다 :

 class Functor f where
   fmap :: (a -> b) -> f a -> f b

테스트 케이스가 아니라 충족시켜야 할 몇 가지 법률이 있습니다.

Functor의 모든 인스턴스는 다음을 준수해야합니다.

 fmap id = id
 fmap (p . q) = (fmap p) . (fmap q)

이제 Functor( source ) 를 구현한다고 가정 해 봅시다 .

instance  Functor Maybe  where
    fmap _ Nothing       = Nothing
    fmap f (Just a)      = Just (f a)

문제는 구현이 법률을 충족하는지 확인하는 것입니다. 어떻게 그렇게합니까?

한 가지 방법은 테스트 사례를 작성하는 것입니다. 이 접근법의 근본적인 한계는 유한 한 수의 경우 (8 매개 변수로 기능을 철저히 테스트하는 행운을 빕니다!)의 동작을 확인한다는 것이므로 테스트 통과는 테스트가 통과한다는 것 외에는 아무것도 보장 할 수 없습니다.

또 다른 접근법은 실제의 정의에 근거한 수학적 추론, 즉 증명을 사용하는 것입니다 (제한된 경우의 행동 대신). 여기서 아이디어는 수학적 증거가 더 효과적 일 수 있다는 것입니다. 그러나 이것은 프로그램이 수학 증명에 얼마나 적합한 지에 달려 있습니다.

위의 Functor사례가 법을 충족 한다는 실제 공식 증거를 안내 할 수는 없지만 증거가 어떻게 보이는지 간략하게 설명하겠습니다.

  1. fmap id = id
    • 우리가 있다면 Nothing
      • fmap id Nothing= Nothing구현의 1 부
      • id Nothing= Nothing의 정의에 의해id
    • 우리가 있다면 Just x
      • fmap id (Just x)= Just (id x)= Just x구현의 2 부에서 다음의 정의id
  2. fmap (p . q) = (fmap p) . (fmap q)
    • 우리가 있다면 Nothing
      • fmap (p . q) Nothing= Nothing1 부
      • (fmap p) . (fmap q) $ Nothing= (fmap p) $ Nothing= Nothing파트 1의 두 애플리케이션에 의해
    • 우리가 있다면 Just x
      • fmap (p . q) (Just x)= Just ((p . q) x)= Just (p (q x))2 부, 다음 정의.
      • (fmap p) . (fmap q) $ (Just x)= (fmap p) $ (Just (q x))= Just (p (q x))파트 2의 두 가지 애플리케이션

-1

"위의 코드에서 버그에주의하십시오; 나는 그것이 시도 된 것이 아니라 단지 올바른 것으로 증명되었습니다." -도널드 크 누스

완벽한 세상에서 프로그래머는 완벽하고 실수를하지 않기 때문에 버그가 없습니다.

완벽한 세상에서 컴퓨터 과학자와 수학자 또한 완벽하며 실수도하지 않습니다.

그러나 우리는 완벽한 세상에 살고 있지 않습니다. 따라서 우리는 프로그래머가 실수하지 않도록 의존 할 수 없습니다. 그러나 우리는 프로그램이 정확하다는 수학적 증거 를 제공하는 컴퓨터 과학자 가 그 증거에 실수를하지 않았다고 가정 할 수 없습니다 . 그래서 그의 코드가 작동한다는 것을 증명 하려는 사람에게는주의를 기울이지 않을 것 입니다. 단위 테스트를 작성하고 코드가 사양에 따라 작동 함을 보여줍니다. 다른 어떤 것도 나에게 아무것도 확신하지 못합니다.


5
단위 테스트에도 실수가있을 수 있습니다. 더 중요한 것은 테스트는 버그가 있다는 것만 보여줄 수 있다는 것입니다. @Ingo가 그의 답변에서 말했듯이, 그들은 훌륭한 위생 검사를하고 증거를 훌륭하게 보완하지만, 그것들을 대체하지는 않습니다.
Doval
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.