이번 장은 람다라는 이름의 유명한 함수형 유틸리티 라이브러리의 기능을 알아보자.
9-1 람다 라이브러리 소개
람다 패키지는 8장에서 소개한 compose나 pipe를 사용하는 함수 조합을 쉽게 할 수 있게 설계된 오픈소스 자바스크립트 라이브러리로서 다음과 같은 특징이 있다.
- 타입스크립트 언어와 100% 호환
- compose와 pipe 함수 제공
- 자동 커리 기능 제공
- 포인트가 없는 고차 도움 함수 제공
- 조합 논리(combinatory logic) 함수 일부 제공
- 하스켈 렌즈 라이브러리 기능 일부 제공
- 자바스크립트 표준 모나드 규격과 호환
실습 프로젝트 구성
npm init -y
npm i -D typescript ts-node @types/node
mkdir src
// ramda package
npm i -S ramda
npm i -D @types/ramda
// 가짜 데이터를 만들어 주는 chance 패키지 설치
npm i -S chance
npm i -D @types/chance
// tsconfig.json
// 람다 라이브러리는 자바스크립트를 대상으로 설계되었다.
// 따라서 타입스크립트는 any 타입을 완전히 자바스크립트적으로 해석해야 하므로 속성값을 false로 설정
noImplicitAny : false
ramda 패키지 불러오기
보통 ramda 패키지를 불러와서 R이라는 심벌로 사용힌디
import * as R from "ramda"
9-2 람다 기본 사용법
R.range 함수
R.range 함수는 다음 형식으로 사용하며, [최솟값, 최솟값 + 1… , 최대값 -1] 형태의 배열을 생성해 준다.
R.range(최솟값, 최대값)
다음 코드는 R.range 함수를 이용해 1부터 9까지 연속된 숫자 배열을 생성하는 예이다.
import * as R from 'ramda'
console.log(
R.range(1, 9 + 1) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
)
R.tap 디버깅용 함수
복잡한 함수를 간단하게 구현하려면 함수 조합을 이용해야한다. 이때 단계별로 값이 어떻게 변하는지 파악하면서 코드를 작성한다. 주로 계획한 설계대로 조합한 함수가 동작하지 않거나 어디서 논리 오류가 발생했는지 디버깅할 때 그렇다. 이대 람다가 제공하는 tap 함수가 2차 고차 함수 형태로 현재 값을 파악할 수 있게 해준다.
R.tap(콜백함수)(배열)
import * as R from 'ramda'
const numbers: number[] = R.range(1, 9 + 1)
R.tap(n => console.log(n))(numbers) // [1, 2, 3, 4, 5, 6, 7, 8, 9]
R.pipe 함수
compose와 pipe 함수를 R.compose , R.pipe 형태로 제공한다. 하지만 로직을 구현할 때는 R.pipe 함수가 이해하기 더 편한 면이 있다. 따라서 이번 장의 모든 샘플 코드에서 함수를 조합할 때는 R.pipe함수를 사용한다.
import * as R from 'ramda'
const array: number[] = R.range(1, 10)
R.pipe(
R.tap(n => console.log(n)) // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
)(array)
포인트가 없는 함수
람다 라이브러리는 200개가 넘는 함수를 제공하지만 대부분은 2차 고차 함수 형태로 구현되어 있다. 2차 고차 함수는 포인트가 없는 함수 형태로 사용할 수 있다. 예를 들어 dump 함수를 보자.
import * as R from 'ramda'
export const dump = R.pipe(
R.tap(n => console.log(n))
)
dump는 포인트가 없는 함수의 전형적인 모습이다. 실제 함수를 동작해보자
import * as R from 'ramda'
import { dump } from './dump'
dump(R.range(1, 10)) // // [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
람다는 타입스크립트를 고려해 만든 라이브러리가 아니다. 따라서 포인트가 없는 함수의 모습이 낯설다고 해서 일반 화살표 함수로 만들면 오류가 발생한다.
이러한 오류는 타입 단언을 사용해서 해결할 수 있다.
import * as R from 'ramda'
export const dump = <T>(array: T[]): T[] => R.pipe(
R.tap(n => console.log(n))
)(array) as T[]
결론적으로, dump처럼 포인트가 없는 함수를 만드는 것이 불필요한 자바스크립트 오류를 만나지 않는 방안이 된다.
자동 커리 이해하기
람다 라이브러리의 함수들은 매개변수가 두 개인 일반 함수처럼 사용할 수도 있고, 2차 고차 함수로 사용할 수도 있다. 람다 라이브러리에서는 이를 자동 커리라고 한다.
import * as R from 'ramda'
console.log(
R.add(1, 2), //3
R.add(1)(2) //3
)
R.curryN 함수
람다 라이브러리의 함수들은 자동 커리 방식으로 동작할 수 있도록 매개변수의 개수가 모두 정해져 있다. 따라서 sum 함수처럼 가변 인수 형태로 구현된 함수는 없다.
export const sum = (...numbers: number[]): number =>
numbers.reduce((result:number, sum: number) => result + sum, 0)
만약, sum 함수를 N차 고차 함수로 만들고 싶다면 R.curryN 함수를 사용할 수 있다. N개의 매개변수를 가진 1차 함수를 N개의 커리 매개변수를 가지는 N차 고차 함수로 만들어 준다.
R.curryN(N, 함수)
다음 코드는 앞의 sum 함수에 R.curryN(4,sum)을 적용해 curriedSum(1)(2)(3)(4)로 동작하는 4차 고차 함수를 만든 예이다.
import * as R from 'ramda'
import {sum} from './sum'
export const curriedSum = R.curryN(4, sum)
다음 코드는 curriedSum 함수의 테스트 코드로서 4차 고차 함수의 개수를 충족하는 10이라는 값을 출력한다.
import { curriedSum } from './curriedSum'
console.log(
curriedSum(), // [Function]
curriedSum(1), // [Function]
curriedSum(1)(2), // [Function]
curriedSum(1)(2)(3), // [Function]
curriedSum(1)(2)(3)(4) // 10
)
순수 함수
람다 라이브러리는 순수 함수를 고려해 설계되었다. 따라서 람다 라이브러리가 제공하는 함수들은 항상 입력 변수의 상태를 변화시키지 안혹 새로운 값을 변환한다.
다음 코드는 resultArray를 만드는 데 사용된 originalArray는 자신의 모습 그대로를 유지하고 있다.
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) // [ 1, 2, 3 ] [ 2, 3, 4 ]
9**-3 배열에 담긴 수 다루기**
선언형 프로그래밍
보통 선언형 프로그래밍은 선언형 프로그래밍 방식으로 코드를 작성한다. 모든 입력 데이터는 다음처럼 단순 데이터보다 배열 형태를 주로 사용한다.
import * as R from "ramda"
const value = 1;
const newValue = R.inc(value) // 2
const newArray = R.pipe(R.map(R.inc))([value]) // 2
다음 예제는 R.tap 디버깅 함수를 사용해 실행하기 전과 후에 배열의 아이템값을 화면에 출력한다.
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)), // before inc: [ 1, 2, 3, 4, 5, 6, 7, 8, 9 ]
R.map(R.inc),
R.tap(a => console.log('after inc:', a)), // after inc: [ 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
)
const newNumbers = incNumbers(numbers)
//console.log(newNumbers) // [ 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
R.pipe 안에서는 console.log 문을 직접 사용할 수 없으므로 반드시 R.tap 함수를 사용해야 한다.
사칙 연산 함수
람다는 사칙 연산 관련 함수들을 제공한다.
R.add(a: number)(b: number) // a + b
R.subtract(a: number)(b: number) // a - b
R.multiply(a: number)(b: number) // a * b
R,divide(a: number)(b: number) // a / b
import * as R from 'ramda'
const incNumbers = R.pipe(
R.map(R.add(1)),
R.tap(a => console.log('after add(1):', a)), // after add(1): [ 2, 3, 4, 5, 6, 7, 8, 9, 10 ]
)
const newNumbers = incNumbers(R.range(1, 9 + 1))
R.addIndex 함수
Array.map은 두 번째 매개변수로 index를 제공하지만, R.map은 Array.map과는 다르게 index 매개변수를 기본으로 제공하지 않는다. 따라서 다음처럼 R.addIndex 함수를 사용해 R.map이 index를 제공하는 새로운 함수를 만들어야 한다.
const indexedMap = R.addIndex(R.map)
indexMap((value : number, index : number) => R.add(number)(index))
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)) // [ 1, 3, 5, 7, 9, 11, 13, 15, 17 ]
)
// @ts-ignore
const newNumbers = addIndex(R.range(1, 9 + 1))
R.flip 함수
R.flip은 R.subtract와 같은 2차 고차 함수의 매개변수 순서를 바꿔준다. R.flip 함수를 이용해 b-a 공식처러 ㅁ동작하는 함수는 다음처럼 작성할 수 있다.
const reverseSubtract = R.flip(R.subtract)
import * as R from 'ramda'
const reverseSubtract = R.flip(R.subtract)
const newArray = R.pipe(
R.map(reverseSubtract(10)), // value - 10
R.tap(a => console.log(a)) // [ -9, -8, -7, -6, -5, -4, -3, -2, -1]
)(R.range(1, 9+1))
9-4 서술자와 조건 연산
Array.filter 함수에서 사용되는 콜백 함수는 boolean 타입 값을 반환해야 하는데, 함수형 프로그래밍에서 boolean 타입 값을 반환해 어떤 조건을 만족하는지를 판단하는 함수를 서술자라고 한다.
수의 크기를 판단하는 서술자
람다는 수를 비교해 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 함수와 결합해 포인트가 없는 함수 형태로 사용된다.
import * as R from 'ramda'
R.pipe(
R.filter(R.lte(3)),
R.tap(n => console.log(n)) // [ 3, 4, 5, 6, 7, 8, 9, 10 ]
)(R.range(1, 10 + 1))
R.allPass 로직 함수
R.lt, R.gt 처럼 boolean 타입의 값을 반환하는 함수들은 R.allPass와 R.anyPass라는 로직 함수를 통해 결합할 수 있다.
R.allPAss(서술자 배열) // 조건을 모두 만족하면 true
R.anyPass(서술자 배열) // 조건을 하나라도 만족하면 true
다음 selectRange 함수는 x가 min ≤ x < max 조건을 만족하는지 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)
])
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)
R.ifElse 함수
세 가지 매개변수를 포함한다. 첫 번째는 true/false를 반환하는 서술자, 두 번째는 선택자가 true를 반환할 때 실행할 함수를 세 번째는 선택자가 false를 반환할 때 실행할 함수이다.
import * as R from 'ramda'
const input: number[] = R.range(1, 10 + 1), halfValue = input[input.length / 2] // 6
const subtractOrAdd = R.pipe(
R.map( R.ifElse(
R.lte(halfValue), //x => half <= x,
R.inc,
R.dec,
) ),
R.tap(a => console.log(a)) // [ 0, 1, 2, 3, 4, 7, 8, 9, 10, 11 ]
)
const result = subtractOrAdd(input)
9-5 문자열 다루기
문자열 앞뒤의 백색 문자 자르기
R.trim 함수는 문자열 앞뒤의 공백을 제거해 준다.
import * as R from 'ramda'
console.log(
R.trim(" \\t hello \\n") // hello
)
toCamelCase 함수 만들기
타입스크립트에서 문자열은 readonly 형태로만 사용할 수 있다. 따라서 Hello world를 helloWorld 처럼 가공하려면 문자열을 배열로 전환해야 한다.
import * as R from 'ramda'
type StringToStringFunc = (string) => string
export const toCamelCase = (delim: string): StringToStringFunc => {
const makeFirstToCapital = (word: string) => {
const characters = word.split('')
return characters.map((c, index) => (index == 0 ? c.toUpperCase() : c)).join('')
}
const indexedMap = R.addIndex(R.map)
return R.pipe(
R.trim, // 문자열의 좌우 공백 문자를 제거합니다
R.split(delim), // 문자열을 delim 문자를 근거로 배열로 전환합니다
R.map(R.toLower), // 배열에 든 모든 문자열을 소문자열로 바꿉니다
indexedMap(
(value: string, index: number) => (index > 0 ? makeFirstToCapital(value) : value) // 배열에 담긴 두 번째 문자열부터 첫 문자만 대문자로 바꿉니다
),
// @ts-ignore
R.join('') // 배열을 다시 문자열로 변환합니다.
) as StringToStringFunc
}
9-6 chance 패키지로 객체 만들기
chance 패키지는 그럴듯한 가짜 데이터를 만들어주는 라이브러리로서 람다와 직접 관련된 것은 아니다. 하지만 람다가 제공하는 객체의 속성을 다루는 함수, 객체를 가공하는 하뭇, 여러 객체를 통합하고 한꺼번에 가공하는 함수들을 사용하려면 그럴듯한 객체 데이터가 필요하다.
9-7 렌즈를 활용한 객체의 속성 다루기
렌즈란?
렌즈는 하스켈 언어의 Control.Lens 라이브러리 내용 중 자바스크립트에서 동작할 수 있는 게터와 세터 기능만을 람다 함수로 구현한 것이다. 람다의 렌즈 기능을 활용하면 객체의 속성값을 얻거나 설정하는 등의 작업을 쉽게 할 수 있다. 렌즈 기능은 객체의 특정 속성에 대한 렌즈를 만들고, 나머지 과정을 진행하는 방식으로 이용한다.
- R.lens 함수로 객체의 특정 속성에 대한 렌즈를 만든다.
- 렌즈를 R.view 함수에 적용해 속성값을 얻는다.
- 렌즈를 R.set 함수에 적용해 속성값이 바뀐 새로운 객체를 얻는다
- 렌즈와 속성값을 바꾸는 함수를 R.over 함수에 적용해 값이 바뀐 새로운 객체를 얻는다.
R.prop과 R.assoc 함수
렌즈 기능을 이해하려면 R.prop와 R.assoc 함수를 알아야한다. R.prop는 property의 앞 네 글자를 따서 만든 이름이다. R.prop는 객체의 특정 속성값을 가져오는 함수로서, 이런 동작을 하는 함수를 게터라고 한다.
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)) // Ollie Schwartz(랜덤하게 생성된 이름입니다)
)(person)
객체의 특성 속성값을 변경하려면 R.assoc 함수를 사용하는데, 이런 목적으로 사용하는 함수를 세터라고 한다.
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) // Jeremiah Reeves
const modifiedPerson = R.assoc('name', 'Albert Einstein')(person)
const modifiedName = getName(modifiedPerson) // Albert Einstein
R.lens 함수
렌즈 기능을 사용하려면 일단 렌즈를 만들어야 한다. 렌즈는 다음처럼 R.lens, R.prop, R.assoc의 조합으로 만들 수 있다.
export const makeLens = (propName: string) => R.lens(R.prop(propName), R.assoc(propName))
R.view, R.set, R.over 함수
렌즈를 만들었으면 R.view, R.set, R.over 함수에 렌즈를 적용해 보자.
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)
import * as R from 'ramda'
import {makeLens, getter, setter, setterUsingFunc} from './lens'
import {IPerson, makeRandomIPerson} from './model/person'
const nameLens = makeLens('name')
const getName = getter(nameLens)
const setName = setter(nameLens)
const setNameUsingFunc = setterUsingFunc(nameLens)
const person: IPerson = makeRandomIPerson()
const name = getName(person)
const newPerson = setName('Albert Einstein')(person)
const anotherPerson = setNameUsingFunc(name => `'Mr. ${name}'`)(person)
const capitalPerson = setNameUsingFunc(R.toUpper)(person)
console.log(
name, getName(newPerson), getName(anotherPerson), getName(capitalPerson)
// Micheal Chandler Albert Einstein 'Mr. Micheal Chandler' MICHEAL CHANDLER
)
9-8 객체 다루기
R.toPairs와 R.fromPairs 함수
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)
/* pairs [
[ 'name', 'Jeff Bridges' ],
[ 'age', 57 ],
[ 'title', 'Flight Engineer' ],
[
'location',
{
country: 'LY',
city: 'Guvmevu',
address: '701 Veuha Plaza',
coordinates: [Object]
}
]
] */
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) /* person: {
name: 'Sadie George',
age: 35,
title: 'Tax Specialist',
location: {
country: 'RO',
city: 'Giwraak',
address: '569 Hopve Lane',
coordinates: { latitude: -34.78422, longitude: -114.44199 }
}
} */
R.keys와 R.values 함수
R.keys 함수는 객체의 속성 이름만 추려서 string[] 타입 배열로 반환한다.
import * as R from 'ramda'
import {makeRandomIPerson} from './model/person'
const keys: string[] = R.keys(makeRandomIPerson())
console.log('keys:', keys) // keys: [ 'name', 'age', 'title', 'location' ]
R.values 함수는 객체의 속성값만 추려서 any[] 타입 배열로 반환한다.
import * as R from 'ramda'
import {makeRandomIPerson} from './model/person'
const values: any[] = R.values(makeRandomIPerson())
console.log('values:', values) /* values: [
'Dora Young',
62,
'MIS Manager',
{
country: 'ST',
city: 'Wuarujov',
address: '1937 Sijuw Point',
coordinates: { latitude: 83.83357, longitude: 12.53624 }
}
] */
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) /* originalPerson: {
name: 'Amy Burgess',
age: 63,
title: 'Program Manager',
location: {
country: 'AQ',
city: 'Hahmegsel',
address: '154 Neser View',
coordinates: { latitude: -77.55442, longitude: -82.01775 }
}
} zippedPerson: {
name: 'Amy Burgess',
age: 63,
title: 'Program Manager',
location: {
country: 'AQ',
city: 'Hahmegsel',
address: '154 Neser View',
coordinates: { latitude: -77.55442, longitude: -82.01775 }
}
} */
9-9 배열 다루기
R.prepend와 R.append 함수
R.prepend와 R.append는 기존 배열의 앞뒤에 새 아이템을 삽입한 새 배열을 만들어 준다. 순수 함수 관점에서 기존 배열에 아이템을 직접 삽입하면 기존 배열의 내용을 훼손하게 되므로 이 함수들이 만들어졌다.
// prepend
import * as R from 'ramda'
const array: number[] = [3, 4]
const newArray = R.prepend(1)(array)
console.log(array, newArray) // [ 3, 4 ] [ 1, 3, 4 ]
// append
import * as R from 'ramda'
const array: number[] = [3, 4]
const newArray = R.append(1)(array)
console.log(array, newArray) // [ 3, 4 ] [ 3, 4, 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) // [ [ [ 1, 1 ], [ 1, 2 ] ], [ [ 2, 1 ], [ 2, 2 ] ] ]
const flattendArray = R.flatten(array)
console.log(flattendArray) // [ 1, 1, 1, 2, 2, 1, 2, 2 ]
// unnest 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) // [ [ [ 1, 1 ], [ 1, 2 ] ], [ [ 2, 1 ], [ 2, 2 ] ] ]
const unnestedArray = R.unnest(array)
console.log(unnestedArray) // [ [ 1, 1 ], [ 1, 2 ], [ 2, 1 ], [ 2, 2 ] ]
// @ts-ignore
const twoUnnestedArray = R.pipe(R.unnest, R.unnest)(array)
console.log(twoUnnestedArray) // [ 1, 1, 1, 2, 2, 1, 2, 2 ]
// 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 => a - b)(array)
console.log(array, sortedArray) // [ 65, 61, 70, 12, 52 ] [ 12, 52, 61, 65, 70 ]
// 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) /* sorted by name: [
{ name: 'Adrian Elliott', age: 55 },
{ name: 'Alta Reynolds', age: 61 },
{ name: 'Curtis Rhodes', age: 43 },
{ name: 'Jerome Ortiz', age: 57 }
] */
displayPersons('sorted by age: ')(ageSortedPersons) /* sorted by age: [
{ name: 'Curtis Rhodes', age: 43 },
{ name: 'Adrian Elliott', age: 55 },
{ name: 'Jerome Ortiz', age: 57 },
{ name: 'Alta Reynolds', age: 61 }
] */
// sortWith
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) /* sorted by name: [
{ name: 'Sadie Webb', age: 23 },
{ name: 'Rosetta Davidson', age: 52 },
{ name: 'Maggie Diaz', age: 23 },
{ name: 'Anne Conner', age: 41 }
] */
9-10 조합 논리 이해하기
함수형 프로그래밍의 가장 큰 이론적인 배경은 람다 수학과 조합 논리학, 그리고 카테고리 이론이다. 그런데 람다 수학의 모든 이론을 컴퓨터 프로그래밍 언어로 표현할 수 없으므로 어떤 제한된 범위에서 람다 수학을 구현하기 위해 조합 논리학이 생겨났다.
조합자란
조합 논리학은 조합자라는 특별한 형태의 고차 함수들을 결합해 새로운 조합자를 만들어 내는 것이다. 이는 함수형 언어의 컴파일러를 만드는 데 필요한 이론을 검증하고 개발할 때 주로 사용된다.
R.chain 함수 탐구
이 함수는 다음처럼 함수를 매개변수로 받아 동작한느 함수로서, 매개변수가 한 개일 때와 두 개일 때의 동작이 조금 다르다.
import * as R from 'ramda'
const array = [1, 2, 3]
R.pipe(
R.chain(n => [n, n]),
R.tap(n => console.log(n)) // [ 1, 1, 2, 2, 3, 3 ]
)(array)
R.pipe(
R.chain(R.append, R.head),
R.tap(n => console.log(n)) // [ 1, 2, 3, 1 ]
)(array)
R.flip 조합자
2차 고차 함수의 매개변수 순서를 서로 바꿔주는 역할을 한다.
import * as R from 'ramda'
const flip = cb => a => b => cb(b)(a)
const reverseSubtract = flip(R.subtract)
const newArray = R.pipe(
R.map(reverseSubtract(10)), // value - 10
R.tap(a => console.log(a)) // [ -9, -8, -7, -6, -5, -4, -3, -2, -1]
)(R.range(1, 9+1))
R.identity 조합자
가장 단순한 조합자이지만, 조합자의 구조상 반드시 함수가 있어야 하는 곳에 위치할 때 그 위력을 발휘한다.
const identity = x => x
import * as R from 'ramda'
import {flatMap} from './flatMap'
const unnest = flatMap(R.identity)
const array = [ [1], [2], [3] ]
R.pipe(
unnest,
R.tap(n => console.log(n)) // [ 1, 2, 3 ]
)(array)
R.always 조합자
두 개의 고차 매개변수 중 첫 번째 것을 반환한다. 이런 특징 때문에 R.always 조합자는 constant라는 의미에서 k-조합자라고 하는데, 이때 k의 독일어로 Konstante(상수)를 의미한다.
const always = x => y => x
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), // 1
second(1)(2) // 2
)
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) // 100
const add1Value = value100(R.add(1)) // 101
R.ap 조합자
콜백 함수들의 배열을 첫 번째 매개변수로, 배열을 두 번째 매개변수로 입력받는 2차 고차 함수이다.
const ap = ([콜백함수]) => 배열 => [콜백함수](배열)
콜백함수가 한 개일 때는 마치 R.map처럼 동작한다.
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) // [ 2, 4, 6 ]
콜백 함수가 두 개일 때는 마치 R.chain(n ⇒ [n,n]) 형태로 동작한다.
다음 코드는 두 콜백 함수를 적용한 각각의 배열을 만든 다음, 연산이 끝나면 이 배열을 모두 통합해 한 개로 만들어준다.
import * as R from 'ramda'
const callAndAppend = R.pipe(
R.ap([R.multiply(2), R.add(10)]),
R.tap(a => console.log(a))
)
const input = [1, 2, 3]
const result = callAndAppend(input) // [ 2, 4, 6, 11, 12, 13 ]
다음 코드는 R.ap 조합자의 이런 성질을 이용해 [1,2,3] 배열을 세 번 복제한 뒤 통합한 배열을 만드는 코드이다.
import * as R from 'ramda'
const repeat = (N, cb) => R.range(1, N + 1).map(n => cb)
const callAndAppend = R.pipe(
R.ap(repeat(3, R.identity)),
R.tap(a => console.log(a))
)
const input = [1, 2, 3]
const result = callAndAppend(input) // [ 1, 2, 3, 1, 2, 3, 1, 2, 3 ]
'Books > Do It 타입스크립트 프로그래밍' 카테고리의 다른 글
8장 함수 조합의 원리와 응용 (0) | 2023.01.04 |
---|---|
7장 비동기 콜백 함수 (0) | 2022.12.30 |
6장 반복기와 생성기 (0) | 2022.12.12 |
5장 배열과 튜플 (0) | 2022.12.11 |
4장 함수와 메서드 (0) | 2022.11.29 |