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에서는 별다른 연산이나 저장을 할 필요가 없기 때문에 콜스택 영역이 전보다 여유롭게 사용될 수 있다.
3-4. 다양한 매개 및 종료 함수들
- 매개 함수: 매개 함수는 종료 함수가 처리될 때까지 계산되지 않는다.
- Map
- Sort
- Filter
- 종료 함수: 즉시 딱 한번 실행된다.
- GroupBy
- Reduce
- Join
- Map / Sort 는 변환 후 동일한 크기의 컬렉션을 돌려준다
- FIlter / GroupBy 는 더 작은 크기의 컬렉션으로 변환한다
- Reduce는 컬렉션 요소들을 대상으로 계산하여 하나의 값만 돌려준다
- Join 은 두 개의 컬렉션을 하나의 컬렉션으로 병합한다
함수종류 | 함수 | Gleam 지원 | 타입보존 | 수 유지 | 순서 유지 | 설명 |
---|---|---|---|---|---|---|
매개함수 | map | Y | N | Y | Y | 주어진 리스트의 각 원소들을 변환해 같은 크기를 가진 새 리스트의 원소들로 만든다. |
매개함수 | filter | Y | Y | N | Y | 술어 함수를 호출한다. 결과가 참이면 현재의 아이템은 건너뛰고 결과물 리스트에 넣지 않는다. |
매개함수 | sort | Y | Y | Y | Y | 주어진 기준에 따라 집합을 정렬한다. |
함수종류 | 함수 | Gleam 지원 | 아이템 그룹화 | 부수 효과 생성 | 결과 수집 | 설명 |
---|---|---|---|---|---|---|
종료함수 | Collect, Join,GroupBy | Y | Y | - | - | 새 컬렉션 생성 |
종료함수 | ForEach | Y | - | Y | - | 원소들의 개별 처리 시 사용 |
종료함수 | Reduce | Y | - | - | 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는 순수 함수형 프로그래밍을 고안하지 않았으므로 제네릭 지원이 결여되어 있다. ➡ 함수형 프로그래밍 스타일을 택할 경우, 성능 저하를 감수해야 한다.
Author And Source
이 문제에 관하여(2. 컬렉션 다루기), 우리는 이곳에서 더 많은 자료를 발견하고 링크를 클릭하여 보았다 https://velog.io/@developyoun/2.-컬렉션-다루기저자 귀속: 원작자 정보가 원작자 URL에 포함되어 있으며 저작권은 원작자 소유입니다.
우수한 개발자 콘텐츠 발견에 전념 (Collection and Share based on the CC Protocol.)