Daheen Lee all white cheat sheet

OOP + FP - 객제지향과 함수형 적절히 사용하기

절차지향 -> 객체지향

절차지향은 순차적인 처리로 프로그램을 구성하는 방식이다. 컴퓨터의 처리구조와 유사해서 실행속도가 빠르다. 일을 처리하는 순서대로 프로그램이 진행된다.

절차지향에서 “데이터와 처리함수를 함께 묶을 수 없을까?”, “어떻게 하면 프로그램을 구조적으로 나눌 수 있을까?” 라는 질문에서 출발한게 객체지향이다. 절차지향에서는 변수는 서브루틴(함수)와 연결되어있지 않다. 따라서 관련된 함수여도 변수는 다른 곳에서 선언된다. 절차지향은 처리속도가 빠르지만, 모듈을 재활용하기는 힘들다.

반면에 객체지향에서는 데이터와 관련된 함수를 하나로 묶어 객체가 된다. 이 객체는 상태값을 가지고 그 상태값에 따라 다르게 행동한다. 클래스 단위로 데이터와 처리 메소드가 같이 묶여있으므로 모듈을 재활용하기에도 용이하다.

 

객체지향 -> 함수 중심

객체 지향에서는 상태값이 존재한다. 이 상태값에 따라 다른 행동을 취한다. 따라서 런타임에 상태값의 변화에 따른 동작을 예측할 수 없다. 이를 side effect라고 한다.

이런 불편함을 해결하기 위해 immutable 불변의 속성을 가진 순수함수를 연결해서 프로그램을 구성하는 방식이 함수형, 함수 중심 프로그래밍이다. 객체 지향에서는 상태에 따라 동작이 달라지므로 복잡도가 증가한다. 이 복잡도를 줄이기 위해 함수 중심 프로그래밍이 주목받고 있다.

 

OOP + FP

Swift 를 포함한 최근 등장한 언어들은 대부분 객체지향과 함수형의 특성을 모두 갖고있다. Kotlin Rust, Ruby, Scala 등 여러 언어가 객체지향과 함수형 컨셉을 모두 구현하고 있다.

OOP 혹은 FP 어느 한쪽이 더 좋은 방법이라는 절대적이고 명확한 정답은 없다. 그저 서로의 단점을 보완하기 위해, 그리고 경우에 따라 더 적절하게 판단할 수 있다는 것 뿐이다.

oop , fp 전혀 다른게 아니며, 완전 정반대의 개념이 아니다. 문제를 해결하는 방식의 차이이며, 적절한 것을 사용하는게 중요하다.

 

고려 해볼 것들

  • 객체 생성 - 불변(immutable) 객체 만들기
  • 상태의 변화 없이 복사본을 생성하기
  • 객체의 메소드를 순수함수처럼 상태의 변화가 없도록 만들기
  • 참조 투명성 - reference 가 없어야 한다.
  • 함수를 리턴타입, 매개변수로 쓰이는데 익숙해져야한다.
  • 타입 기반 개발 - 타입이 필요한건지, 타입에 관련된 함수가 필요한건지 생각해본다
    • 함수가 필요하면 struct, class 를 확장
    • 타입만 필요하면 typealias를 사용
  • 재귀호출 : depth 가 어디까지 인지 명확하지 않은 경우에 주로 사용한다
    • exit 이 확실하면 재귀호출 사용이 적절할 수 있다
    • 하지만 메모리 사용을 고려해야 된다. 모바일은 특히 메모리 고려해야 됨
  • 구조체 vs. 클래스
  • 추상화 <——> 구체화
    • 프로토콜 ↔️ 클래스 ↔️ 객체
  • Lazy evaluation : Swift 의 lazy keyword
    • 미리 계산하지 않고 나중에 해야 이점이 있는 경우
    • 특히 배열관련

Example : fizzbuzz

1~100 에 대해 다음을 출력한다.

  • fizz : 3로 나누어 떨어지는 경우
  • buzz : 5로 나누어 떨어지는 경우
  • fizzbuzz : 3, 5 로 모두 나누어 떨어지는 경우
  • 모두 해당 안되는 경우 그냥 그 수 출력

FP 로 변하는 과정

1️⃣

func fizzbuzz() {
  for index in 1...100 {
    let isFizz = index % 3 == 0
    let isBuzz = index % 5 == 0
    switch (isFizz, isBuzz) {
      case (true, true):
      	print("fizzbuzz")
      case (true, _):
      	print("fizz")
      case (_, true):
      	print("buzz")
      default:
      	print("\(index)")
    }
  }
}

2️⃣ foreach() 와 closure 사용 (avoid side effect)

func fizzbuzzEach() {
  (1...100).forEach { index in 
    let isFizz = index % 3 == 0
    let isBuzz = index % 5 == 0
    switch (isFizz, isBuzz) {
      case (true, true):
      	print("fizzbuzz")
      case (true, _):
      	print("fizz")
      case (_, true):
      	print("buzz")
      default:
      	print("\(index)")
    }
  }
}

3️⃣ fizz와 buzz 에 해당할 경우 출력 문자열을 반환하는 함수를 변수에 넣어 사용하기

func fizzbuzzClosure() {
  let fizz: (Int) -> String = { index in index % 3 == 0 ? "fizz" : ""}
  let buzz: (Int) -> String = { index in index % 5 == 0 ? "buzz" : ""}
  
  (1...100).forEach { index in
    let result = fizz(index) + buzz(index)
    let output = result.isEmpty? "\(index)" : result
    print(output)
  }
}

4️⃣ 출력도 함수로 선언해서 사용하자 (Declarative)

func fizzbuzzDeclarative() {
  let fizz: (Int) -> String = { index in index % 3 == 0 ? "fizz" : ""}
  let buzz: (Int) -> String = { index in index % 5 == 0 ? "buzz" : ""}
  let fizzbuzz: (Int) -> String = { index in
    { result in result.isEmpty? "\(index)" : result } (fizz(index) + buzz(index))
  }
  // index 에 Int 타입 값을 넣으면 {result in ~} 함수에 (fizz(index) + buzz(index)) 값을 넣어 실행한 결과를 반환한다
  let output: (String) -> () = { print($0) }
  
  (1...100).map(fizzbuzz).forEach(output)
}
index -> ((result -> result.isEmpty? "\(index)" : result)(fizz(index) + buzz(index)))

5️⃣ Type Inference 타입 추론 사용 ($0 사용하기)

func fizzbuzzTypeInference() {
  let fizz = { index % 3 == 0 ? "fizz" : ""}
  let buzz = { index % 5 == 0 ? "buzz" : ""}
  let fizzbuzz = { index in
    { $0.isEmpty? "\(index)" : $0 } (fizz(index) + buzz(index))
  }
  let output: (String) -> () = { print($0) }
  
  (1...100).map(fizzbuzz).forEach(output)
}

6️⃣ Monad : context 안의 content를 가공해서 같은 type의 context에 넣어 반환해줌

func + (_ s1: String?, _ s2: String?) -> (String?) {
  if s1 == nil, s2 == nil { return nil }
  if s1 != nil, s2 == nil { return s1 }
  if s1 == nil, s2 != nil { return s2 }
  return s1! + s2!
}

func fizzbuzzMonad() {
  let fizz = { index % 3 == 0 ? "fizz" : nil}
  let buzz = { index % 5 == 0 ? "buzz" : nil}
  let fizzbuzz = { index in fizz(index) + buzz(index) ?? String(index)}
  let output = { print($0) }
  
  (1...100).map(fizzbuzz).forEach(100)
}

7️⃣ Iterate

func fizzbuzzIterate() {
	func iterate<A>(_ arr: [A], _ f: ((A) -> ())) {
    arr.forEach( { f($0) } )
  }	
  
  let fizz = { index % 3 == 0 ? "fizz" : nil}
  let buzz = { index % 5 == 0 ? "buzz" : nil}
  let fizzbuzz = { index in fizz(index) + buzz(index) ?? String(index)}
  let output = { print($0) }
  
  iterate(Array(1...100)) { index in output(fizzbuzz(index)) }
}

8️⃣ 연산 순서대로 할 수 있게 하기 (➡️ 이방향으로) : //custom operator 정의하기

// 우선순위 그룹 정의
precedencegroup ForwardPipe {
  associativity: left
  higherThan: LogicalConjunctionPrecedence
}

// 중위 연산자 정의
infix operator |> : ForwardPipe

// rhs(lhs(result)) 를 lhs |> rhs 순서로 명시할 수 있게 해줌
public func |> <A, B, C>(lhs: @escaping (A) -> B, rhs: @escaping (B) -> C) -> (A) -> B {
  return { result in rhs(lhs(result)) }
}
func fizzbuzzPipe() {
  func iterate<A>(_ arr: [A], _ f: ((A) -> ())) {
    arr.forEach( { f($0) } )
  }	
  
  let fizz = { index % 3 == 0 ? "fizz" : nil}
  let buzz = { index % 5 == 0 ? "buzz" : nil}
  let fizzbuzz = { index in fizz(index) + buzz(index) ?? String(index)}
  let output = { print($0) }
  
  iterate(Array(1...100), fizzbuzz |> output )
  // 앞에서는 output(fizzbuzz(index)) 였음
}

:pushpin: Reference

Let us go 2018 - Functiona Programming by 송치원님