예전부터 TCA(The Composable Architecture)를 만든 Point-Free 형님들의 영상을 봐야겠다 생각하고 있었습니다. 하지만 영어의 장벽,,,으로 미루고 미루다가(이것도 언제까지 미룰거야!!) 이제는 Swift 딥다이브가 정말로 필요하다는 생각이 들어 무료로 풀린 첫 번째 에피소드부터 시청하기로 했습니다. 첫 에피소드부터 머리를 띵,, 하고 맞은 것 같은 내용이어서 휘발되기 전에 메모를 남겨두려 합니다.
++ 영상 보고, 해석하고 하는건 시간이 너무 오래걸릴 것 같아 스크립트 번역을 통해 시청했습니다!
영상 출처: https://www.pointfree.co/episodes/ep1-functions
스크립트 흐름
1. Introduction
- Point-Free에서는 다양한 함수형 프로그래밍 개념을 다룰 예정.
- 함수란 무엇인가? → '입력과 출력을 갖는 계산'(Computation with an input & output.)
//입력 //출력
func incr(_ x: Int) -> Int {
return x + 1
}
incr(2) // 3
//입력 //출력
func square(_ x: Int) -> Int {
return x * x
}
square(2) // 4
- 아래와 같이 함수 호출을 중첩할 수도 있음.
square(incr(2)) // 9
- 하지만 Swift에서 흔히 사용하는 방법은 아님.
- 왜냐면 `incr`, `square`과 같은 최상위 자유 함수보다 메서드 사용이 더 일반적이기 때문.
- 자유함수? → 최상위(Top-level)에 작성된 함수로 `print` 등과 같은 것을 상상하면 됨.
- 메서드? → class, struct 등 타입 내부에 작성된 함수.
- 그래서 `Int`를 확장(extension)해 메서드로 정의하는 방법을 사용할 수 있음.
extension Int {
func incr() -> Int {
return self + 1
}
func square() -> Int {
return self * self
}
}
- 메서드로 정의하면 아래와 같이 사용할 수 있음.
- 물론 메서드 호출을 체이닝할 수도 있음.
2.incr() // 3
2.incr().square() // 9
- 메서드를 사용했을 때는 왼쪽에서 오른쪽으로 직관적으로 읽기 쉬움.
- 하지만 자유 함수를 사용했을 때는 안쪽에서 바깥으로 읽기 때문에 상대적으로 어려움.
2. Introducing |>
- 자유 함수를 사용하면서도 가독성을 유지하는 언어들이 많이 있음.
- 자유 함수 적용 시 중위 연산자를 사용해 가독성을 유지할 수 있음.
- Swift에서도 사용자 지정 연산자를 생성할 수 있기 때문에 비슷한 기능을 만들어 볼 것임.
infix operator |>
- `infix operator` → 중위 연산자를 등록하는 Swift 지정 키워드.
- "pipe-forward"라고 불리는 연산자를 정의했음.
- 이는 기존 기술을 기반으로 한 것으로, F#, Elixir, Elm 언어들은 모두 함수 적용에 이 연산자를 사용 중임.
- 이 연산자를 사용하기 위해 함수를 정의할 것임.
func |> <A, B>(a: A, f: (A) -> B) -> B {
return f(a)
}
- A, B 타입을 제네릭으로 설정.
- 인자 a: A 타입의 값.
- 인자 f: A 타입의 값을 받아 B를 반환하는 함수.
- 그럼 아래와 같이 사용이 가능함.
2 |> incr // 3
- 물론 체이닝도 가능해야 할 것임.
2 |> incr |> square
- 하지만 에러가 발생할 것.
- `Adjacent operators are in non-associative precedence group 'DefaultPrecedence'`
- 이는 연산자가 여러 번 사용될 때 어느쪽을 먼저 계산해야 하는지 알 수 없기 때문에 발생.
- 물론 아래와 같이 괄호를 사용해 왼쪽이 먼저 계산되게 할 수도 있음.
(2 |> incr) |> square // 9
- 이 방법은 효과적이지만, 복잡해질 수록 중첩된 괄호들이 많아져 사용이 어려워질 것.
- 이를 해결하기 위해 `precedencegroup`(우선순위그룹)을 사용해 연산자의 우선순위를 정할 수 있음.
precedencegroup ForwardApplication {
associativity: left
}
- 이런식으로 왼쪽 연산자가 먼저 계산되게 만들 수 있고, 이를 우리가 만든 중위 연산자에 적용할 수 있음.
infix operator |>: ForwardApplication
- 이렇게 작성하게 되면 원하는 것처럼 사용할 수 있게 됨.
- 위 방식은 앞서 설명했던 메서드 방식과 매우 유사함.
2 |> incr |> square // 9
2.incr().square() // 9
3. Operator interlude
- 함수가 중첩되어 발생하는 가독성 문제는 해결했지만, 새로운 문제가 발생했음.
- 바로 우리가 "pipe-forward"라 불리는 사용자 지정 연산자를 사용했다는 것임.
- 사용자 지정 연산자는 많은 사람들의 부정적인 인식 때문에 사용을 피하게 되고, 흔하지 않음.
- 하지만 "pipe-forward"의 경우, 다른 언어에서 사용 중인 기술을 채택한 것이고, 직관적이기 때문에 문제 없음!
- 이렇게 새로운 연산자를 도입할 때는 몇 가지 사항을 확인해야 함.
- 기존에 사용 중이던 연산자(혹은 이미 의미를 가진 연산자)에 새로운 의미를 부여하지 말 것.
- 기존 기술을 최대한 활용하고 연산자가 의미를 잘 전달할 수 있는 멋진 형태(Shape)를 갖도록 해야함.
- 특정 분야에만 국한된 문제를 해결하기 위함이 아닌, 매우 일반적인 방식으로 사용 및 재사용할 수 있는 연산자만 도입할 것.
4. Introducing >>>
- 자유 함수는 메서드가 할 수 없는 일을 할 수 있음! → 함수 합성(function composition)
- 함수 합성을 위한 새로운 연산자를 소개함.
infix operator >>>
- 이 연산자는 "forward compose" 또는 "right arrow" 연산자로 알려져 있음.
- 이를 함수로 정의하면 다음과 같음.
func >>> <A, B, C>(
f: @escaping (A) -> B, g: @escaping (B) -> C) -> ((A) -> C
) {
return { a in
g(f(a))
}
}
- A, B, C 타입을 제네릭으로 받음.
- 인자 f: A 타입의 값을 받아 B를 리턴.
- 인자 g: B 타입의 값을 받아 C를 리턴.
- 리턴: A 타입의 값을 받아 C를 리턴하는 함수를 리턴.
incr >>> square
- 이제 `incr` 함수를 가져와 `square` 함수와 forward compose 할 수 있음.
- 위 식 자체가 '하나의 함수' 처럼 동작하게 되는 것임.
- 이 함수는 증가 후 제곱을 구하는 동작을 실행함.
- 물론 이를 뒤집어서(square >>> incr) 제곱 후 증가시키는 새로운 함수를 만들 수도 있음!
- 이렇게 만든 새로운 함수를 기존 방식처럼 호출할 수 있게 됨.
(incr >>> squre)(2) // 9
- 하지만 이는 읽기 좋진 않으니 앞에서 만들었던 `|>` 연산자를 사용하면 도움이 될 것임.
2 |> incr >>> square
- 하지만 안타깝게도 다음과 같은 오류가 발생함.
- `Adjacent operators are in unordered precedence groups ‘ForwardApplication’ and ‘DefaultPrecedence’`
- 지금 두가지 연산자(`|>`, `>>>`)를 섞어서 사용 중인데, Swift는 어떤 연산자를 먼저 사용해야 할지 모르기 때문.
- 이 문제를 해결하기 위해 또다시 `precedencegroup`을 사용해 우선순위를 정의할 수 있음.
/// 새로운 우선순위 그룹 생성
precedencegroup ForwardComposition {
associativity: left
higherThan: ForwardApplication
}
/// 위에서 만들었던 >>> 연산자에 적용
infix operator >>>: ForwardComposition
- 이제 우리가 기대했던 것처럼 사용이 가능함!
- 또한 위에서 알아봤던 연산자 생성 주의사항에도 잘 부합함.
2 |> incr >>> square // 9
5. Method composition
- 메서드로 함수 합성을 하려면 어떻게 해야할까?
- 타입을 다시 확장(extension)해서 각 메서드를 합성하는 새로운 메서드를 작성하는 것 외에는 다른 선택지가 없음.
extension Int {
func incrAndSquare() -> Int {
return self.incr().square()
}
}
- 이 방법은 원하는대로 동작하긴 하지만, 많은 작업을 필요로 하게 됨.
- 5줄의 코드를 추가로 작성해야 했고, 4개의 키워드를 사용했으며, 타입을 지정하기 까지 해줘야 함.
- 함수 합성에 이렇게 많은 보일러플레이트와 노력이 필요하다면 그럴만한 가치가 있을지 고민해봐야 함.
- 그에 비해 자유 함수의 합성은 완벽하게 유지 되는 작은 조각으로 구성됨.
(bite-sized piece that wholly intact without any noise)
incr >>> square
- 또한 자유함수는 개별의 작은 단위로써 컴파일이 가능함.
2 |> incr >>> square
// every composed unit still compiles:
2 |> incr
2 |> square
let newFunction = incr >>> square
- 반면에 메서드는 호출 혹은 체이닝을 위해 값이 꼭 필요함.
// valid:
2.incr().square()
// not:
.incr().square()
incr().square()
- 이 때 문에 메서드는 기본적으로 재사용성이 떨어짐.
- 일반적으로 함수가 아닌 메서드를 사용하는 것처럼 느낄 수 있지만, 사실 Swift에서는 매일 함수를 사용하고 있습니다.
- 그 중에서도 가장 많이 사용하는 것이 초기화(`init`) 함수임.
- Swift의 모든 초기화 함수는 함수 합성에 사용할 수 있음.
incr >>> square >>> String.init
// (Int) -> String
- 즉, 다음과 같이 입력해 문자열 결과를 생성할 수도 있음!
2 |> incr >>> square >>> String.init // "9"
- 반면, 메서드로 작성 시 결과를 초기화 함수에 연결할 수 없기 때문에 감싸서 호출해야 함.
- 이는 읽는 순서를 안쪽에서 바깥쪽으로 읽어야 함.
String(2.incr().square())
- 초기화 함수 이외에도 표준 라이브러리에는 자유 함수를 입력으로 받는 함수가 많이 있음.
- 대표적으로 `Array`의 `map`이라는 메서드가 있음.
[1, 2, 3].map
// (transform: (Int) throws -> T) rethrows -> [T]
- 메서드만 사용하면 재사용성을 확보하기 어렵지만, 함수는 직접 재사용할 수 있음.
[1, 2, 3]
.map(incr)
.map(square)
// [4, 9, 16]
- 위 방식은 임시 함수를 열거나 인수(point)를 지정할 필요가 없었는데, 이를 point-free 스타일이라고 함.
- Point-free 스타일? : 함수를 정의하거나 호출할 때, 입력 인자(arguments)를 명시적으로 언급하지 않는 프로그래밍 스타일
- ex) `users.map(\.name)`
- Point-free 스타일 프로그래밍은 함수와 합성에 중점을 두어 연산 대상 데이터를 참조할 필요조차 없음.
[1, 2, 3].map(incr >>> square) // [4, 9, 16]
6. What’s the point?
- 오늘 작성한 코드가 우리 프로젝트에 '함수'를 도입해야 하는 강력한 이유를 제시했기를 바람.
- 핵심은 함수와 메서드는 다른 방식으로 구성된다는 것.
- 메서드를 사용해 기능을 구성(compose)하려면 훨씬 더 많은 작업과 보일러플레이트가 들어감.
- 그리고 나중에 이를 파악하려면 불필요한 부분을 걸러내야할 것.
- 단 몇 개의 연산자만으로도 이전에 없었던 구성(compose)의 세계를 열 수 있고, 가독성을 상당 부분 유지할 수 있을 것!
- 또한 Swift는 전역 NameSpace를 사용하지 않고도 함수의 범위를 여러가지 방법으로 지정할 수 있음.
- 접근제어자를 활용해 함수를 정의할 수 있음.
- struct, enum의 static 함수로 정의할 수 있음.
- 모듈 범위로 지정된 함수를 정의할 수 있음.
- 함수를 두려워하지 말자!
후기
TCA를 만든 Point-Free 형님들의 팀명이 그냥 멋있는 단어인줄 알았는데,,, 이런 심오한 함수형 프로그래밍의 뜻을 담고 있었다니 놀랐습니다. 첫 번째 영상으로 왜 이 주제를 선택했는지도 자연스럽게 알 수 있었죠. 그 강력함은 너무나 와닿게 인지할 수 있었지만, 이걸 제 프로젝트에 어떻게 적용하면 좋을지 고민이 되기도 합니다.(무엇보다 정말 재밌네요. 😆)
'내게 필요한 개발 공부' 카테고리의 다른 글
Task는 항상 부모 Context를 상속 받을까? (2) | 2025.07.22 |
---|---|
멀티태스킹과 멀티프로세싱, 비슷한듯 다른 두 개념 (0) | 2025.03.17 |
안전한 놀이터 샌드박스 알아보기 (2) | 2025.02.26 |
iOS에서 OS 뜯어보기 (0) | 2025.02.26 |
스마트폰의 CPU, AP 알아보기 (1) | 2025.02.24 |