함수형 프로그래밍 배경 지식 2 - 타입, 함수 합성, 부분 함수, 펑터(Functor)

Raymond Yoo·2023년 12월 11일
0
post-thumbnail

a type is a name for a set of things

타입(type)이란 무엇일까?
사실 타입에 대해서 얘기하려면 범주론부터 시작해서
추상 데이터 타입(Abstract Data Type) 등의 얘기를 해야하지만
여기서는 생각의 전개를 돕기 위해서 쉽게 접근하기로 한다.
타입은 어떤 것들의 집합(a set of things)에 이름을 붙이는 것이다.
집합이라고 하면 하나의 집합에 속한 요소끼리는
서로 공통된 속성을 공유하고 있으므로
각각의 요소가 동작하는 방식이나
해당 집합에 속한 요소에 접근하는 방식이 일원화된다.

함수형 프로그래밍에서 타입은 함수를 표현할때 사용한다.
함수의 입력값은 특정한 타입을 지니고
출력값도 특정한 타입을 지닌다.
그리고 함수조차도 입력값, 출력값 쌍으로 추론되는 타입을 갖는다.

여러가지 타입

왼쪽의 예시는 정수 타입이고
가운데는 문자열 타입이고
오른쪽은 사람 타입이다.

여러가지 타입 2

이렇게 표현하면 과일타입이 된다.
함수도 타입으로 표현할 수 있다.
이렇게 하면 입력이 과일 타입이고 출력이 과일타입인 함수,
줄여서 Fruit-to-Fruit 타입의 함수가 된다.

함수 사상 그림 A -—f--> B

처음에 봤던 함수 그림으로 다시 돌아가서 시작해보자.
수학에서는 A 집합의 모든 요소 각각에서
B 집합의 어떤 하나의 요소에 대응되는 관계를 f 라고 한다.
이를 프로그래밍에서는
함수 f 의 입력타입이 A 이고 출력타입이 B 라고 말한다.

함수 합성

그림처럼 여러 개의 함수가 있을때
함수 f 의 입력타입은 A 이고 출력타입은 B 이다.
함수 g 의 입력타입은 B 이고 출력타입은 C 이다.
이렇게 f 와 g 의 관계처럼
한 함수의 출력타입과 다른 함수의 입력타입이 같은 타입이라면
함수 f 와 g 를 합성해서 새로운 함수처럼 사용할 수 있다.
이렇게 합쳐서 얻어낸 새로운 함수 g o f 는
입력타입이 A 이고 출력타입이 C 인 함수가 된다.
이를 함수 합성(function composition)이라고 한다.

부분 함수

지금까지 설명을 할때 항상
함수는 입력타입과 출력타입이 있고
해당 타입에 맞는 입력값을 전달해서 호출하면
명시한 출력타입에 맞는 값을 반환한다고 서술했다.

하지만 프로그래밍 세계에서 실제로는 꼭 그렇지만은 않다.
명세 타입에 맞는 값을 아규먼트로 입력한다고 하더라도
어떤 이유에서든 함수는 예외를 던질 수가 있다.
어떤 함수가 예외를 던진다면
더 이상 입력값에 대해서 올바른 출력값을 내보내는 함수가 아니다.
치밀한 이론과 완벽한 논리로 무장한 수학의 세계에서와 달리
프로그래밍의 세계에서 함수는 거짓말을 친다.
이런식으로 올바른 타입의 값을 입력하더라도
출력타입에 맞는 리턴값을 되돌려주지 않는 함수를
부분 함수(partial function)라고 한다.

Functor - f, g, map, map(g)

함수형 프로그래밍 관련해서 중요한 개념 중에는
Functor 라는 것이 있다.
함수형 프로그래밍에서 부수효과를 다루는데
핵심이 되는 개념이므로 한번 살펴보고 넘어가자.

이해를 돕기 위해서 그림을 그려보겠다.
여기 그림들에서 '모양 + 색깔' 조합이 하나의 타입을 의미한다.
모양과 색깔이 모두 같은 컴포넌트는 서로 같은 타입이고
모양과 색깔중에 하나라도 다르면 서로 다른 타입이다.
먼저 가장 왼쪽 위에 있는 파란색 원 컴포넌트는 어떤 타입이다.
파란색원 컴포넌트를 입력으로 해서 펑터(Functor) F 를 호출하면
박스 안에 담긴 파란색 원 컴포넌트가 된다.
박스 안에 담긴 파란색 원 컴포넌트는
위에서 언급한 wrapper 타입이라고 생각하면 된다.
파란색 원을 입력으로 해서 함수 g 를 호출하면 노란색 원을 출력한다.
노란색 원 컴포넌트와 박스 안에 담긴 노란색 원 컴포넌트는 서로
펑터(Functor) 관계에 있다.
여기서 맵(Map) 이라는 개념이 등장한다.
맵(Map) 은 함수를 들어올리는(life the function) 고차함수이다.
오른쪽에 있는 함수 각각의 시그니처를 보면
보다 이해가 쉬운데
함수 map 은 A -> B 라는 타입의 함수를 파라미터로 받아서
F[A] -> F[B] 라는 타입의 함수를 반환한다.
그래서 함수 map(g) 는 F[A] -> F[B] 라는 시그니처를 갖는다.

객체지향 프로그래밍 언어에 익숙한 사람이라면
map 이라는 함수에서 낯선 느낌을 받을 것이다.
객체지향 언어에서는 map 함수를
어떤 클래스 안에 메서드로 구현했기 때문에
아이디어만 차용하고 구현방식에 맞춰서 타입을 변경하다보니
그렇게 된 듯하다.
본질적인 개념을 추적하다보면 map 함수에 대해서
여기서 설명하는 방식이 본래의 의미에
더 가깝다는 것을 알게 될 것이다.
전통적인 함수형 프로그래밍 언어에서는
클래스 개념이 없기 때문에
map 함수를 static function 으로 구현하게 되고
자연스럽게 함수 시그니처도 (A -> B) -> (F[A] -> F[B]) 가 된다.

map 함수 시그니처 추가 설명을 위한 Optional, List 예시

다음과 같은 함수 g 가 있다.
함수 g 는 입력타입이 파란색 원이고 출력타입이 노란색 원이다.
파란색 원에 대한 Optional 을 생각해보자.
Optional 은 실제 내부구현은 다를 수도 있지만
개념적으로는 파란색 원에 대한 Some 이거나 Empty
둘 중에 하나의 값을 갖는 객체라고 보면 된다.
여기서 파란색 원에 대한 Some 을 입력으로 해서
map(g) 를 호출하면 노란색 원에 대한 Some 이 된다.
앞쪽에서 함수 map 의 시그니처는
(A -> B) -> (F[A] -> F[B]) 라고 했고
어떤 함수를 입력으로 해서 함수 map 을 호출한 함수 map(g)
F[A] -> F[B] 시그니처를 갖는 함수를 출력한다고 설명했다.
그러므로 Some<파란색 원> 을 입력으로 map(g) 를 호출하면
출력값으로 Some<노란색 원> 을 얻게 된다.
Empty<파란색 원> 을 입력으로 map(g) 를 호출하면
출력값으로 Empty<노란색 원> 을 얻는다.
이렇게 하면 Some 과 Empty 의 조합으로 이루어진
Optional 에 대해서도 map 함수를 적용할 수 있게 된다.

List 타입에 속한 항목에 대해서도 map 함수를 적용할 수 있다.
List<파란색 원> 을 입력으로 해서 map(g) 를 호출하면
출력으로 List<노란색 원> 을 얻게 된다.

이 부분을 계속 보다보면 map 함수는
내용물을 감싸고 있는 박스의 모양을
항상 보존한다는 것을 알 수 있다.
map 함수는 어떤 데이터의 내용물(content)만 바꾸고
데이터의 구조(structure)는 바꾸지 않는다.

여기까지가 Functor 가 무엇인지 아주 간단하게만 소개한 내용이다.
Functor 개념을 정확하게 이해하려면
복잡한 개념을 많이 학습해야 하지만
여기서는 최대한 프로그래머로서 이해하는데
문제없는 수준에서 풀어내려고 노력해봤다.
Functor 는 map 메서드를 가진 제너릭 인터페이스라고
말할 수 있곘다.

여기까지 쌓은 관련 배경 지식을 바탕으로
함수형 프로그래밍에서 부수효과(side effect)를
어떻게 다루는지 알아보자.

<참조>
유뷰트 영상, Functional Design Patterns - Scott Wlaschin
유튜브 영상, No Nonsense Monad & Functor - The foundation of Functional Programming by César Tron-Lozai

profile
세상에 도움이 되고, 동료에게 도움이 되고, 나에게 도움이 되는 코드를 만들고 싶습니다.

0개의 댓글