functional programming을 위한 사고 확장 - 1부

Th Kang·2022년 2월 8일
5
post-thumbnail

함수형 프로그래밍의 위키피디아식 정의는 다음과 같다.

함수형 프로그래밍 (Functional programming) 자료 처리를 수학적 함수의 계산으로 취급하고 상태와 가변 데이터를 멀리하는 프로그래밍 패러다임의 하나. [1]

Haskell과 같은 함수형 프로그래밍 언어는 람다 대수 (Lambda Calculus) [2] 와 같은 수학적 체계의 영향을 받아서 학계를 중심으로 발전해 왔다. 그 개념의 탄생은 오래되었지만 필자가 함수형 프로그래밍 개념을 접한때는 막 4년차가 되던 해였다. 그 이후 2년 동안 Haskell, F#, Elm 과 같은 언어를 가지고 놀아보고 현업에서는 typescript를 활용해 그 개념을 적용해 보면서 느낀점을 정리해 보려한다. 특히 이전의 절차 중심 혹은 객체 중심의 개발방식과 어떤 부분에서 사고방식의 차이가 두드러지는지 살펴본다.

타입과 대응

타입과 각 타입이 어떻게 대응되는지를 중심으로 생각한다. 객체 지향으로 프로그램을 설계해 봤다면, 메모리 상에서 상호작용하는 객체의 모습을 상상해 봤을 것이다. 타입과 대응은 객체가 생존해있는 층보다 한 단계 높은 추상에 위치한다. 그렇다고 클래스 (class) 와 유사한 위치인가하면 그렇지도 않다. 왜냐하면 타입에는 동작이 없기 때문이다.

아래의 f# 코드 샘플을 살펴보자. 간단한 2개의 함수를 정의하고 마지막에 호출하는 코드이다. 문법적인 설명을 생략하고, 10 이라는 숫자를 넣으면 1~10 까지 숫자 각각을 제곱하여 ([1^2, 2^2, ..., 10^2]) 각 요소를 합하는 동작 (1^2 + 2^2 + ... + 10^2)을 한다.

// ref: https://fsharpforfunandprofit.com/posts/fvsc-sum-of-squares/

// define the square function
let square x = x * x

// define the sumOfSquares function
let sumOfSquares n =
   [1..n] |> List.map square |> List.sum

sumOfSquares 10

위의 코드를 읽을 때 머릿속에 절차지향적인 코드처럼 시간에 따라 코드를 따라 하나하나 따라가며 실행되는 모습을 상상한다면 함수형 프로그래밍의 주요 발상을 놓치고 있는 것이다. 시간 개념을 제외하고 대응관계만 살펴야 한다.

중학교 수학에서 배운 함수 그림을 하나 떠올려 보자. 두개의 타원과 화살표를 통해 정의역, 치역 개념을 표현하는 그림이다.

위의 모델을 그대로 적용하여 샘플코드에 적용하면 아래 그림과 같다. 세분화하여 크게 4개의 집합으로 나누었으며, 빨간색 텍스트는 정의한 함수에 대응한다. 그림을 유심히 살펴보면 square 함수가 그림 속 대응관계에 직접적으로 드러나지 않는다. 대응 관계에 대해 상세하게 살펴볼 때 Functor 개념을 설명하면서 그 이유에 대해 설명한다. 지금 여기에서는 직접적으로 드러나진 않지만 각 요소에 square 함수가 적용되었음을 이해하고 넘어간다.

이 대응 관계를 먼 발치에서 바라본다고 하자. 구체적인 요소는 보이지 않고 각 집합의 처음과 끝 대응 관계만 남기고 표기한다면, sumOfSqure: X -> T 방식으로 표기할 수 있다. 이렇게 우리는 함수 대응 관계를 그림으로 상상하고 표기법을 사용할 수 있다.

완전 다른 방식으로 프로그램 설계하기

위의 그림과 표기법을 가지고 무엇을 할 수 있을까? 간단하게 이미지처리하는 프로그램을 설계한다고 해보자. 이미지를 자르고(crop)하고, 크기를 조절(resize)할 수 있는 프로그램이다.

프로그램을 설계할 때, 하나의 세계를 만들어 어떤 대상이 존재하고 어떻게 상호작용하는지 상상해야 한다. 함수형으로 설계한다면 어떤 타입과 어떤 대응관계가 존재하는지 상상해야 한다. 아래의 예시를 참고하자.

BBox
- 왼쪽 상단을 원점으로 x,y 만큼 떨어져 있는 width와 height를 가지는 박스를 추상화한 타입

Size
- width와 height 가지는 크기를 추상화한 타입

Image
- 이미지를 추상화한 타입

crop: Image -> BBox -> Image
resize: Image -> Size -> Image

crop

이 함수의 표기법에 대해 조금 더 설명하자면, 함수형 언어들에서는 모든 함수는 하나의 입력과 하나의 출력을 가진다고 생각한다. 그래서 2개의 입력을 가지는 함수도 crop(image, bbox) -> image 분해하여 Image를 넣으면 BBox를 넣으면 Image를 리턴하는 함수를 상정한다. curring 개념 을 떠올리면 된다. javascript같은 언어로 구현한다면 newImage = crop(image)(bbox) 같은 API로 구현할 수 있다.

crop을 하는 동작 자체에 집중해보자. 이미지 일부를 잘라내는 행위(네모 박스 형태로만 자른다고 가정)를 떠올려보자. 우선 자를 대상이 되는 이미지가 있다. 그리고 어느 위치에 어떤 크기로 박스를 자를지 결정해야 한다. 그 결과로

위에 함수 대응 그림을 생각해 보면, 처음과 끝은 Image 집합으로 대응될 수 있는데, 중간에는 BBox 집합이 대응되는 그림은 아니다. 마치 합성 함수처럼 보이지만 미묘한 차이가 있다. square 예시처럼 중간에 숨겨진 다른 그림이 있음을 우선 인지하고 넘어가자. 자세한 내용은 뒤에서 설명한다.

조금은 다른 그림으로 생각해보면, 수 많은 이미지가 있고 수많은 BBox 가 있고 두 집합이 어떤 식으로 조합하니 새로운 이미지 집합이 대응되는 비유를 생각해보자. resize에 대해서도 한번 아래와 같은 방식으로 상상해보자.

이제야 함수형 사고방식을 익숙해 지기 위한 준비운동을 마쳤다. 다음 글에서 해상도를 높여 자세한 내용을 살펴본자. 쉽게 시작하기 위해 비유를 사용했기 때문에 완전히 정확하지 않은 부분이 있음을 고려해 주기를 바란다.

What's Next?

  • 타입
    - 변하지 않기
    - 시간과 함께 변하기
    - (보너스) 도메인 녹여내기
  • 대응
    - first-class, higher-order
    - 고해상도로 살펴보기
    - 방향 틀기
    - 반복
    - 자기 자신으로 대응
  • outro
    - 변하는 것과 변하지 않는 것
    - 새로운 사고 방식 확장
profile
생각을 개발하다.

0개의 댓글