함수와 메서드는 다르다.

2025. 8. 1. 14:40·내게 필요한 개발 공부

예전부터 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"의 경우, 다른 언어에서 사용 중인 기술을 채택한 것이고, 직관적이기 때문에 문제 없음!
  • 이렇게 새로운 연산자를 도입할 때는 몇 가지 사항을 확인해야 함.
    1. 기존에 사용 중이던 연산자(혹은 이미 의미를 가진 연산자)에 새로운 의미를 부여하지 말 것.
    2. 기존 기술을 최대한 활용하고 연산자가 의미를 잘 전달할 수 있는 멋진 형태(Shape)를 갖도록 해야함.
    3. 특정 분야에만 국한된 문제를 해결하기 위함이 아닌, 매우 일반적인 방식으로 사용 및 재사용할 수 있는 연산자만 도입할 것.

 

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
'내게 필요한 개발 공부' 카테고리의 다른 글
  • Task는 항상 부모 Context를 상속 받을까?
  • 멀티태스킹과 멀티프로세싱, 비슷한듯 다른 두 개념
  • 안전한 놀이터 샌드박스 알아보기
  • iOS에서 OS 뜯어보기
thinkyside
thinkyside
스스로에게 솔직해지고 싶은 공간
  • thinkyside
    또 만드는 한톨
    thinkyside
  • 전체
    오늘
    어제
    • 모아보기 (60)
      • 솔직해보려는 회고 (1)
      • 꾸준히 글쓰기 (9)
      • 생각을 담은 독서 (6)
      • 내게 필요한 개발 공부 (24)
      • 트러블슈팅 (4)
      • 프로젝트 일지 (8)
      • 개발 서적 (3)
      • 취준 (3)
      • 대외활동 (1)
      • UXUI (1)
  • hELLO· Designed By정상우.v4.10.3
thinkyside
함수와 메서드는 다르다.
상단으로

티스토리툴바