2. 컬렉션 다루기

"Go로 배우는 함수형 프로그래밍" 교재를 읽고 정리한 내용입니다.

  • 컬렉션 순회하기
  • 매개 및 종결 함자 학습
  • 컬렉션의 아이템을 필터링하기 위한 술어 사용
  • Mocha-like BDD 라이브러리를 사용해 테스트하기
  • 맵 함수 집중 탐구
  • Itertools의 컬렉션 처리 함수 맛보기
  • 컬렉션 순회를 위한 루틴과 채널 활용
  • 빅데이터 컬렉션 처리를 위한 Go 사용법 살펴보기

1. 컬렉션 순회

// Next 인터페이스
type CarIterator interface {
    // 값인 value와, 반복가능여부인 ok
    Next() (value string, ok bool)
}
const INVALID_INT_VAL = -1
const INVALID_ID_STRING_VAL = ""
// 컬렉션 객체
type Collection struct {
    index int	// 컬렉션 객체 접근을 위한 정수 인덱스
    List []string	// 컬렉션에 저장된 실제 데이터인 문자열 슬라이스
}
// Next 구현
func (colletion *Collection) Next() (value string, ok bool) {
    collection.index++
    if collection.index >= len(collection.List) {
        return INVALID_STRING_VAL, false
    }
    return collection.List[collection.index], true
}
// newSlice 생성자
func newSlice( s []string) *Collection {
    return &Collection{INVALID_INT_VAL, s}
}
  • 이 구현의 문제점은 원하는 바와 구현 방법이 뒤섞여 있다는 점이다.
    • 반복 동작 수행을 위해 명시적으로 for 반복을 수행
    • 원소들을 순회하기 위해 인덱스의 값을 정의하고 변경
    • 명령행 방식
  • 함수형 프로그래밍은
    • 구현 방법을 단계별로 하나하나 상세히 지시하기 보다 무엇을 할지 선언하는 방법을 사용 (선언형 프로그래밍 := 함수형 프로그래밍)
    • 이렇게 함으로서 for 반복의 순차성도 피할 수 있다.
  • 순수한 함수형 언어는
    • 상태를 관리하지 않는다.
    • 함수 호출은 종종 연쇄적
    • 입력이 주어지면 함수 사이에서 전달하며 반환하므로 외부상태와 독립적 (부수 효과 유발❌)
    • 효율적인 테스팅 가능

2. 배시 명령어 파이핑

  • 함수 합성이나 체인은 일련의 배시 명령 실행과 닮았다.
  • 하나의 명령 실행 결과(함수형 프로그래밍)가 파이프를 통해 다음 명령으로 전달되는 형태(파이핑)와 비슷.

파이핑 ❓

: 복수의 명령어들을 서로 직접 결합시키는 것. 파이핑은 여러번 중첩도 가능하다.

$ cat ips.log | awk '{print $7}' | sort | uniq -c
  • 이러한 패턴은 함수형 프로그래밍에서 자주 등장. ➡ 데이터의 컬렉션을 함수 또는 함수 호출 체인의 입력으로 주고 변환된 결과를 얻는 방식에서 많이 사용함
  • 컬렉션을 다루려면 원하는 바를 명시적으로 선언하는 함수 호출 체인 을 만들면 된다. ➡ 이를 통해 높은 표현력, 간결하며 읽기 쉬운 코드 습득

3. 함자

  • Go에는 세 개의 기본 데이터 타입인 bool, string, 수 타입(float, int64) 가 있다.
  • 그 밖의 테이터 타입들은 type 키워드를 선언해야 한다. (함수는 배열, 구조체, 포인터, 인터페이스, 슬라이스, 맵, 채널 타입과 마찬가지로 type 키워드로 선언)
  • 함수를 전달 인자로 취하고 함수를 반환하는 함수를 고계 함수 라고 한다.

함자(funtor)❓

: 함자는 변수 X의 컬렉션에 함수 f를 적용해 Y컬렉션을 생성하는 변환이다. 즉, f(x) -> f(Y) 가 성립한다.

3-1. 암묵적 프로그래밍

  • 암묵적 프로그래밍은 다른 함수와 함수 컴피네이터를 조합해 함수를 정의하고 전달 인자를 가공하는 프로그래밍 스타일이다.
  • 컴비네이터는 고계함수로서 함수와 이미 정의된 컴비네이터만을 사용해 전달 인자로부터 결과를 도출한다.

암묵적 프로그래밍 추가 설명

: 인수를 선언할 필요없이 간결하면서 선언적인 무인수(point-free) 코드 작성 기법. 무인수 프로그래밍이라고도 한다.

그러나 합성을 과용하면 할수록 헷갈리는 프로그램이 될 수 있으며, 에러 처리 및 디버깅에도 문제가 될 수 있다. 명령형 코드는 if나 else, for 같은 절차적 제어 장치로 프로그램의 흐름을 제어할 수 있지만, 함수 조합기(function combinator)를 사용하여 제어 로직처럼 작동시킬 수 있는 고계함수가 필요하다.

# 유닉스 파이프 예제
$ cat access10k.log | head -n 1 | awk '{print $7}' | grep "\.json" | uniq -c | sort -nr
  • 위의 예제에서도 보면, 각 함수 컴비네이터는 출력을 표준 출력으로 보내고, 표준 입력으로부터 입력을 취하는 함수들이다.
  • 명령내에서 전달 인자들이 등장하지 않는 점을 유의깊게 보자 ➡ 암묵적 프로그래밍

3-2. 암묵적 프로그래밍과 함수형 프로그래밍

  • 정수 리스트 합을 구하는 프로그래밍을 예시로 들자
    • 명령행 프로그래밍 - 반복문의 인덱스를 사용해 누적합을 저장
    • 함수형 프로그래밍 - 재귀 호출을 이용해 반복문을 구현하며, 누적합은 다음 재귀호출의 매개변수로 전달

3-3. 재귀 호출의 중요성

  • 꼬리 재귀 함수는 반복문을 함수형 프로그래밍 스타일로 구성한 것이며, TCO를 통해 반복문에 가까운 효율로 실행할 수 있다.
  • 재귀 호출이 없었다면 대부분의 반복문은 명령행 프로그래밍 방식으로 구현해야 했을 것이므로 Go의 TCO지원은 중요하다.

TC(Tail Call) ❓ TCO (Tail Call Optimization) ❓

: Tail Call 이란 직역하면 '꼬리 재귀' 의 뜻인데, 재귀호출이 끝나면 아무일도 하지 않고 결과만 바로 반환되도록 하는 방법이다. 즉 호출 당한 함수의 결과 => 호출한 함수의 결과 가 되는 것이다.

:Tail Call Optimization은 이러한 꼬리 재귀의 특징을 이용하여 최적하는 방법이다. 일반적인 재귀에서 발생하는 콜스택 오버플로우를 방지하여 최적화 할 수 있다.

// 자바스크립트 예제

// 일반 재귀
const factorial = (n) => {
    if (n <= 1) {
        return 1;
    }
    return n * factorial(n-1);
}
// 꼬리 재귀
const factorial = (n, total) => {
    if (n <= 1) {
        return 1;
    }
    return factorial(n-1, n*total);
}
// 자바 예제

// 일반 재귀
public class Factorial {
	...생략
	public static int factorial(final int n) {
		if (number == 1) {
			return number;
		}
         return n * factorial(n-1); 
	}
}
// 꼬리 재귀
public class Factorial {
	...생략
	public static int factorial(final int n, final int total) {
		if (number == 1) {
			return number;
		}
         return factorial(n-1, n*total); 
	}
}
  • 이처럼 변화점들은 return 문에서 밖에 없지만, return n * factorial(n-1) 은 재귀가 완료될 때까지 연산의 부분 결과를 계속 스택에 보관하고 있어야 한다. ➡ 계속 호출할 때마다, 콜스택에 저장하게 되고 숫자가 커지면 스택이 폭발한다. (스택에 저장하지 않고 재귀를 사용할 수 있는 방법을 찾아야 한다.)
  • 그러나 return factorial(n-1, n*total) 은 계산된 depth에서는 별다른 연산이나 저장을 할 필요가 없기 때문에 콜스택 영역이 전보다 여유롭게 사용될 수 있다.

참고: https://joooing.tistory.com/entry/%EC%9E%AC%EA%B7%80-%E2%86%92-%EA%BC%AC%EB%A6%AC-%EC%9E%AC%EA%B7%80-Tail-Recursion#recentComments


3-4. 다양한 매개 및 종료 함수들

  • 매개 함수: 매개 함수는 종료 함수가 처리될 때까지 계산되지 않는다.
    • Map
    • Sort
    • Filter
  • 종료 함수: 즉시 딱 한번 실행된다.
    • GroupBy
    • Reduce
    • Join
  • Map / Sort 는 변환 후 동일한 크기의 컬렉션을 돌려준다
  • FIlter / GroupBy 는 더 작은 크기의 컬렉션으로 변환한다
  • Reduce는 컬렉션 요소들을 대상으로 계산하여 하나의 값만 돌려준다
  • Join 은 두 개의 컬렉션을 하나의 컬렉션으로 병합한다
함수종류함수Gleam 지원타입보존수 유지순서 유지설명
매개함수mapYNYY주어진 리스트의 각 원소들을 변환해 같은 크기를 가진 새 리스트의 원소들로 만든다.
매개함수filterYYNY술어 함수를 호출한다. 결과가 참이면 현재의 아이템은 건너뛰고 결과물 리스트에 넣지 않는다.
매개함수sortYYYY주어진 기준에 따라 집합을 정렬한다.
함수종류함수Gleam 지원아이템 그룹화부수 효과 생성결과 수집설명
종료함수Collect, Join,GroupByYY--새 컬렉션 생성
종료함수ForEachY-Y-원소들의 개별 처리 시 사용
종료함수ReduceY--Y필요에 따라 느긋한 계산을 시작하고 결과들을 도출한다.
// Map 예제
names := []interface{}{
    "Alice",
    "Bob",
    "Cindy"
}
collection := collections.NewFromSlice(planets)
collection = collection.Map(func(v interface{}) interface{} {
    return strings.Join([]string{ "Hey ", v.(string)})
})
println(collection)
---
// 결과
Hey Alice
Hey Bob
Hey Cindy
// join 예제

// 컬렉션1
0001, "alice", "bob"
0001, "cindy", "dan"
0002, "evelyn", "frank"
// 컬렉션2
0001, "greg", "izzy"
0002, "jenny", "alice"
---
// 결과
0001, "alice", "bob", "greg", "izzy"
0001, "cindy", "dan", "greg", "izzy"
0002, "evelyn", "frank", "jenny", "alice"
// GroupBy 예제
0001, "alice", 0002
0001, "bob", 0002
0003, "cindy", 0002
GroupyBy(1, 3)
---
// 결과
0001, 0002, ["alice", "bob"]
0003, 0002, ["cindy"]
// Reduce 예제
numbers := []interface{}{
    1,
    5,
    3,
    2
}
collection := collections.NewFromSlice(numbers)
min := collection.Reduce(0, func(a, b interface{}) interface{} {
    if a > b {return a} else {return b}
})

4. 술어

  • 입력 데이터를 대상으로 연산을 수행하기 위해 술어를 사용가능
  • 컬렉션에 적용해 입력 데이터를 컬렉션 또는 값의 형태로 변환하는 다양한 함수를 구현하는 데 술어 사용가능

술어(predicate) 함수 ❓

: 하나의 아이템을 입력으로 취해, 주어진 조건을 충족하는지 따져보고 참 또는 거짓을 반환하는 함수. 실행 체인 내에서 특정 연산을 적용할지 판단하고자 할 때 사용한다.

  • 컴비네이터 패턴
    • Go에서는 함수를 값처럼 주고받을 수 있으므로, 술어 컴비네이터를 생성해 간단한 술어들로부터 좀 더 복잡한 술어를 만들어 낼 수 있다.
    • 합성과 컴비네이터 패턴은 후에 다시 살펴볼 예정

컴비네이터 패턴 ❓

: 기본 함수들을 좀 더 복잡한 함수로 연쇄 호출해 시스템을 생성하는 행위


5. 맵과 필터

  • 예시를 보고 참고

6. Contains 연산

  • 찾고자 하는 아이템이 컬렉션 내에 있는지 알려주는 메소드
  • Go는 제네릭 리스트를 대상으로 동작하는 contains 메소드가 없기에 직접 구현한다.

7. Itertools

  • Intertools 는 파이썬 표준 라이브러리의 고계 함수들과 동일한 여러 함수들을 제공하는 Go 패키지

  • 다양한 타입의 고계함수를 제공한다

  • 선언적 코딩 스타일을 위한 기본 어휘를 제공한다.

  • 무한 반복자 생성기

    • Count(i): i 부터 계속 셈한다.
    • Cycle(iter): iter를 계속해서 반복 순회한다. (메모리 공간 필요)
    • Repeat(element [, n]): element를 n번 반복한다. (또는 무한반복한다.)
  • 반복자 파괴자

    • Reduce(iter, reducer, memo): 반복자 iterator를 통해 합친다.
    • List(iter): iterator로부터 리스트를 생성한다.
  • 반복자 변경자

    • Chain(iters...): 여러 개의 반복자를 연쇄 호출한다.
    • DropWhile(predicate, iter): predicate(el) == false 를 만족할 때까지 원소들을 삭제한다.
    • TakeWhile(predicate, iter): predicate(el) == false 를 만족할 때까지 원소들을 취한다.
    • Filter(predicate, iter): predicate(el) == false 를 만족하는 원소들을 걸러낸다.
    • FilterFalse(predicate, iter): predicate(el) == true 를 만족하는 원소들을 걸러낸다.
    • Slice(iter, start[, stop[, step]]): start 인덱스를 만날 떄까지 원소를 버린다. stop에서 멈추며 step을 따로 지정하지 않으면 기본 1값을 가진다.
  • 그 외에도 많은 반복자 변경자가 있다.


8. 마지막으로

  • 컬렉션을 다루는 선언적 코드를 작성할 때, 유용한 고계 함수를 제공하는 다른 Go 패키지도 있다. ➡ 보통 이들은 빈 인터페이스와 리플렉션을 활용하므로 성을 저하시킨다.
  • 현재 Go는 전통적인 명령형 프로그래밍 기법의 성능은 순수 프로그래밍 보다 좋은 성능은 아니다. ➡ 앞으로의 목표는 Go의 선언적 함수형 프로그래밍 스타일을 사용하되 기대만큼의 성능을 내는 방법을 찾는 것.
  • Go는 많은 특징을 가진다. ➡ 성능 , 개발 과정이 쉽고 빠름 , 다중 플랫폼 지원 , 소스 코드 보호 , 병행 처리
  • 그러나, Go는 순수 함수형 프로그래밍을 고안하지 않았으므로 제네릭 지원이 결여되어 있다. ➡ 함수형 프로그래밍 스타일을 택할 경우, 성능 저하를 감수해야 한다.

좋은 웹페이지 즐겨찾기