콘텐츠로 건너뛰기
Home » Functor의 개념과 Swift내의 functor

Functor의 개념과 Swift내의 functor

배열은 Swift에서 동일 타입의 원소를 하나 이상 가질 수 있는 순서있는 집합을 나타내는 자료 구조이다. 배열은 Swift 뿐만 아니라 거의 모든 프로그래밍 언어에서 가장 기본적이며 중요한 자료 구조 중 하나인데, 특히 함수형 프로그래밍 언어에서는 배열을 리스트라고 하는 재귀적인 데이터 구조를 사용하여 일련의 연속적인 값들을 집합으로 다루게 된다. 배열 혹은 리스트에 대한 연산 중에서 가장 대표적인 것은 맵핑 연산이다.  하스켈의 fmap 은 임의의 타입의 원소를 갖는 리스트의 각 원소에 대해 주어진 함수를 적용하여 새로운 리스트를 생성한다. 그리고 대부분의 배열을 지원하는 현대 언어들도 비슷한 기능을 가지고 있다. 자바 스크립트의 Array와 Swift의 Array도 공통적으로 map이라는 메소드를 통해서 변형(transform)함수를 받아 자신의 원소들에게 적용하여 유사한 동작을 수행한다.

보통은 이러한 map 동작을 배열 타입의 메소드로 이해하고 (그것이 틀린 말은 아니지만) 사용한다. 여기서 배열은 일종의 자료 구조로서의 기능의 구현으로 보는 시각이다. 그런데 조금 다른 시각으로 이를 바라 볼 수 있지 않을까.

배열은 하나 이상의 값을 갖고 있는 컨테이너라고 보는 것이다. 이 역시 너무나 당연한 사실이기 때문에 받아들이는데 별 어려움이 없을 것이다.  let array = [1, 2, 3, 4]라는 표현은 arr이 정수 4개가 들어있는 배열이며, 배열 전체를 봤을 때는 내부에 정수 값 4개를 감싸고 있는 컨테이너인 셈이다.

이 때 map 동작은 컨테이너 내부에 들어있는 각각의 정수값에 어떠한 변형을 가한다. 이 지점에서도 관점을 컨테이너 중심으로 옮기게 되면 maptransform 이라는 어떤 A 타입의 값을 B타입의 값으로 바꿀 수 있는 함수를 컨테이너의 “내부”에 적용하는 연산이라 볼 수 있다.

[1,2,3,4]라는 리스트에 y = x * x 라는 변형을 적용했다.

이 때 맵핑 자체를 배열에 대한 연산으로 보기 위해서는 .map 이라는 메소드 형태보다는 fmap 이라는 자유 함수 형태로 보는 것이 좀 더 편할 것이다. 따라서 다음과 같은 함수를 하나 선언한다.

func fmap<A, B>(_ anArray: [A], _ tranform: (A) -> B) -> [B] {
  return anArray.map(transform)
}

이제 위에 있는 그림은 fmap([1, 2, 3, 4], { $0 * $0 })이라고 쓰여질 수 있다.  이는 배열 [1, 2, 3, 4]에 대해서 입력값을 제곱하는 “변형”을 각 원소에 적용한다는 것이다. 내부 원소에 특정한 변형을 적용하는 것을 “변형을 사상한다(mapping over an transform)”라고 표현할 수 있다.

이 과정에서 타입과 관련해서 원소의 타입은 변형함수에 의해서 변경될 수 있지만 ((A) -> B ) 배열(혹은 리스트)라는 컨테이너의 구성 자체가 바뀌지는 않는다는 것이다.

Functor

Functor(함자)는 어떤 변형(함수나 연산)을 사상할 수 있는 것을 말한다. 즉 어떤 것이든 변형을 적용할 수 있다면 그것은 functor가 된다고 할 수 있다. 앞에서 살펴본 예에서 배열(리스트)은 이미 functor라는 것을 확인했다. 수학에서 functor는 몇 가지 규칙을 따르는데, 이는 다음과 같다.

  1.  항등 사상의 보존 : 항등함수 id에 대해서 X에 대한 사상 F는 F(id_{x}) = id_{F(X)}를 만족한다.
  2. 사상 합성의 보존 : f:X \rightarrow Yg:X \rightarrow Y에 대해서 F(g \circ f) = F(g) \circ F(f)를 만족한다.

이 규칙에 대해서 크게 신경 쓸 필요는 없다. 찬찬히 읽어보면 그냥 당연한 소리를 써놨을 뿐인 셈이다.  Functor는 결국 변형 연산을 적용할 수 있는 그 어떤 것이든 될 수 있고, 어떤 타입이 functor라면 그 타입을 받는 fmap 함수를 작성할 수 있다는 말이 된다.

다른 functor 들

그렇다면 배열만이 가능한 functor인가? 그렇지는 않다. 집합(Set)타입에 대해서 생각해보자. Swift의 Array와 Set은 중복을 허용하고, 순서가 있는지에 대한 차이가 있을 뿐, “여러 개의 값을 내재하는 컨테이너”라는 맥락은 동일하다. 그리고 위에서 본 다이어그램과 마찬가지로 임의의 변형 f를 사상하는 동작 자체가 Set이라고 불가능하지는 않다.

실제로 Xcode 9.0부터 지원하는 Swift 4에서는 이 Set 타입에 대한 map 동작이 기본적으로 라이브러리에 포함되어 있다.

그렇다면 같은 이치로 Dictionary 타입 역시 함자일까? 그렇다 키-값 쌍의 조합으로 데이터를 저장하는 집합인 사전의 경우에도 변형 함수를 받아서 각 값에 적용하는 사상이 가능하다. (이 역시 Swift 4에서 map이 가능하게끔 추가되었다.)

기본적으로 제공되는 자료 구조 타입이 아니더라도 functor가 될 수 있는 타입은 이런 집합 컨테이너 타입이라면 어떤 것도 functor가 될 수 있을 것이다. 이진 트리, 트리, Trie 등도 모두 변형을 사상할 수 있기 때문이다.

옵셔널

하지만 “집합 컨테이너 == functor” 라는 생각으로 이를 고착화하지는 말자. 왜냐하면 집합이 아닌 컨테이너에 대해서도 이제는 좀 신경을 써야 하기 때문이다. 바로 옵셔널이 그렇다. 옵셔널은 “값이 없을 수도 있을 때” 쓰는 타입인데, Swift에서는 내부적으로 enum을 이용해서 구현하고 있다.

enum Optional<T> {
  case none
  case some(T)
}

따라서 값이 있을 수도 있고, 없을 수도 있는 저 컨테이너에 대해서 앞서 작성했던 것과 같은 fmap 함수를 작성한다면 아래와 같을 수 있다.

func fmap<A, B> (_ x: Optional<A>, _ transform: (A) -> B) -> Optional<B> {
  switch x {
  case .some(let x): return .some(transform(x))
  case .none: return .none
  }
}

이는 다시 평범한(?) 옵셔널을 쓰는 문법으로 바꿔 쓴다면 아래와 같이 쓸 것이다.

func fmap<A, B> (_ x: A?, _ transform: (A) -> B) -> B? {
  guard let x = x else { return nil }
  return transform(x)
}

앞서 우리는 fmap 함수가 첫 인자의 .map() 을 호출하는 것과 같다는 가정을 하였다. Swift에서 옵셔널은 사실 알게 모르게 이미 functor 이며, 실제로 .map() 메소드를 가지고 있다.

컨테이너 보다는 컨텍스트

배열, 사전, 집합과 같은 컬렉션 컨테이너 외에도 옵셔널과 같은 컨텍스트도 functor가 될 수 있음을 보았다. 이는 다시 말하자면 컨테이너 자체를 컨텍스트로 볼 수 있다는 말이다. 옵셔널은 어떤 값에 대해서 그 값이 없을 수 있다는 부재의 컨텍스트가 덧붙여진 값이다. 같은 맥락으로 배열이나 사전은 어떤 값에 대해서 그 값이 1개가 아닐 수 있다는 컨텍스트를 덧붙인 값이라 볼 수 있다. Functor를 컨테이너가 아닌 컨텍스트로 볼 때, 우리는 또 하나의 functor를 찾을 수 있다.

함수라는 컨텍스트

리스트가 하나 이상의 값일 수 있다는 문맥, 옵셔널이 값이 없을 수도 있다는 문맥이라면 함수는 어떠한가? 함수는 “어떤 값을 주면 그것을 변경하는 문맥”으로 이해할 수 있다.  이 역시 어떤 값에 부가적인 연산을 포함하는 문맥인 셈이며, 그렇다면 functor가 될 수 있다.

리스트나 옵셔널에 대해서 변형을 사상하는 것은 결국 리스트나 옵셔널로부터 값을 꺼내어 볼 때, 그 변형이 적용되어 있는 것이다. 따라서 함수의 경우에는 (함수는 어찌보면 값을 가두어 둘 수 없는 컨테이너라 볼 수도 있다.) 원래의 동작에 의해 값이 변형되는 것 외에 부가적으로 사상된 변형에 의해서 값이 한 번 더 변형된다고 볼 수 있다.

이는 수학시간에 배운 두 함수의 합성과 동일하며, 함수 합성은 함수 그 자체가 functor라는 것을 보여주는 반례인 셈이다. 역시 기술적으로 임의의 함수에 대해서 우리는 fmap 함수를 아래와 같이 기술 할 수 있다.

func fmap<A, B, C> (_ x: (A) -> B, _ tranform: (B) -> C) -> (A) -> C {
  return { a in transform(x(a)) }
}

보너스: Functor를 프로토콜로 정의할 수 있을까?

Functor의 특징은 그 동작으로서 ‘무언가를 사상할 수 있는 것’이라고 하였다. 그렇다면 functor인 여러 타입들에 있어서 공통적으로 나타나는 동작인데, 그것을 Swift에서 프로토콜이나 타입으로 표현할 수 있을까?

하스켈에서 functor는 실제로 Functor라는 타입 클래스1로 정의되어 있다.  비슷하게 Functor라는 프로토콜을 Swift에서 만들 수 있을까?

결론부터 말하자면 아직까지는 가능하지 않다. Swift의 타입 시스템은 Higher-Kinded하지 못하기 때문이다. 만약 사상되는 변형이 값은 변하되 타입을 변하지 않는 endofunctor라면 가능하겠지만, 변형 함수는 내재된 값의 타입을 변형하는 경우도 허용해야 하기 때문이다.

/// !!! 동작하지 않는 코드이다.
protocol Functor {
  associatedType F
  func map<G> (_ tranform: (F) -> G) -> Functor
}

대략 모양은 위와 같아야 하지 싶지만,  map 메소드의 리턴 값이 다른 타입과 연계된 Functor가 되어야 하고, 이 경우 제네릭 프로토콜은 리턴 타입이 아닌 타입 제한으로만 사용이 가능하기 때문이다. (그리고 이게 문법적으로 가능했다면, 표준 라이브러리에 벌써 추가됐겠지)


  1. 하스켈의 타입 클래스는 어떤 타입을 특정하지 않고, 임의의 타입이 수행할 수 있는 동작을 말한다. 이 컨셉은 Swift의 프로토콜과 매우 유사하다.