Today
-
Yesterday
-
Total
-
  • 함수적 프로그래밍 작성하기
    Programmer/Programming 2019. 12. 3. 16:14

    모든 조건과 결과는 함수 내부에서 시작하고 내부에서 끝난다면 함수적(functional)이다. 조건과 결과를 제외하고는 외부에 영향을 받지도 주지도 않는다. 조건은 인자(arguments)로 주어지고 결과를 반환값(return value)으로 돌려준다.

     

    수학에서 함수를 처음 배울 때 위와 아래가 열려있는 상자 그림을 소개한다. 이 상자는 위 구명으로 값을 넣으면 아래 구멍으로 결과가 나온다. 위/아래 구멍을 제외하고 비밀스러운 다른 구멍이 없다. 즉, 출력값은 오직 입력으로만 결정된다. 좀 더 세련되게 표현하면 정의역의 임의의 x에 대해서 사상된 단 하나의 y가 치역에 존재한다.

     

     

    프로그래밍의 함수는 종종 함수적이지 않다. 위 함수 정의에 부합하려면 인자와 반환값으로만 소통해야 한다. 반면 프로그래밍 함수의 일부는 (1) 레퍼런스나 포인터로 인자를 받아서 인자의 상태를 바꾸거나, (2) 전역 변수나 멤버 변수를 사용하거나 (3) File, Socket, IPC 등으로 외부와 소통을 한다. 이는 함수의 시그니처에는 드러나지 않는 비밀스러운 구멍이다.

     

    이러한 것들을 함수형 언어에서는 사이드 이펙트(side-effect)라고 부른다. 프로그래머라면 전역 변수 때문에 발생하는 버그로 고생을 한 적이 있지 않은가? 이처럼 사이드 이펙트는 버그를 만들기 쉽고 찾기도 어렵게 만든다.

     

    함수적으로 작성하면 (1) 안전한 코드를 짤 수 있고 (2) 재사용이 쉬우며 (3) 테스트에 용이하다.

     

    프로그램 전부를 함수적으로 작성하는 것은 어렵다. 대부분의 프로그램은 외부-사용자, 연동 시스템 등-과 소통을 해야하기 때문이다. 따라서 가능하면 함수적인 부분을 늘리는 것이 현실적이다. 방법은 함수적인 부분과 그렇지 않은 부분을 분리하는데 있다. 애초부터 함수형 언어로 시작했다면 이러한 습관이 들었겠지만, 대부분 알골(ALGOL) 계열(C, C++, Java, C#, Python, ...)으로 프로그래밍을 시작하여 사이드 이펙트에 둔감하다.

     

    함수적인 부분과 그렇지 않은 부분을 분리하는 몇가지 예를 들어보겠다.

     

    예1) 전역 변수나 멤버 변수를 다루는 함수의 리펙토링

     

    아래와 같이 전역 변수나 멤버 변수를 다루는 함수가 있다면, 이것을 인자로 받는 함수를 하나 더 만들어라. 

    var globalDb List
    
    // ...
    
    func AddRecord(record Record) error {
        // globalDb에 record를 추가한다.
    }

     

    위에 AddRecord 함수는 전역변수 globalDb에 record를 추가한다. 시그나처만 보고는 알 수 없는 사이드 이펙트이다. 함수적인 부분을 분리해보자.

     

    var globalDb List
    
    // ...
    
    func AddRecord(record Record) error {
        globalDb, err := appendRecord(globalDb, record)
        if err != nil {
            return err
        }
        return nil
    }
    
    func AppendRecord(db List, record Record) (List, error) {
        // record를 추가한 db를 반환한다.
    }

     

    완전 함수적으로 작성하려면 인자로 받은 db의 변경 없이 새로운 List를 반환해야 한다. 허나 데이터 복사 등의 성능 이유로 인자로 받은 db에 추가하는 것이 선택할 수 있다. 어느 방식을 선택해도 AppendRecord 함수는 인자가 반환값을 결정한다. 숨어있는 다른 입력이 없음으로 안전하고 재사용이 쉽다. 무엇보다 선/후행조건과 불변식으로 함수를 검증하거나 유닛 테스트를 작성하기 용이하다.

     

    매개 변수의 개수를 줄이는 테크닉으로 전역 변수나 멤버 변수 사용을 장려하는 글을 종종 본다. 이는 나쁜 습관을 만드는 가르침이라고 생각한다. 사이드 이펙트가 반드시 필요한 함수라고 하더라고 위와 같은 방법으로 충분히 함수적인 부분을 뽑아낼 수 있다.

     

    예2) 외부(I/O, 이벤트, 등)와 소통하는 함수

     

    외부와 소통하는 부분과 그렇지 않은 부분을 분리하다. 예를 들면, GUI에서 값을 받아서 계산하는 코드를 하나로 묶어서 작성하지 않는다. 계산하는 코드를 별개의 함수로 분리하고, GUI에서 받은 값을 인자로 전달한다. 이렇게 하면 부작용이 있는 코드를 최대한 격리할 수 있다. 이 계산하는 함수는 사이드 이펙트가 없으니 안전하고 테스트하기 쉽우며  재사용하기도 쉽다.

     

    예3) 로깅, assert의 사용

     

    마지막으로 언급하고 싶은 부분은, 로깅이나 assert에 관한 부분이다. 함수적인 함수에서는 이런 것을 사용하지 않는다. 이런 함수를 호출하기 전에 인자들과 호출 후에 리턴값을 기록하면 충분하다. 버그가 의심된다면 기록된 인자와 리턴값으로 쉽고 정확하게 재현할 수 있다. 함수적인 함수는 인자와 리턴값 이외에 영향을 주는 다른 요소가 없기 때문이다.

    반면, 사이드 이펙트가 있는 함수에서는 로깅과 assert을 필요한만큼 충분히 사용한다. 이 함수에서는 숨어있는 구멍으로 예상치 못한 온갖 것들이 들어오기 때문에 인자만 가지고 재현할 수 없다. 함수 수행 과정 여기 저기에 시스템의 상태를 기록해야 한다.

    댓글

Designed by Tistory.