[함수형 프로그래밍] 순수함수 & 익명함수 & 고차함수

2020. 4. 3. 21:00iOS

Functional Programming?

  • 자료 처리를수학적 함수의 계산으로 취급
  • 상태와 가변 데이터를 멀리함
  • 변수와 반복문이 없음
  • Side-effect가 없음 → 동작을 이해하고 예측하기 쉬워짐
  • Side-effect → 함수형 프로그래밍에서는 잘못된 code로 인한 오동작의 의미가 아닌 실행 결과 상태의 변화를 일으키는 모든 것을 지칭함

→ 모듈화 수준이 높으면 재사용성이 높고 좋은 프로그래밍이라 할 수 있다.

→ 평가 시점이 무관하다는 특성으로 효율적인 로직을 구성하는 것이 함수형 프로그램의 궁극적인 패러다임

순수함수

  • 동일한 인자가 주어졌을 때항상같은 값을 리턴하는 함수
  • → side effect 가 없음
  • Thread에 안전하고 병렬적인 계산이 가능
  • 코드의 블록을 이해하기 위해 일련 상태 갱신을 따라갈 필요가 없고 국소 추론만으로도 코드 이해 가능
  • 모듈적인 프로그램은 독립적으로 재사용할 수 있는 구성요소(component)로 구성
// a와 b를 더하는 것 외의 어떤 side-effect도 발생시키지 않음
func addValue(_ a: Int, b: Int) -> Int {
        return a + b
}

print(addValue(3, b: 5)) // 8
  • 입력과 결과가 분리되어 있으며, 어떻게 사용되는 지에 대해 전혀 신경 쓰지않아도 되므로 재사용성이 높아진다.

순수 함수가 아닌 경우

  • 변수가 포함되어 있을 경우
var c = 10
func addValue2(a: Int, b: Int, c: Int) -> Int {
        return a + b + c
}

print(addValue2(a: 3, b: 5, c: c)) // 18
c = 20
print(addValue2(a: 3, b: 5, c: c)) // 28

함수 내에서 외부의 c라는'변수'값이 변하면 결과 값도 달라지기 때문

c가 변하지 않는 **'상수'**라면 addValue2는 순수 함수이다.

외부의 값을 참조해도 리턴 값이 동일하다는 것을 보장됨

  • 외부 상태에 영향을 미칠 경우(1)
var c = 20
func addValue3(a: Int, b: Int) -> Int {
        c = b  // 외부 상태에 영향을 미침 -> 부수효과(side-effect)
        return a + b
}

print(c) // 20
print(addValue3(a: 3, b: 5)) // 8
print(c) // 5

함수가외부의 값을 변경하는 코드를 가지고 있기 때문 리턴하는 값이 항상 일정하더라도 외부의 상태를 변경하는 코드가 있으면 순수 함수가 아님.

  • 외부 상태에 영향을 미칠 경우(2)
class ObjectEx {
    var num = 10
}

func addVlaue4(obj: ObjectEx, b: Int) -> Int {
        obj.num += b
        return obj.num
}

let objEx = ObjectEx()
print(objEx.num) // 10
print(addValue4(obj: objEx, b: 5)) // 15
print(objEx.num) // 15

객체를 인자로 받아서 그 상태를 변경시키는 코드를 가지고 있음

객체를 순수 함수로 나타내기

class ObjectEx {
    var num = 10
}

func addValue5(obj: ObjectEx, b: Int) -> Int {
        return obj.num + b
}

let objEx = ObjectEx()
print(objEx.num) // 10
print(PureFunction.addValue5(obj: objEx, b: 5)) // 15
print(objEx.num) // 10

ObjectEx의 num의 값만 참조 하는 식으로 작성한다.

순수 함수→ 외부의 상태를 변경하지 않으면서 동일한 인자에 대해 항상 똑같은 값을 리턴하는 함수

익명함수

  • 일반 함수의 경우 func + 함수 이름을 선언하고 사용
  • swift에서는클로저에 해당
  • in키워드를 통해 전달 인자와 실행 코드를 구분
  • 함수의 이름을 선언하지 않고 바로 몸체만 만들어 일회용 함수로 사용
func test(_ isFinish: Bool) -> Void {
        print("isFinish: \(isFinish)")
}

위의 함수를 익명 함수로 바꾸면

let test = { (isFinish: Bool) -> Void in
        print("isFinish: \(isFinish)")
}

{
    (매개변수) -> (반환타입) in
    실행구문
}

가 된다.

  • 컴파일러가 반환 타입을 미리 알고 있다면 반환 타입도생락이 가능하다
let test = { (isFinish: Bool) in
        print("isFinish: \(isFinish)")
}
{ (매개변수) in
    실행구문
}

경량 문법이 있지만 그것은 클로저에서 공부하시면 될듯

  • 클로저 내부 코드가 한 줄이라면return생략 가능

@escaping을 붙이지 않아도 탈출하는 방법

고차함수

  • 함수를 매개변수로 받는 함수
  • 함수를 반환 값으로 사용하는 함수
  • 대표적인 고차 함수로는map,filter,reduce가 있다.

map

  • 데이터를 변형하고자 할 때 사용
  • 기존 컨테이너의 값은 변경되지 않고 새로운 컨테이너를 생성해 반환
  • for-in구문과 별 차이가 없지만 더 좋은 점
    • 코드 재사용이 용이
    • 컴파일러 최적화 측면에서 성능이 좋음
      • 빈 배열 초기 생성할 필요 x
      • append연산 수행할 필요 x
    • 다중 스레드 환경에서 하나의 컨테이너에 여러 스레드들이 동시에 변경하려고 할 때 예측 못한 결과 발생을 방지할 수 있다
let numbers = [0, 1, 2, 3, 4]

// 이것은 함수형이 아닙니다ㅜ
func multiply2(_ numbers: [Int]) -> [Int] {
    var multiplyNumbers = [Int]()

    for number in numbers {
        multiplyNumbers.append(number * 2)
    }
    return multiplyNumbers
}

print(multiply2(numbers)) // [0, 2, 4, 6, 8]

// map  활용한다면?
print(numbers.map({ (num) -> Int in
    num * 2
}))  // [0, 2, 4, 6, 8]

print(numbers.map { $0 * 2 }) // [0, 2, 4, 6, 8]

매개 변수로 전달 할 함수를 클로저 상수로 두어 코드를 재사용할 수 있다.

let multiple: (Int) -> Int = { $0 * 2 }
print(numbers.map(multiple) // [0, 2, 4, 6, 8]

filter

  • 컨테이너 내부의 값을 걸러서 추출하고자 할 때 사용
  • filter를 매개 변수로 전달되는 함수의 반환 타입은Bool
let numbers = [0, 1, 2, 3, 4]

func filterEvens(_ numbers: [Int]) -> [Int] {
    var evenNumbers = [Int]()

    for number in numbers {
        if number % 2 == 0 { evenNumbers.append(number) }
    }

    return evenNumbers
}

print(filterEvens(numbers)) // [0, 2, 4]

print(numbers.filter({ (num) -> Bool in
    num % 2 == 0
}))  // [0, 2, 4]
print(numbers.filter({ $0 % 2 == 0 })) // [0, 2, 4]

// 물론 위의 map에서 했듯 클로저 상수로 둘 수 있다.
let filterEven2: (Int) -> Bool = { $0 % 2 == 0}
print(numbers.filter(filterEven2))

map과filter를 연결하여 사용할 수도 있다.

let doubleEven = numbers
    .map { $0 + 2 }
    .filter { $0 % 2 == 0 }
print(doubleEven)  // [2, 4, 6]

reduce

  • 컨테이너 내부를 하나로 합쳐주는 기능
  • 정수 배열이면 전달받은 함수의 연산 결과로 합쳐주고, 문자열 배열이라면 문자열을 하나로 합쳐준다
  • 첫 번째 매개변수를 통해 초깃값을 지정할 수 있다.
    • 이 초깃값이 최초의$0으로 사용된다
let numbers = [0, 1, 2, 3, 4]

func reduceNumbers(_ numbers: [Int]) -> Int {
    var reduceNumbers = 10 // 초깃값

    for number in numbers {
        reduceNumbers += number
    }
    return reduceNumbers
}

print(reduceNumbers(numbers)) // 20

// 여기서도 10이 초깃값
print(numbers.reduce(10, { (result: Int, currentItem: Int) -> Int in
    return result + currentItem
})) // 20

print(numbers.reduce(10) { $0 + $1 }) // 20

let texts = ["a", "b", "c", "d"]
print(texts.reduce("") { $0 + $1 }) // abcd
  • reduce에서 클로저의 매개변수 이름을 first, second보다는 result, currentItem이라고 하는 것이 좋다.
    • result 는 초깃값으로부터 출발하여 마지막 요소까지 순회하는 결과값
    • currentItem은 현재 순회하고 있는 요소의 값
    • 만약 currentItem들이 없으면 초기값을 반환

reduce(into:_:)

let letters = "abracadabra"
let letterCount = letters.reduce(into: [:]) { counts, letter in
    counts[letter, default: 0] += 1
} // ["d": 1, "c": 1, "a": 5, "r": 2, "b": 2]
  • 단어 빈도 같은 것에 사용 가능하다
  • 결과는 copy-on-write(inout)으로 배열이나 딕셔너리와 같다

forEach

  • 순차적으로 element에 접근할 수 있음
  • collection에서 제공하는 기능이며 클로저 방식으로 사용됨
  • for-in과 다르게 중간에 break로 탈출하지 못함
  • return을 하여 종료하지만 element를 인자로 가진 클로저가 실행이 된다.
enum Calculator {
    case nomal
    case multiple
    case plus
    var calc: (Int) -> Void {
        switch self {
        case .nomal: return {print($0)}
        case .multiple: return {print($0 * $0)}
        case .plus: return {print($0 + $0)}
        }
    }
}

let calc: Calculator = Calculator.nomal
let numbers: [Int] = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
numbers.forEach(calc.calc) //enum 내부의 함수를 실행

flatMap

  • flatten + map의 합성
  • map에서 Optional로 둘러싸진 결과에서 Optional을 풀어낸 값이 나올 수 있게 한 것
  • swift4 이후 compactMap으로 변경
// 2차 배열을 1차 배열로 합성
let arr = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
let flatArr = arr.flatMap { $0 }
print(flatArr) // [1, 2, 3, 4, 5, 6, 7, 8, 9]

// Optional 제거
let a = [1, 2, 3, 4, 5]
let c: (Int) -> Int? = { n in
    if n % 2 == 0 {
        return n * 2
    }
    return nil
}

print(a.map(c)) // [nil, Optional(4), nil, Optional(8), nil]
let b = a.compactMap(c)
print(b)  // [4, 8]

참고

순수 함수란? (함수형 프로그래밍의 뿌리, 함수의 부수효과를 없앤다)

[Swift] 익명 함수(Anonymous Functions)

Swift 고차 함수 - Map, Filter, Reduce

고차함수

'iOS' 카테고리의 다른 글