Chap09 람다 라이브러리
9-1) 람다 라이브러리 소개
- ramda 패키지는 compose나 pipe를 사용하는 함수 조합을 쉽게 할 수 있게 설계된 오픈소스 자바스크립트 라이브러리이다.
특징
- 타입스크립트 언어와 100% 호환
- compose와 pipe 함수 제공
- 자동 커리(auto curry) 기능 제공
- 포인트가 없는 고차 도움 함수 제공
- 조합 논리(combinatory logic) 함수 일부 제공
- 하스켈 렌즈(lens) 라이브러리 기능 일부 제공
- 자바스크립트 표준 모나드 규격(fantasyland-spec)과 호환
(1) ramda 패키지 구성
- ramda 패키지는 많은 도움 함수(utility function)를 제공하며, 이 도움 함수들의 문서는 아래 주소에서 찾을 수 있다.
https://ramdajs.com/docs/: 함수를 알파벳 순서로 분류
https://devdocs.io/ramda/: 함수를 기능 위주로 분류
- ramda 패키지를 사용하기 위해선 기존의 프로젝트 세팅에서 다음과 같은 과정을 추가한다.
또한 가짜 데이터를 만들어주는 chance 패키지 또한 설치한다.
※ tsconfig 속성중 noImplicitAny 속성값이 false인 이유
- 람다 라이브러리는 자바스크립트를 대상으로 설계되었으므로, 타입스크립트는 any 타입을 완전히 자바스크립트적으로 해석해야 하므로 false로 설정함
(2) ramda 패키지를 불러오기
- 타입스크립트 소스코드에서 ramda 패키지를 불러와서 R이라는 심벌로 일반적으로 사용함
Import * as R from ‘ramda’
9-2) 람다 기본 사용법
(1) R.range 함수
- R.range 함수는 [최솟값, 최솟값+1, …, 최댓값-1] 형태의 배열을 생성해줌
R.range(최솟값, 최댓값)
※ R.range를 사용해 연속된 숫자 배열을 생성하는 예
import * as R from 'ramda'
console.log(
R.range(1, 9 + 1)
)
(2) R.tap 디버깅용 함수
- R.tap 함수는 2차 고차 함수 형태로 현재 값을 파악할 수 있게 gowa
R.tap(콜백 함수)(배열)
※ R.range 함수로 생성한 배열의 내용을 R.tap 함수를 사용해 화면에 출력하는 예
import * as R from 'ramda'
const numbers: number[] = R.range(1, 9+1)
R.tap(n => console.log(n))(numbers)
(3) R.pipe 함수
- 람다는 compose와 pipe함수를 R.compose, R.pipe 형태로 제공함
※ R.pipe 함수를 사용한 예
import * as R from 'ramda'
const array: number[] = R.range(1, 9+1)
R.pipe(
R.tap(n => console.log(n))
)(array)
(4) 포인트가 없는 함수
- 람다 라이브러리는 200개가 넘는 함수 중 대부분은 2차 고차 함수 형태로 구현되어 있으며, 2차 고차 함수는 포인트가 없는 함수(pointless function) 형태로 사용할 수 있다.
※ 포인트가 없는 함수의 예
import * as R from 'ramda'
export const dump = R.pipe(
R.tap(n => console.log(n))
)
- 람다는 타입스크립트를 고려해 만든 라이브러리가 아니므로 포인트 없는 함수를 일반 화살표 함수로 만들면 오류가 난다. 따라서 타입 단언(type assertion)을 사용해 다음과 같이 구현할 수 있다.
import * as R from 'ramda'
export const dump = <T>(array: T[]): T[] => R.pipe(
R.tap(n => console.log(n))
)(array) as T[]
(5) 자동 커리 이해하기
- 람다 라이브러리의 함수들은 매개변수가 두 개인 일반함수처럼 사용할 수도 있고, 05행처럼 2차 고차 함수로 사용할 수도 있는데 이를 자동 커리(auto curry)라고 한다.
import * as R from 'ramda'
console.log(
R.add(1, 2),
R.add(1)(2)
)
(6) R.curryN 함수
- 람다 라이브러리 함수들은 자동 커리 방식으로 동작할 수 있도록 매개변수의 개수가 모두 정해져 있다.
- R.curryN 함수는 N개의 매개변수를 가진 1차 함수(first function)를 N개의 커리(curry) 매개변수를 가지는 N차 고차 함수로 만들어 준다.
R.curryN(N, 함수)
import * as R from 'ramda'
import {sum} from './sum'
export const curriedSum = R.curryN(4, sum)
//N개의 매개변수를 가진 1차 함수를 N개의 커리 매개변수를 가지는 N차 고차함수로 만들어줌
- 만약 위 식처럼 4차 고차 함수의 매개변수를 만들 때 매개변수 개수를 충족하지 못하면 모두 부분 함수 이므로 [Function]을 결괏값으로 출력한다.
(7) 순수 함수
- 람다 라이브러리는 순수 함수(pure function)을 고려해 설계되었으므로 항상 입력 변수의 상태를 변화시키지 않고 새로운 값을 반환함.
※ 람다라이브러리의 순수 함수 예
import * as R from 'ramda'
const originalArray : number[] = [1, 2, 3]
const resultArray = R.pipe(
R.map(R.add(1))
)(originalArray)
console.log(originalArray, resultArray)
9-3) 배열에 담긴 수 다루기
(1) 선언형 프로그래밍
- 보통 함수형 프로그래밍은 선언형 프로그래밍(declarative programming) 방식으로 코드를 작성하며 선언형 프로그래밍에서 모든 입력 데이터는 단순 데이터보다 배열 형태를 주로 사용
import * as R from 'ramda'
const numbers : number[] = R.range(1, 9+1)
const incNumbers = R.pipe(
R.tap(a => console.log('before inc:', a)),
R.map(R.inc),
R.tap(a => console.log('after inc:', a))
)
const newNumbers = incNumbers(numbers)
console.log(newNumbers)
(2) 사칙 연산 함수
- 람다는 다음과 같은 사칙 연산 관련 함수들을 제공함
R.add(a: number)(b: number)
R.subtract(a: number)(b: number)
R.multiply(a: number)(b: number)
R.divide(a: number)(b: number)
- R.inc는 R.add(1)과 같은 함수며, ‘포인트가 있는’ 함수 형태로 R.add를 사용해 inc 만든 예는 다음과 같다.
const inc = (b:number): number => R.add(1)(b)
- 포인트가 없는 함수로 구현하면 다음과 같다.
const inc = R.add(1)
- inc를 R.map 함수에 ‘포인트가 있는’ 형태로 사용하면 다음과 같다.
R.map((n: number) => R.inc(n))
- R.map(콜백함수)의 콜백 함수를 익명 함수로 구현한 것인데, 현재 inc는 그 자체가 콜백 함수로 사용될 수 있다. 따라서 앞 코드는 다음처럼 간결하게 표현할 수 있다.
R.map(inc)
(3) R.addIndex 함수
- Array.map은 두번째 매개변수로 index를 제공하지만, R.map은 Array.map과는 다르게 index 매개변수를 기본으로 제공하지 않는다. 따라서 R.map이 Array.map처럼 동작하려면 다음처럼 R.addIndex 함수를 사용해 R.map이 index를 제공하는 새로운 함수를 만들어야 한다.
const indexedMap = R.addIndex(R.map)
indexMap((value: number, index: number) => R.add(number)(index))
※ Index를 추가한 ramda의 맵을 구현한 예
import * as R from 'ramda'
const addIndex = R.pipe(
R.addIndex(R.map)(R.add),
// R.addIndex(R.map)((value: number, index: number) => R.add(value)(index)),
R.tap(a => console.log(a))
)
const newMembers = addIndex(R.range(1, 9 + 1))
(4) R.flip 함수
- R.add, R.multiply와 달리 R.subtract, R.divide는 매개변수의 순서에 따라 값이 달라진다.
즉, R.add(1)(2)와 R.add(2)(1)은 같은 값이 되지만, R.subtract(1)(2)는 -1, R.subtract(2)(1)은 1이된다.
R.subtract는 다음과 같이 첫 번째 매개변수값에서 두번째 매개변수 값을 빼는 형태로 구현되어있다.
import * as R from 'ramda'
const subtract = a => b => a-b
const subtractFrom10 = subtract(10)
const newArray = R.pipe(
R.map(subtractFrom10),
R.tap(a => console.log(a))
)(R. range(1, 9+1))
- 람다는 R.flip이라는 함수를 제공하며, R.flip은 R.subtract와 같은 2차 고차 함수의 매개변수 순서를 바꿔준다.
Const reverseSubtract = R.flip(R.subtract)
※ R.flip을 사용해 subtract의 매개변수 순서를 바꿔준 예
import * as R from 'ramda'
const reverseSubtract = R.flip(R.subtract)
const newArray = R.pipe(
R.map(reverseSubtract(10)),
R.tap(a => console.log(a))
)(R.range(1, 9+1))
(5) 사칙 연산 함수들의 조합
※ 수학 공식 f(X) = ax^2 + bx + c 을 타입스크립트로 구현한 예
type NumberToNumberFunc = (number) => number
export const f = (a:number, b: number, c: number): NumberToNumberFunc =>
(x: number): number => a * x ** 2 + b * x + c
위 코드를 다시 람다를 사용해 구현하면 다음과 같이 구현이 가능하다.
import * as R from 'ramda'
export const exp = (N: number) => (x: number): number => x ** N
export const square = exp(2)
type NumberToNumberFunc = (number) => number
export const f = (a: number, b: number, c: number): NumberToNumberFunc =>
(x: number): number => R.add(
R.add(
R.multiply(a)(square(x))
)(R.multiply(b)(x)),
c
)
(6) 2차 방정식의 해(quadratic equation) 구현
※ 위의 2차 함수에 값을 대입해 만든 1차 함수의 예
import {f, exp, square} from './f-using-ramda'
export const quadratic = f(1, 2, 1)
export {exp, square} // exp와 square을 다시 export 한다.
※quadratic 함수를 활용해 1~10까지 변수를 변수 x에 대입한 결과값 구하기
import * as R from 'ramda'
import {quadratic} from './quadratic'
const input: number[] = R.range(1, 10+1)
const quadraticResult = R.pipe(
R.map(quadratic),
R.tap(a => console.log(a))
)(input)
9-4) 서술자와 조건 연산
- Array.filter 함수에서 사용되는 콜백 함수는 boolean 타입 값을 반환해야 하는데, 함수형 프로그래밍에서 boolean 타입 값을 반환해 어떤 조건을 만족하는지를 판단하는 함수를 ‘서술자(predicate’)라고 한다.
(1) 수의 크기를 판단하는 서술자
- 람다는 수를 비교해 true나 false를 반환하는 다음의 서술자들을 제공한다.
R.lt(a)(b): boolean // a < b이면 true.a가 b보다 작음
R.lte(a)(b): boolean // a <= b이면 true.a가 b보다 작거나 같음
R.gt(a)(b) : boolean // a > b이면 true.a가 b보다 큼
R.gte(a)(b) : boolean // a >= b이면 true.a가 b보다 크거나 같음
- 위 함수들은 R.filter 함수와 결합해 포인트가 없는 함수 형태로 사용되며, R.lte(3)은 3 <= x의 의미를 갖는다.
※ 서술자를 사용한 예시 (lte3은 3 <= x를 구한다)
import * as R from 'ramda'
R.pipe(
R.filter(R.lte(3)),
R.tap(n => console.log(n))
)(R.range(1, 10 + 1))
※ 서술자를 사용한 예시2 (gt(6+1)은 x < 7를 구한다)
import * as R from 'ramda'
R.pipe(
R.filter(R.gt(6+1)),
R.tap(n => console.log(n))
)(R.range(1, 10+1))
(2) R.allPass 로직 함수
- R.lt, R.gt 같은 boolean 타입 값을 반환하는 함수들은 R.allPass와 R.anyPass라는 로직함수를 통해 결합할 수 있다.
R.allPass(서술자 배열) // 배열의 조건을 모두 만족하면 true
R.anyPass(서술자 배열) // 배열의 조건을 하나라도 만족하면 true
※ R.allPass 함수를 사용한 예
import * as R from 'ramda'
type NumberToBooleanFunc = (n: number) => boolean
export const selectRange = (min: number, max: number): NumberToBooleanFunc =>
R.allPass([
R.lte(min),
R.gt(max)
])
(3) R.not 함수
- true이면 false를, false이면 true를 반환하는 함수이다. 이미 구현한 함수들을 조합하는 것으로 다음과 같은 형태로 구현이 가능하다.
import * as R from 'ramda'
import {selectRange} from './selectRange'
export const notRange = (min:number, max:number) => R.pipe(selectRange(min, max),
R.not)
(4) R.ifElse 함수
- R.ifElse 함수는 세 가지 매개변수를 포함하는데, 첫 번째는 true/false를 반환하는 서술자, 두 번째는 선택자가 true를 반환할 때 실행할 함수를, 세 번째는 선택자가 false를 반환할 떄 실행할 함수이다.
R.ifElse{
조건 서술자,
True일 때 실행할 함수,
False일 때 실행할 함수
)
※ R.ifElse를 활용해 1~10까지 수 중에서 중간값 6보다 작은 수는 1씩 감소 시키고, 같거나 큰 수는 1씩 증가시키는 예
import * as R from 'ramda'
const input: number[] = R.range(1, 10 + 1), halfValue = input[input.length/2]
const subtractOrAdd = R.pipe(
R.map(R.ifElse(
R.lte(halfValue),
R.inc,
R.dec
)),
R.tap(a => console.log(a))
)
const result = subtractOrAdd(input)
9-5) 문자열 다루기
(1) 문자열 앞뒤의 백색 문자 자르기
- R.trim은 문자열 앞뒤로 공백을 제거해준다.
※R.trim 사용 예
import * as R from 'ramda'
console.log(
R.trim('\t hello \n')
)
(2) 대소문자 전화
- R.toLower 함수는 문자열에서 대문자를 모두 소문자로 전환해주며, R.toUpper은 반대로 소문자를 모두 대문자로 전환해준다.
※ R.toLower, R.toUpper 사용 예
import * as R from 'ramda'
console.log(
R.toLower('HELLO'),
R.toUpper('hello'),
)
(3) 구분자를 사용해 문자열을 배열로 변환
- R.split 함수는 구분자(delimiter)를 사용해 문자열을 배열로 바꿔줌.
문자열 배열 = R.split(구분자)(문자열)
- 문자열 배열은 R.join을 사용해 문자열로 바꿀 수 있다.
문자열 = R.join(구분자)(문자열 배열)
※ R.split 사용예
import * as R from 'ramda'
const words : string[] = R.split(' ', `Hello world!, I'm peter`)
console.log(
words
)
(4) toCamelCase 함수 만들기
- 타입스크립트에서 문자열은 readonly 형태로만 사용할 수 있다. 따라서 문자열을 가공하려면 일단 문자열을 배열로 전환해야 하는데, toCamelcase 함수는 임의의 문자열을 프로그래밍에서 심벌의 이름을 지을 때 많이 사용하는 낙타 등 표기법(camel case convention)으로 바꿔준다.
9-6) chance 패키지로 객체 만들기
- chance 패키지는 그럴듯한 가짜 데이터를 만들어주는 라이브러리로서 람다와 직접 관련된 것은 아니지만, 람다가 제공하는 객체의 속성을 다루는 함수, 객체를 가공하는 함수, 여러 객체를 통합하고 한꺼번에 가공하는 함수들을 사용하려면 그럴듯한 객체 데이터를 필요로 하기 때문에 필요하다.
(1) Icoordinates 타입 객체 만들기
- Iperson 객체는 Ilocation 타입 속성을 포함하며, Ilocation은 다시 Icoordinates 타입의 속성을 포함하는 중첩되는 객체를 구현함.
※ Icoordinates 타입 객체를 만드는 예시
Src/model/coordinates/Icoordinates.ts
export type ICoordinates = {
latitude: number
longitude: number
}
Src/model/coordinates/makeCoordinates.ts
import {ICoordinates} from './ICoordinates'
export const makeICoordinates = (latitude: number, longitude: number):
ICoordinates => ({latitude, longitude})
Src/model/coordinates/makeRandomCoordinates.ts
import {ICoordinates} from './ICoordinates'
import {makeICoordinates} from './makeICoordinates'
import Chance from 'chance'
const c = new Chance
export const makeRandomICoordinates = (): ICoordinates =>
makeICoordinates(c.latitude(), c.longitude())
src/index.ts
import {ICoordinates} from './ICoordinates'
import {makeICoordinates} from './makeICoordinates'
import {makeRandomICoordinates} from './makeRandomIcoordinates'
// ICoordinates와 makeIcoordinates, makeRandomICoordinate를 re-export한다
export {ICoordinates, makeICoordinates, makeRandomICoordinates}
※ import문에서 index.ts 파일은 생략이 가능하다.
- 경로 추적시 해당 디렉터리에 index.ts 파일이 있고 경로의 마지막이 디렉터리 이름이면, 타입스크립트 컴파일러는 디렉터리 밑의 index.ts 파일로 해석한다.
(2) Ilocation 타입 객체 만들기
- Icoordinates 타입 속성을 포함하는 Ilocation 타입 구성하기
Src/model/location/Ilocation.ts
import {ICoordinates} from '../coordinates'
export type ILocation = {
country: string
city?: string
address?: string
coordinates?: ICoordinates
}
Src/model/location/makeILocation.ts
import {ILocation} from './ILocation'
import {ICoordinates, makeICoordinates} from '../coordinates'
export const makeILocation = (
country: string,
city: string,
address: string,
coordinates: ICoordinates
): ILocation =>({country, city, address, coordinates})
Src/model/location/makeRandomILocation.ts
import {ILocation} from './ILocation'
import {makeILocation} from './makeILocation'
import {makeRandomICoordinates} from '../coordinates'
import Chance from 'chance'
const c = new Chance
export const makeRandomILocation = (): ILocation =>
makeILocation(c.country(), c.city(), c.address(), makeRandomICoordinates())
src/model/location/index.ts
import {ILocation} from './ILocation'
import {makeILocation} from './makeILocation'
import {makeRandomILocation} from './makeRandomILocation'
export {ILocation, makeILocation, makeRandomILocation}
src/location-test.ts
import {makeRandomILocation, ILocation} from './model/location'
const location: ILocation = makeRandomILocation()
console.log(location)
(3) Iperson 타입 객체 만들기
Src/model/person/Iperson.ts
import {ILocation} from '../location'
export type IPerson = {
name: string
age: number
title?: string
location?: ILocation
}
export {ILocation}
src/model/person/makeIPerson.ts
import {IPerson, ILocation} from './IPerson'
export const makeIPerson = (
name: string,
age: number,
title?: string,
location?: ILocation
) => ({name, age, title, location})
export {IPerson, ILocation}
src/model/person/makeRandomIPerson.ts
import {IPerson, makeIPerson} from './makeIPerson'
import {makeRandomILocation} from '../location'
import Chance from 'chance'
const c = new Chance
export const makeRandomIPerson = (): IPerson => makeIPerson(c.name(), c.age(), c.profession(), makeRandomILocation())
src/model/person/index.ts
import {IPerson, makeIPerson} from './makeIPerson'
import {makeRandomIPerson} from './makeRandomIPerson'
export {IPerson, makeIPerson, makeRandomIPerson}
src/person-test.ts
import {IPerson, makeRandomIPerson} from './model/person'
const person: IPerson = makeRandomIPerson()
console.log(person)
9-7) 렌즈를 활용한 객체의 속성 다루기
(1) 렌즈란?
- 렌즈(lens)는 하스켈 언어의 Control.Lens 라이브러리 내용 중 자바스크립트에서 동작할 수 있는 게터(getter)와 세터(setter) 기능만을 람다 함수로 구현한 것이다. 람다의 렌즈 기능을 활용하면 객체의 속성값을 얻거나 설정하는 등의 작업을 쉽게 할 수 있다.
※ 렌즈 사용 절차
1. R.lens 함수로 객체의 특정 속성에 대한 렌즈를 만든다
2. 렌즈를 R.view 함수에 적용해 속성값을 얻는다.
3. 렌즈를 R.set 함수에 적용해 속성값이 바뀐 새로운 객체를 얻는다
4. 렌즈와 속성값을 바꾸는 함수를 R.over 함수에 적용해 값이 바뀐 새로운 객체를 얻는다.
(2) R.prop과 R.assoc 함수
- R.prop는 ‘property’의 앞 네 글자를 따서 만든 이름으로, 객체의 특정 속성값을 가져오는 함수이다. 이런 동작을 하는 함수를 게터(getter)라고 한다.
import * as R from 'ramda'
import {IPerson, makeRandomIPerson} from'./model/person'
const person: IPerson = makeRandomIPerson()
const name = R.pipe(
R.prop('name'),
R.tap(name => console.log(name))
)(person)
- 객체의 특정 속성값을 변경하려면 R.assoc 함수를 사용하며, 이런 목적으로 사용하는 함수를 세터(setter)라고 한다.
import * as R from 'ramda'
import {IPerson, makeRandomIPerson} from './model/person'
const getName = R.pipe(R.prop('name'), R.tap(name => console.log(name)))
const person: IPerson = makeRandomIPerson()
const originalName = getName(person)
const modifiedPerson = R.assoc('name', 'Albert Einstein')(person)
const modifiedName = getName(modifiedPerson)
(3) R.lens 함수
- 렌즈 기능을 사용하려면 레즈를 만들어야하며, R.lens, R.prop, R.assoc의 조합으로 만들 수 있다.
export const makeLens = (propName: string) => R.lens(R.prop(propName), R.assoc(propName))
(4) R.veiw, R.set, R.over 함수
- R.view, R.set, R.over 함수에 렌즈를 적용해서 다음과 같은 게터와 세터 그리고 setterUsingFunc과 같은 함수를 만들 수 있다.
※ lens를 사용해 Getter Setter 함수를 구현한 예
import * as R from 'ramda'
export const makeLens = (propName: string) =>
R.lens(R.prop(propName), R.assoc(propName))
export const getter = (lens) => R.view(lens)
export const setter = (lens) => <T>(newValue: T) => R.set(lens, newValue)
export const setterUsingFunc = (lens) => <T, R>(func: (T) => R) => R.over(lens, func)
(5) R.lensPath 함수
- 람다 라이브러이에서는 객체의 중첩 속성(nested property)을 ‘경로(path)’라고 하며, longitude처럼 긴 경로의 속성을 렌즈로 만들려면 R.lensPath 함수를 사용
렌즈 = R.lensPath([‘location’, ‘coordinates’, ‘longitude’])
※ lents path를 사용해 중첩속성 longitude를 갖고와서 활용한 예
import * as R from 'ramda'
import {getter, setter, setterUsingFunc} from './lens'
import {IPerson, makeRandomIPerson} from './model/person'
const longitudeLens = R.lensPath(['location', 'coordinates', 'longitude'])
const getLongitude = getter(longitudeLens)
const setLongitude = setter(longitudeLens)
const setLongitudeUsingFunc = setterUsingFunc(longitudeLens)
const person: IPerson = makeRandomIPerson()
const longitude = getLongitude(person)
const newPerson = setLongitude(0.1234567)(person)
const anotherPerson = setLongitudeUsingFunc(R.add(0.1234567))(person)
console.log(
longitude, getLongitude(newPerson), getLongitude(anotherPerson)
)
9-8) 객체 다루기
(1) R.toPairs와 R.fromPairs 함수
- R.toPairs 함수는 객체의 속성들을 분해해 배열로 만들어준다. 이때 배열의 각 아이템은 [string, any]타입의 튜플이다.
※ R.toPairs 함수 사용 예
import * as R from 'ramda'
import {IPerson, makeRandomIPerson} from './model/person'
const person: IPerson = makeRandomIPerson()
const pairs: [string, any][] = R.toPairs(person)
console.log('pairs', pairs)
- R.fromPairs 함수는 [키:값] 형태의 아이템을 가진 배열을 다시 객체로 만들어 준다.
※ R.fromPairs 함수 사용 예
import * as R from 'ramda'
import {IPerson, makeRandomIPerson} from './model/person'
const pairs :[string, any][] = R.toPairs(makeRandomIPerson())
const person: IPerson = R.fromPairs(pairs) as IPerson
console.log('person', person)
(2) R.keys와 R.values 함수
- R.keys 함수는 객체의 속성 이름만 추려서 string[] 타입 배열로 반환한다.
※ R.keys 함수 사용 예
import * as R from 'ramda'
import {makeRandomIPerson} from './model/person'
const keys: string[] = R.keys(makeRandomIPerson())
console.log('keys', keys)
※ R.values 함수 사용 예
import * as R from 'ramda'
import {makeRandomIPerson} from './model/person'
const values: any[] = R.values(makeRandomIPerson())
console.log('values', values)
(3) R.zipObj 함수
- ‘키 배열(속성 이름 배열)’과 ‘값 배열(속성에 설정할 값 배열)’이라는 두가지 매개변수를 결합해 객체로 만들어 준다.
객체 = R.zipObj(키 배열, 값 배열)
※R.zipObj 함수 사용 예
import * as R from 'ramda'
import {IPerson, makeRandomIPerson} from './model/person'
const originalperson: IPerson = makeRandomIPerson()
const keys: string[] = R.keys(originalperson)
const values: any[] = R.values(originalperson)
const zippedPerson: IPerson = R.zipObj(keys, values) as IPerson
console.log('originalPerson', originalperson, 'zippedPerson:', zippedPerson)
(4) R.mergeLeft와 R.mergeRight 함수
- R.mergeLeft와 R.mergeRight 함수는 두 개의 객체를 입력받아 두 객체의 속성들을 결합해 새로운 객체를 생성합니다.
새로운 객체 = R.mergerLeft(객체1)(객체2) : 속성값이 다를 때 왼쪽 객체의 우선순위가 높음
새로운 객체 = R.mergeRight(객체1)(객체2) : 속성값이 다를 때 오른쪽 객체의 우선순위가 높음
※mergeLeft 함수 사용 예
import * as R from 'ramda'
const left = {name: 'Jack'}, right={name: 'Jane', age: 32}
const person = R.mergeLeft(left, right)
console.log(person)
※ mergeRight 함수의 사용예
import * as R from 'ramda'
const left = {name:'Jack'}, right = {name: 'Jane', age: 32}
const person = R.mergeRight(left, right)
console.log(person)
(5) R.mergeDeepLeft와 R.mergeDeepRight 함수
- mergeLeft와 mergeRight 함수는 객체의 속성에 담긴 객체를 바꾸지는 못한다. 즉 속성 안의 객체들의 값을 바꿔주진 못한다.
하지만, mergeDeepLeft와 mergeDeepRight는 경로(path)의 속성값들도 바꿀수 있다.
※ mergeDeepRight 함수 사용 예
import * as R from 'ramda'
import {IPerson, makeRandomIPerson} from './model/person'
import {ILocation, makeRandomILocation} from './model/location'
import {ICoordinates, makeRandomICoordinates} from './model/coordinates'
const person: IPerson = makeRandomIPerson()
const location: ILocation = makeRandomILocation()
const coordinates: ICoordinates = makeRandomICoordinates()
const newLocation = R.mergeDeepRight(location, {coordinates})
const newPerson = R.mergeDeepRight(person, {location: newLocation})
console.log('person', person)
console.log('newPerson', newPerson)
9-9) 배열 다루기
(1) R.prepend와 R.append 함수
- 기존 배열의 앞뒤에 새 아이템을 삽입한 새 배열을 만들어 준다. 순수 함수 관점에서 기존 배열에 아이템을 직접 삽입하면 기존 배열의 내용을 훼손하게 되므로 이 함수들을 사용한다.
R.prepend 함수는 배열의 맨 앞에 아이템을 삽입한다.
※R.prepend 함수 사용 예
import * as R from 'ramda'
const array: number[] = [3, 4]
const newArray = R.prepend(1)(array)
console.log(array, newArray)
R.append 함수는 배열의 맨 뒤에 아이템을 삽입한다.
※ R.append 함수 사용 예
import * as R from 'ramda'
const array: number[] = [3, 4]
const newArray = R.append(1)(array)
console.log(array, newArray)
(2) R.flatten 함수
- 복잡합 배열을 1차원의 평평한 배열로 바꿔줌
※ flatten함수 사용 예
import * as R from 'ramda'
const array = R.range(1, 2+1).map((x: number) => {
return R.range(1, 2 + 1).map((y: number) => {
return [x, y]
})
})
console.log(array)
const flattendArray = R.flatten(array)
console.log(flattendArray)
(3) R.unnest 함수
- R.unnest 함수는 R.flatten보다 조금 정교하게 배열을 가공해준다.
※ unnest 함수 사용예
import * as R from 'ramda'
const array = R.range(1, 2 + 1).map((x: number) => {
return R.range(1, 2 + 1).map((y: number) => {
return [x, y]
})
})
console.log(array)
const unnestedArray = R.unnest(array)
console.log(unnestedArray)
const twoUnnestedArray = R.pipe(R.unnest, R.unnest)(array)
console.log(twoUnnestedArray)
(4) R.sort 함수
- 배열 타입이 number[]이라면 R.sort 함수를 사용해 배열을 내림차순이나 오름차순으로 정렬할 수 있다. (첫번째 매개변수에 콜백 함수를 입력받는 2차 고차 함수)
정렬된 배열 = R.sort(콜백 함수)(배열)
콜백 함수는 다음과 같은 형태로 구현
// 마이너스값이면 오름차순, 0이나 플러스값이면 내림차순
(a: number, b: number): number => a – b
※ sort 함수 활용 예
import * as R from 'ramda'
type voidToNumberFunc = () => number
const makeRandomNumber = (max: number) : voidToNumberFunc =>
(): number => Math.floor(Math.random() * max)
const array = R.range(1, 5 + 1).map(makeRandomNumber(100))
const sortedArray = R.sort( (a:number, b: number): number => a - b)(array)
console.log(array, sortedArray)
(5) R.sortBy 함수
- 배열에 담긴 아이템이 객체일때, 특정 속성값에 따라 정렬해야하므로 이때 사용하는 함수가 sortBy이다.
정렬된 배열 = R.sortBy(객체 속성을 얻는 함수)(배열)
※ sortBy함수 사용 예
import * as R from 'ramda'
import {IPerson, makeRandomIPerson} from './model/person'
import {displayPersons} from './displayPersons'
const persons: IPerson[] = R.range(1, 4 + 1).map(makeRandomIPerson)
const nameSortedPersons = R.sortBy(R.prop('name'))(persons)
const ageSortedPersons = R.sortBy(R.prop('age'))(persons)
displayPersons('sorted by name: ')(nameSortedPersons)
displayPersons('sorted by age: ')(ageSortedPersons)
※ displayPersons
import * as R from 'ramda'
import {IPerson} from './model/person'
export const displayPersons = (prefix: string) => R.pipe(
R.map((person: IPerson) => ({name:person.name, age: person.age})),
R.tap(o => console.log(prefix, o))
) as any
(6) R.sortWith 함수
- sortBy 함수는 오름차순, 내림차순 정렬을 하지 못하고 항상 오름차순으로만 정렬하지만, R.sortWith 함수는 R.ascend, R.descend 함수와 함꼐 사용되어 오름차순, 내림차순 정렬을 할 수 있다.
※ R.sortWith, R.descend 사용 예
import * as R from 'ramda'
import {IPerson, makeRandomIPerson} from './model/person'
import {displayPersons} from './displayPersons'
const persons: IPerson[] = R.range(1, 4 + 1).map(makeRandomIPerson)
const nameSortedPersons = R.sortWith([
R.descend(R.prop('name'))
])(persons)
displayPersons('sorted by name: ')(nameSortedPersons)
9-10) 조합 논리 이해하기
- 함수형 프로그래밍의 가장 큰 이론적인 배경은 람다 수학(lambda calculus)과 조합 논리학(combinatory logic), 그리고 카테고리 이론(category theory)이다. 그러나 람다 수학의 모든 이론을 컴퓨터 프로그래밍 언어로 표현할 수 없어 어떤 제한된 범위에서 람다 수학을 구하기 위해 조합 논리학이 생겼다.
(1) 조합자란?
- 조합 논리학은 ‘조합자(combinator)’라는 특별한 형태의 고차 함수들을 결합해 새로운 조합자를 만들어 내며 함수형 언어의 컴파일러를 만드는데 필요한 이론을 검증하고 개발할 때 주로 사용된다.
※ 람다가 제공하는 조합자
I(Identity) : R.identity
K(constant) : R.always
T(thrush) : R.applyTo
W(duplication) : R.unnest
C(flip) : R.flip
S(substitution) : R.ap
(2) R.chain 탐구
- 람다 라이브러리는 R.chain 함수를 제공하며, 함수를 매개변수로 받아 동작하는 함수이다.
사용법은 매개변수가 한 개일때와 두 개일때로 나뉜다.
R.chain(콜백 함수1)
R.chain(콜백 함수1, 콜백 함수2)
※ chain 함수 사용 예
import * as R from 'ramda'
const array = [1, 2, 3]
R.pipe(
R.chain(n => [n, n]),
R.tap(n => console.log(n))
)(array)
R.pipe(
R.chain(R.append, R.head),
R.tap(n => console.log(n))
)(array)
- R.chain 함수는 매개변수가 한 개 일때는 아래 flatMap함수처럼 동작함
import * as R from 'ramda'
export const flatMap = (f) => R.pipe(
R.map(f),
R.flatten
)
- R.chain 함수의 매개변수가 두 개 일때는 아래 chainTwoFunc 함수처럼 동작함
import * as R from 'ramda'
export const chainTwoFunc = (firstFn, secondFn) => (x) => firstFn(secondFn(x), x)
(3) R.flip 조합자
- R.flip은 2차 고차 함수의 매개변수 순서를 서로 바꿔준느 역할을 함
(4) R.identity 조합자
- 다음과 같이 단순한 조합자이지만, 반드시 함수가 있어야 하는 곳에 위치할 때 위력을 발휘함.
Const identity = x => x
(5) R.always 조합자
- R.always 조합자는 다음처럼 두 개의 고차 매개변수 중 첫번째 것을 반환하며, R.always 조합자는 constant라는 의미에서 ‘K-조합자’라고 하며 K는 독일어로 ‘Konstante(상수)’를 의미한다.
Const always = x => y => x
※R.always 조합자 예시
import * as R from 'ramda'
const always = a => b => a
const flip = cb => a => b => cb(b)(a)
const first = <T>(a: T) => (b: T): T => always(a)(b)
const second = <T>(a: T) => (b: T): T => flip(always)(a)(b)
console.log(
first(1)(2),
second(1)(2)
)
(6) R.applyTo 조합자
- R.applyTo 조합자는 값을 첫 번쨰 매개변수로 하며, 이 갑슬 입력으로 하는 콜백함수를 두 번째 매개변수로 받아 작동한다.
Const applyTo = value => cb => cb(value)
import * as R from 'ramda'
const T = value => R.pipe(
R.applyTo(value),
R.tap(value => console.log(value))
)
const value100 = T(100)
const sameValue = value100(R.identity)
const add1Value = value100(R.add(1))
(7) R.ap 조합자
- 콜백 함수들의 배열을 첫 번째 매개변수로, 배열을 두 번째 매개변수로 입력받는 2차 고차 함수이다.
Const ap = ([콜백 함수]) => 배열 => [콜백 함수](배열)
import * as R from 'ramda'
const callAndAppend = R.pipe(
R.ap([R.multiply(2)]),
R.tap(a => console.log(a))
)
const input = [1, 2, 3]
const result = callAndAppend(input)