Million Dreams
100만개의 꿈을 꾸는 개발자 지망생
TypeScript (11)
Do it! 타입스크립트 프로그래밍 11장

Chap11 모나드

11-1) 모나드 이해하기

- 모나드(Monad)카테고리 이론(category theory)’ 분야에서 사용되는 용어로, 프로그래밍에서 모나드는 코드 설계 패턴(design pattern)’으로서 몇 개의 인터페이스를 구현한 클래스다.

 

(1) 타입 클래스란?

- 2차 고차 함수 callMap은 두 번째 고차 매개변수 bmap이라는 메서드를 가졌다고 가정함

const callMap = fn => b => b.map(fn)

 

- 제대로 된 코드를 사용하지 않으면 프로그램은 제대로 인식을 하지 못해 비정상적으로 종료되므로, 매개변수 b가 반드시 map 메서드가 있는 타입이라고 제한을 해야함

const callMap = <TU>(fn: (T) => U) => <T extends {map(fn)}>(b: T) => b.map(fn)

이를 통해 비정상으로 종료되는 것을 막을 수 있음

 

- 하스켈(haskell) 언어는 객체지향 언어와 달리 모나드 방식 설계를 통해 mapof가 있는 Monad<T> 클래스를 만듦

class Monad<T> {

    constructor(public value: T) {}

    static of<U>(value: U): Monad<U> {return new Monad<U>(value)}

    map<U>(fn: (x: T) => U): Monad<U> {return new Monad<U>(fn(this.value))}

}

 

- 위와 같은 Monad<T> 클래스를 타입 클래스라고 하며, 타입 클래스는 함수를 만들 때 특별한 타입으로 제약하지 않아도 됨.

const callMonad = (fn) => (b) => Monad.of(b).map(fn).value

 

- 위와 같이 모나드 클래스를 구현했을 때 장점은 타입에 따른 안정성 뿐만 아니라 코드 재사용성(code reusability)가 뛰어난 범용 함수를 쉽게 만들 수 있다는 점이다.

 

(2) 고차 타입이란?

- Monad<T>처럼 타입 T를 한 단계 높은 타입으로 변환하는 용도의 타입을 고차 타입(higher-kinded type)’이라고 한다. 하지만 타입스크립트는 스칼라처럼 3차 이상의 고차 타입을 만들 순 없다.

 

(3) 카테고리 이론이란?

- 카테고리 이론(category theory)1940년대에 시작된 수학의 한 분야로, 함수형 프로그래밍 언어의 중요한 이론적 배경이 되었다.

- 수학에서 집합(set)은 프로그래밍에서의 타입으로, 수학에서 카테고리는 집합의 집합이기 때문에 프로그래밍에서 카테고리는 고차 타입이라는 개념으로 이해할 수 있다.

 

(4) 판타지랜드 규격

- 모나드는 카테고리 이론에서 사용되는 용어였지만, 하스켈 언어의 Prelude라는 표준 라이브러리에서 사용되는 용어이기도 하다.

- 모나드는 모나드 룰(Monad raw)’이라고 하는 코드 설계 원칙에 맞춰 구현된 클래스를 의미하며, 다음과 같은 네가지 요소의 조건을 만족하면 그 클래스는 모나드라고 할 수 있다.

(https://github.com/fantasyland/fantasy-land) – 모나드 계층 구조 참조

 

모나드 네가지 요소

펑터(Functor) : map이라는 인스턴스 메서드를 가지는 클래스

어플라이(Apply) : 펑터이면서 ap라는 인스턴스 메서드를 가지는 클래스

애플리커티브(Applicative) : 어플라이이면서 of라는 클래스 메서드를 가지는 클래스

체인(Chain) : 애플리커티브이면서 chain이라는 메서드를 가지는 클래스

(5) 모나드 룰

- 어떤 클래스 이름이 M이고 이 클래스의 인스턴스를 m이라고 할 때 모나드는 앞서 언급한 애플리커티브와 체인의 기능을 가지고 있으며 다음처럼 왼쪽과 오른쪽 법칙을 만족하게 구현한다.

왼쪽 법칙(Left identity) : M.of(a).chain(f) == f(a)

오른쪽 법칙(right identity) : m.chain(M.of) == m

 

11-2) Identity 모나드 이해와 구현

(1) 값 컨테이너 구현용 IValuable<T> 인터페이스 구현

- 일반적으로 컨테이너는 배열을 의미하지만, 배열이 아닌 한 개의 값만 가지는 컨테이너 클래스를 생각해 볼 수 있다. 이 같은 컨테이너 클래스는 구체적인 타입이아닌 모든 타입 T를 가지는 컨테이너로 값 컨테이너(value container)’라고 한다.

값 컨테이너 예

export interface IValuable<T> {

    value() : T

}

 

(2) 클래스 이름이 왜 Identity인가?

- 함수형 프로그래밍에서 identity는 다음과 같은 의미를 갖는 함수

const identity = <T>(value: T): T => value

 

- Identity는 앞서 본 map, ap, of, chain과 같은 기본 메서드만 구현한 모나드로 카테고리 이론에서 자신의 타입에서 다른 타입으로 갔다가 돌아올 때 값이 변경되지 않는 카테고리를 Identity라고 부른다. \

 

(3) 값 컨테이너로서의 Identity<T> 구현하기

_valueprivate하게 구현하고, value 메서드를 public 하게 구현한 예

import {IValuablefrom '../interfaces'

 

export class Identity<Timplements IValuable<T> {

    constructor(private _value: T) {}

    value() {return this._value}

}

 

(4) Isetoid<T> 인터페이스와 구현

- 판타지랜드 규격에서 setoidequals라는 이름의 메서드를 제공하는 인터페이스를 의미하며, 다음과 같이 구현이 가능

import {IValuablefrom './IValuable'

 

export interface ISetoid<Textends IValuable<T> {

    equals<U>(value: U): boolean

}

 

IdentityIsetoid를 구현한 예

import {ISetoidfrom '../interfaces'

 

export class Identity<Timplements ISetoid<T> {

    constructor(private _value: T) {}

    value() {return this._value}

    equals<U>(that: U): boolean {

        if(that instanceof Identity)

            return this.value() == that.value()

        return false

    }

}

 

(5) Ifunctor<T> 인터페이스와 구현

- 판타지랜드 규격에서 펑터(functor)map이라는 메서드를 제공하는 인터페이스로, ‘엔도펑터(endofunctor)’라는 특별한 성질을 만족시켜야 한다.

 

export interface IFunctor<T> {

    map<U>(fn: (x: T) => U)

}

 

(6) 엔도펑터란?

- ‘엔도(endo)’는 단어 앞에 붙는 접두사로, 엔도펑터(endofunctor)는 특정 카테고리에서 출발해도 도착 카테고리는 다시 출발 카테고리가 되게 하는 펑터를 의미한다.

 

(7) Iapply<T> 인터페이스와 구현

- 어플라이(apply)는 자신은 펑터이면서 동시에 ap라는 메서드를 제공하는 인터페이스이다.

import {IFunctorfrom './IFunctor'

 

export interface IApply<Textends IFunctor<T> {

    ap<U>(b: U)

}

 

(8) Iapplicative<T> 인터페이스와 구현

- 애플리커티브(applicative)는 그 자신이 어플라이이면서 of 라는 클래스메서드(정적 메서드)를 추가로 제공하는 인터페이스다.

import {IApplyfrom './IApply'

 

export interface IApplicative<Textends IApply<T> {

    // static of(value: T)

}

 

(9) Ichain<T> 인터페이스와 구현

- 체인(chian)은 그 자신이 어플라이이면서 chian이라는 메서드를 구현하는 인터페이스이다.

 

import {IApplyfrom './IApply'

 

export interface IChain<Textends IApply<T> {

    chain<U>(fn: (T) => U)

}

 

- 체인의 chain 메서드는 펑터의 map과 달리 엔도펑터로 구현해야할 의무가 없으므로 시그너처는 같지만 구현 내용은 조금 다른데, 엔도펑터인 map은 항상 같은 카테고리에 머무르지만, chian은 자신이 머무르고 싶은 카테고리를 스스로 정해야한다.

 

mapchain의 메소드 비교 (결괏값은 동일)

import {Identityfrom '../classes/Identity'

 

console.log(

    Identity.of(1).map(value => `the count is ${value}`).value(),

    Identity.of(1).chain(value => Identity.of(`the count is ${value}`)).value()

)

 

(10) IMonad<T> 인터페이스와 구현

import {IChainfrom './IChain'

import {IApplicativefrom './IApplicative'

 

export interface IMonad<Textends IChain<T>, IApplicative<T> {}

 

 

최종적으로 구현된 모나드 클래스 예

import {ISetoidIMonadfrom '../interfaces'

 

export class Identity<Timplements ISetoid<T>, IMonad<T> {

    constructor(private _value: T) {}

    // IValuable

        value() {return this._value}

 

    //Iapplicative

    static of<T>(value: T): Identity<T> {return new Identity<T>(value)}

 

    // ISetoid

    equals<U>(that: U): boolean {

        if(that instanceof Identity)

            return this.value() == that.value()

        return false

    }

 

    // IFunctor

    map<UV>(fn: (x: T) => U): Identity<U>  {

        return new Identity<U>(fn(this.value()))

    }

 

    // IApply

    ap<U>(b: U) {

        const f = this.value()

        if(f instanceof Function)

            return Identity.of<U>((f as Function)(b))

    }

   

    // IChain

    chain<U>(fn: (T) => U): U {

        return fn(this.value())

    }

    

}

 

 

 

왼쪽 법칙 테스트 예

import {Identityfrom '../classes/Identity'

 

const a = 1

const f = a => a * 2

console.log(

    Identity.of(a).chain(f== f(a// true

)

 

오른쪽 법칙 테스트 예

import {Identityfrom '../classes/Identity'

 

const m = Identity.of(1)

 

console.log(

    m.chain(Identity.of).equals(m)

)

 

  Comments,     Trackbacks
Do it! 타입스크립트 프로그래밍 10장

Ch10 제네릭 프로그래밍

10-1) 제네릭 타입 이해하기

- 제네릭 타입은 인터페이스나 클래스, 함수, 타입 별칭 등에 사용할 수 있는 기능으로, 해당 심벌의 타입을 미리 지정하지 않고 다양한 타입에 대응하려고 할 때 사용한다.

 

//제네릭 인터페이스 구문

Interface IValuable<T> {

  Value: T

}

- 어떤 인터페이스가 value라는 이름의 속성을 가질 때, 속성의 타입을 다음처럼 string, number 등으로 특정하지 않고 T로 지정해 제네릭 타입으로 만들 수 있으며, 인터페이스 이름 뒤에 <T>로 표기한다.

 

(1) 제네릭 사용하기

- 제네릭 인터페이스 정의

export interface IValuable<T> {

    value : T

}

 

- 제네릭 인터페이스 IValuable<T>를 구현하는 제네릭 클래스는 자신이 가진 타입 변수 T를 인터페이스 쪽 제네릭 타입 변수로 넘길 수 있다.

 

import {IValuablefrom './IValuable'

 

export class Valuable<Timplements IValuable<T> {

    constructor(public value: T) {}

}

 

export {IValuable}

 

- IValuable<T>, Valuable<T>를 사용하는 제네릭 함수는 다음처럼 자신의 타입 변수 T를 제네릭 인터페이스의 타입 변수 쪽으로 넘기는 형태로 구현할 수 있다.

import {IValuableValuablefrom './Valuable'

 

export const printValue = <T>(o: IValuable<T>): void => console.log(o.value)

export {IValuableValuable}

 

제네릭함수 사용 예시

import {printValueValuablefrom './printValue'

 

printValue(new Valuable<number>(1))

printValue(new Valuable<boolean>(true))

printValue(new Valuable<string>('hello'))

printValue(new Valuable<number[]>([123]))

 

10-2) 제네릭 타입 제약

- 프로그래밍 언어에서 제네릭 타입 제약(generic type constraint)은 타입 변수에 적용할 수 있는 타입의 범위를 한정하는 기능을 한다.

<최종타입1 extend 타입1, 최종 타입2 extend 타입2>(a: 최종 타입1, b: 최종 타입2, …) {}

 

타입 제약 구문 예시

import {IValuablefrom './IValuable'

 

export const printValueT = <QT extends IValuable<Q>>(o: T) => console.log(o.value)

export {IValuable}

 

- 이때 주의해야할 점은

printValueT <T extnds IValuable<T>>(o: T) 이런식으로 구현하면 안된다.

그 이유는 매개변수 oT입장에서 타입 TIValuable<T>이므로, 타입스크립트는 IValuable <IValuable<T>>로 해석하기 때문이다.

 

(1) new 타입 제약

-  프로그래밍 분야에서 팩토리 함수(factory function)new 연산자를 사용해 객체를 생성하는 기능을 하는 함수를 의미하며, 객체 생성 방법을 단순화하려는 목적을 가진다.

 

- 타입스크립트에서는 타입의 타입을 허용하지 않으므로, 타입스크립트 언어의 창시자이자 C# 언어의 창시자인 아네르스 하일스베르(Anders Hejlsberg)C# 언어에서의 구문을 빌려 타입스크립트 구문으로 만들었다.

 

Const create = <T extends {new(): T}>(type: T): T => new type()

 

중괄호 생략하여 간결하게 표현도 가능하다.

- const create = <T>(type: new() => T): T => new type()

 

결론적으로, {new(): T}new() => T는 같은 의미이므로 new 연산자를 type에 적용하면서 type의 생성자 쪽으로 매개변수를 전달해야 할 때는 new(..args) 구문을 사용한다.

Const create = <T>(type: {new(…args): T}, …args): T => new type(…args)

 

new 타입 제약을 생성하는 예

export const create = <T>(type: {new(...args): T}, ...args): T => new type(...args)

 

(2) 인덱스 타입 제약

- 객체의 일정 속성들만 추려서 좀 더 단순한 객체를 만들어야 할 때는 인덱스 타입 제약을 사용한다.

 

예시

export const pick = (objkeys) => keys.map(key => ({[key]: obj[key]}))

        .reduce((resultvalue) => ({...result...value}), {})

 

import {pickfrom './pick'

 

const obj = {name: 'Jane', age: 22, city: 'Seoul', country: 'Korea'}

console.log(

    pick(obj, ['name''age']), 

    pick(obj, ['nam''agge'])

)

 

- 위 예시처럼 오타가 발생하면 엉뚱한 결과가 나오므로 이를 방지할 목적으로 타입스크립트에서는 keyof T 형태로 타입제약을 설정할 수 있게 지원하며 이를 인덱스 타입 제약(index type constraint)라고 한다.

<T, K extends keyof T>

 

export const pick = <TK extends keyof T>(obj: Tkeys: K[]) =>

 keys.map(key => ({[key]: obj[key]}))

        .reduce((resultvalue) => ({...result...value}), {})

 

10-3) 대수 데이터 타입

- 객체 지향 프로그래밍 언어에서 ADT라는 용어는 추상 데이터 타입(abstract data type)’을 의미하지만, 함수형 언어에서는 대수 데이터 타입(algebraic data type)을 의미한다.

 

- 타입스크립트에서 대수 데이터 타입은 합집합 타입(union type)’교집합 타입(intersection type)’ 두 가지 종류가 있다.

 

(1) 합집합 타입

- ‘또는(or)’의 의미인 ‘|’ 기호로 다양한 타입을 연결해서 만든 타입을 말한다.

type NumberOrString = number | string

let ns: NumberOrString = 1

ns = 'hello'

 

 

(2) 교집합 타입

- ‘이고(and)’의 의미인 ‘&’ 기호로 다양한 타입을 연결해서 만든 타입을 말한다.

- 대표적인 교집합 타입의 예는 두 개의 객체를 통합해서 새로운 객체를 만드는 것이다.

 

두 개의 객체를 결합한 예

export const mergeObjects = <TU>(a: Tb: U): T & U => ({...a...b})

 

import {mergeObjectsfrom './mergeObjects'

 

type INameable = {name: string}

type IAgeable = {age: number}

 

const nameAndAge: INameable & IAgeable = mergeObjects({name: 'Jack'}, {age: 32})

console.log(nameAndAge)

 

 

(3) 합집합 타입 구분하기

Ex) 세 개의 인터페이스와 각각의 인터페이스로 만든 객체를 가정

interface ISquare {size: number}

interface IRectangle {width: numberheight: number}

interface ICircle {radius: number}

 

const square: ISquare = {size: 10}

const rectangle: IRectangle = {width: 4, height: 5}

const circle: ICircle = {radius: 10}

 

위 객체들을 받아 면적(area)을 계산해 주는 calcArea라는 함수를 가정

type IShape = ISquare | IRectangle | ICircle

export const calcArea = (shape: IShape): number => {

    return 0

 

하지만 이 경우 shape 객체가 구체적으로 어떤 타입인지 알 수 없으므로 이를 해결할 수 있도록 합집합의 타입을 각각 구분할 수 있게하는 식별 합집합(discriminated unions)’이라는 구문을 제공한다.

 

(4) 식별 합집합 구문

- 식별 합집합 구문을 사용하려면 합집합 타입을 구성하는 인터페이스들이 모두 똑 같은 이름의 속성을 가지고 있어야 한다. (공통 속성 : tag를 구현)

export interface ISquare {tag: 'square'size: number}

export interface IRectangle {tag: 'rectangle'width: numberheight: number}

export interface ICircle {tag: 'circle'radius: number}

 

export type IShape = ISquare | IRectangle | ICircle

 

- switch문을 사용해 tag 값이 무엇이냐에 따라 적용될 함수를 구분

import {IShapefrom './IShape'

 

export const calcArea = (shape: IShape): number => {

    switch(shape.tag) {

        case 'square' : return shape.size * shape.size

        case 'rectangle' : return shape.width * shape.height

        case 'circle' : return Math.PI * shape.radius * shape.radius

    }

 

}

 

10-4) 타입 가드

- 두 개의 타입을 가정하고 아래와 같이 매개변수 타입을 합집합 타입으로 할 때, 객체가 무엇인지 구체적으로 알기 어렵다는 문제가 발생한다.

export class Bird {fly() {console.log(`I'm flying.`)}}

export class Fish {swim() {console.log(`I'm swimming`)}}

 

(1) instanceof 연산자

- 자바스크립트는 instanceof라는 연산자를 제공하며, 이 연산자는 두개의 피연산자가 필요하다.

객체 instanceof 타입 // boolean 타입의 값 반환

 

instanceof를 사용해 구현한 예

import {BirdFishfrom './BirdAndFish'

 

export const flyOrSwim = (o: Bird | Fish): void => {

    if(o instanceof Bird) {

        (o as Bird).fly() // 혹은 (<Bird>o).fly()

    } else if(o instanceof Fish) {

        (<Fish>o).swim() // 혹은 (o as Fish).swim()

    }

}

 

(2) 타입 가드

- 타입스크립트에서 instanceof 연산자는 자바스크립트와는 다르게 타입 가드(type guard)’ 기능이 있는데, 타입 가드는 타입을 변환하지 않은 코드 때문에 프로그램이 비정상으로 종료되는 상황을 보호해 준다는 의미이다.

 

export const flyOrSwim = (o: Bird | Fish): void => {

    if(o instanceof Bird) {

        o.fly() 

    } else if(o instanceof Fish) {

        o.swim() 

    }

}

 

 

(3) is 연산자를 활용한 사용자 정의 타입 가드 함수 제작

- 개발자 코드에서 instanceof처럼 타입 가드 기능을 하는 함수를 구현할 수 있는데, 이 때 함수의 반환 타입 부분에 is라는 이름의 연산자를 사용해야 한다.

 

사용자 정의 타입 가드 함수 예

import {BirdFishfrom './BirdAndFish'

 

export const isFlyable = (o: Bird | Fish): o is Bird => {

    return o instanceof Bird

}

 

 

import {BirdFishfrom './BirdAndFish'

 

export const isSwimmable = (o: Bird | Fish): o is Fish => {

    return o instanceof Fish

}

 

 

- 단 사용자 정의 타입 가드 함수는 if문에서 사용해야한다.

사용자 정의 타입 가드함수 사용 예

import {BirdFishfrom './BirdAndFish'

import {isFlyablefrom './isFlyable'

import {isSwimmablefrom './isSwimmable'

 

export const swimOrFly = (o: Fish | Bird) => {

    if(isSwimmable(o))

        o.swim()

    else if(isFlyable(o))

        o.fly()

}

 

import {BirdFishfrom './BirdAndFish'

import {swimOrFlyfrom './swimOrFly'

 

[new Birdnew Fish].forEach(swimOrFly)

 

10-5) F-바운드 다형성

(1)this 타입과 F-바운드 다형성

- 타입스크립트에서 this 키워드는 타입으로도 사용됨. This가 타입으로 사용되면 객체지향 언어에서 의미하는 다형성(polymorphism) 효과가 나는데, 일반적으로 다형성과 구분하기 위해 this 타입으로 인한 다형성을 ‘F-바운드 다형성(F-bound polymorphism)이라고 한다.

F-바운드 타입

- 자신을 구현하거나, 상속하는 서브타입(subtype)을 포함하는 타입을 말한다.

export interface IValueProvider<T> {

    value(): T

}

- IvalueProvider 인터페이스는 자신을 상속하는 타입이 포함되어있지 않은 일반 타입

 

export interface IAddable<T> {

    add(value: T): this

}

- Iaddable<T> add 메서드가 내가 아닌 나를 상속하는 타입을 반환하는 F-바운드 타입

 

export interface IMultiplyable<T> {

    multiply(value: T): this

}

- Imultiplyable 역시 반환 타입이 this 이므로 F-바운드 타입

 

IvalueProvider<T> 인터페이스의 구현

- IvalueProvider<T> 인터페이스를 구현하는 Class Calculator를 만들어, _value 속성을 private로 만든 후, _value속성이 아닌 value() 메서드로 접근할 수 있게 설계함.

 

import {IValueProviderfrom '../interfaces'

 

export class Calculator implements IValueProvider<number> {

    constructor(private _value: number = 0) {}

    value(): number {return this._value}

}

 

- 같은 방식으로 StringComposer란 클래스로 IvalueProvider<T>를 구현

 

import {IValueProviderfrom '../interfaces'

 

export class StringComposer implements IValueProvider<string> {

    constructor(private _value: string = '') {}

    value(): string {

        return this._value

    }

}

 

Iaddable<T>Imultiplyable<T> 인터페이스 구현

- Calculatoradd 메서드는 클래스의 this 값을 반환하며, 이는 메서드 체인(method chain)을 구현하기 위함이며, multiply도 마찬가지임

import {IValueProviderIAddableIMultiplyablefrom '../interfaces'

 

export class Calculator implements IValueProvider<number>, IAddable<number>, IMultiplyable<number> {

    constructor(private _value: number = 0) {}

    value(): number {return this._value}

    add(value: number): this {

        this._value = this._value + value

        return this

    }

    multiply(value: number): this {

        this._value = this._value * valuereturn this

    }

}

 

※ 테스트

import {Calculatorfrom '../classes/Calculator'

 

const value = (new Calculator(1))

            .add(2)

            .add(3)

            .multiply(4)

            .value()

console.log(value)

 

- StringComposer도 다음과 같이 구현할 수 있으며, Iaddable<T>, Imultiplyable<T>의 각각의 메소드들은 클래스를 어떻게 구현하느냐에 따라 반환 타입이 Calculator 방식 혹은 StringComposer 방식이 되기도 한다.

import {IValueProviderIAddableIMultiplyablefrom '../interfaces'

 

export class StringComposer implements IValueProvider<string>,  IValueProvider<string>, IAddable<string>{

    constructor(private _value: string = '') {}

    value(): string {

        return this._value

    }

    add(value: string): this {this._value = this._value.concat(value); return this}

    multiply(repeat: number): this {

        const value = this.value()

        for(let index=0index < repeatindex++)

            this.add(value)

        return this

    }

}

 

10-6) nullable 타입과 프로그램 안전성

(1) nullable 타입이란?

- 자바스크립트와 타입스크립트는 변수가 초기화되지 않으면 undefined라는 값을 기본으로 지정하는데, 사실상 같은 의미인 null이 존재한다.

- 타입스크립트에서 undefined값의 타입은 undefined이고, null값의 타입은 null이며 둘은 사실상 같은 것이므로 서로 호환이 된다.

- undefinednull 타입을 nullable타입이라고 하며, 코드로는 다음처럼 표현할 수 있다.

export type nullable = undefined | null

export const nullable: nullable = undefined

 

- nullable 타입들은 프로그램을 비정상으로 종료시키는 주요 원인이 되므로, 함수형 언어들은 이를 방지하기 위해 연산자나 클래스를 제공함.

 

(2) 옵션 체이닝 연산자

- 변수가 선언만 되고 어떤 값으로 초기화되지 않으면 런타임시 오류가 발생해 프로그램이 비정상적으로 종료되는데, 이를 방지하기 위해 옵션 체이닝연산자를 사용한다.

- 자바스크립트는 최근에 물음표 기호와 점 기호를 연이어 쓰는 ?. 연산자를 표준으로 채택하고 타입 스크립트는 버전 3.7.2 부터 이 연산자를 지원한다.

 

옵션 체이닝 연산자 사용 예

export type ICoordinates = {longitude: number}

export type ILocation = {country: stringcoords?: ICoordinates}

export type IPerson = {name: stringlocation?: ILocation}

 

let person: IPerson = {name: 'Jack'}

let longitude = person?.location?.coords?.longitude // safe navigation

console.log(longitude)

if(person && person.location && person.location.coords) {

    longitude = person.location.coords.longitude

}

 

(3) 널 병합 연산자

- 자바스크립트는 옵션 체이징 연산자를 표준으로 채택하면서 동시에 물음표 두 개를 연달아 이어 붙인 ?? ‘널 병합 연산자(nullish coalescing operator)’도 표준으로 채택했다.

- 옵션 체이닝 연산자 부분이 undefined가 되면 널 병합 연산자가 동작해 undefined 대신 0을 반환한다.

export type ICoordinates = {longitude: number}

export type ILocation = {country: stringcoords: ICoordinates}

export type IPerson = {name: stringlocation: ILocation}

 

let person: IPerson

 

// 병합 연산자를 사용해 기본값 0 설정

let longitude = person?.location?.coords?.longitude ?? 0

console.log(longitude)

 

 

  Comments,     Trackbacks
Do it! 타입스크립트 프로그래밍 9장

Chap09 람다 라이브러리

9-1) 람다 라이브러리 소개

- ramda 패키지는 composepipe를 사용하는 함수 조합을 쉽게 할 수 있게 설계된 오픈소스 자바스크립트 라이브러리이다.

특징

- 타입스크립트 언어와 100% 호환

- composepipe 함수 제공

- 자동 커리(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(19 + 1

)

 

(2) R.tap 디버깅용 함수

- R.tap 함수는 2차 고차 함수 형태로 현재 값을 파악할 수 있게 gowa

R.tap(콜백 함수)(배열)

 

R.range 함수로 생성한 배열의 내용을 R.tap 함수를 사용해 화면에 출력하는 예

import * as R from 'ramda'

 

const numbers: number[] = R.range(19+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(19+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))

)(arrayas T[]

 

(5) 자동 커리 이해하기

- 람다 라이브러리의 함수들은 매개변수가 두 개인 일반함수처럼 사용할 수도 있고, 05행처럼 2차 고차 함수로 사용할 수도 있는데 이를 자동 커리(auto curry)라고 한다.

import * as R from 'ramda'

 

console.log(

    R.add(12),

    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 {sumfrom './sum'

 

export const curriedSum = R.curryN(4sum

//N개의 매개변수를 가진 1 함수를 N개의 커리 매개변수를 가지는 N 고차함수로 만들어줌

 

- 만약 위 식처럼 4차 고차 함수의 매개변수를 만들 때 매개변수 개수를 충족하지 못하면 모두 부분 함수 이므로 [Function]을 결괏값으로 출력한다.

 

(7) 순수 함수

- 람다 라이브러리는 순수 함수(pure function)을 고려해 설계되었으므로 항상 입력 변수의 상태를 변화시키지 않고 새로운 값을 반환함.

 

람다라이브러리의 순수 함수 예

import * as R from 'ramda'

 

const originalArray : number[] = [123]

const resultArray = R.pipe(

    R.map(R.add(1))

)(originalArray)

 

console.log(originalArrayresultArray)

 

 

9-3) 배열에 담긴 수 다루기

(1) 선언형 프로그래밍

- 보통 함수형 프로그래밍은 선언형 프로그래밍(declarative programming) 방식으로 코드를 작성하며 선언형 프로그래밍에서 모든 입력 데이터는 단순 데이터보다 배열 형태를 주로 사용

 

import * as R from 'ramda'

 

const numbers : number[] = R.range(19+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.incR.add(1)과 같은 함수며, ‘포인트가 있는함수 형태로 R.add를 사용해 inc 만든 예는 다음과 같다.

const inc = (b:number): number => R.add(1)(b)

 

- 포인트가 없는 함수로 구현하면 다음과 같다.

const inc = R.add(1)

 

- incR.map 함수에 포인트가 있는형태로 사용하면 다음과 같다.

R.map((n: number) => R.inc(n))

 

- R.map(콜백함수)의 콜백 함수를 익명 함수로 구현한 것인데, 현재 inc는 그 자체가 콜백 함수로 사용될 수 있다. 따라서 앞 코드는 다음처럼 간결하게 표현할 수 있다.

R.map(inc)

 

(3) R.addIndex 함수

- Array.map은 두번째 매개변수로 index를 제공하지만, R.mapArray.map과는 다르게 index 매개변수를 기본으로 제공하지 않는다. 따라서 R.mapArray.map처럼 동작하려면 다음처럼 R.addIndex 함수를 사용해 R.mapindex를 제공하는 새로운 함수를 만들어야 한다.

const indexedMap = R.addIndex(R.map)

 

indexMap((value: numberindex: 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(19 + 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))

)(Rrange(19+1))

 

 

- 람다는 R.flip이라는 함수를 제공하며, R.flipR.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(19+1))

 

(5) 사칙 연산 함수들의 조합

수학 공식 f(X) = ax^2 + bx + c 을 타입스크립트로 구현한 예

type NumberToNumberFunc = (number) => number

export const f = (a:numberb: numberc: 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: numberb: numberc: 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 {fexpsquarefrom './f-using-ramda'

 

export const quadratic = f(121)

export {expsquare// exp square 다시 export 한다.

 

 

quadratic 함수를 활용해 1~10까지 변수를 변수 x에 대입한 결과값 구하기

import * as R from 'ramda'

import {quadraticfrom './quadratic'

 

const input: number[] = R.range(110+1)

const quadraticResult = R.pipe(

    R.map(quadratic),

    R.tap(a => console.log(a))

)(input)

 

 

9-4) 서술자와 조건 연산

- Array.filter 함수에서 사용되는 콜백 함수는 boolean 타입 값을 반환해야 하는데, 함수형 프로그래밍에서 boolean 타입 값을 반환해 어떤 조건을 만족하는지를 판단하는 함수를 서술자(predicate’)라고 한다.

 

(1) 수의 크기를 판단하는 서술자

- 람다는 수를 비교해 truefalse를 반환하는 다음의 서술자들을 제공한다.

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(110 + 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(110+1))

 

(2) R.allPass 로직 함수

- R.lt, R.gt 같은 boolean 타입 값을 반환하는 함수들은 R.allPassR.anyPass라는 로직함수를 통해 결합할 수 있다.

R.allPass(서술자 배열// 배열의 조건을 모두 만족하면 true

R.anyPass(서술자 배열// 배열의 조건을 하나라도 만족하면 true

 

R.allPass 함수를 사용한 예

import * as R from 'ramda'

 

type NumberToBooleanFunc = (n: number) => boolean

export const selectRange = (min: numbermax: number): NumberToBooleanFunc => 

    R.allPass([

        R.lte(min),

        R.gt(max)

    ])

 

(3) R.not 함수

- true이면 false, false이면 true를 반환하는 함수이다. 이미 구현한 함수들을 조합하는 것으로 다음과 같은 형태로 구현이 가능하다.

import * as R from 'ramda'

import {selectRangefrom './selectRange'

export const notRange = (min:numbermax:number) => R.pipe(selectRange(minmax),

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(110 + 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 {ICoordinatesfrom './ICoordinates'

 

export const makeICoordinates = (latitude: numberlongitude: number):

    ICoordinates => ({latitudelongitude})

 

Src/model/coordinates/makeRandomCoordinates.ts

import {ICoordinatesfrom './ICoordinates'

import {makeICoordinatesfrom './makeICoordinates'

import Chance from 'chance'

const c = new Chance

 

export const makeRandomICoordinates = (): ICoordinates =>

    makeICoordinates(c.latitude(), c.longitude())

 

src/index.ts

import {ICoordinatesfrom './ICoordinates'

import {makeICoordinatesfrom './makeICoordinates'

import {makeRandomICoordinatesfrom './makeRandomIcoordinates'

 

// ICoordinates makeIcoordinates, makeRandomICoordinate re-export한다

export {ICoordinatesmakeICoordinatesmakeRandomICoordinates}

import문에서 index.ts 파일은 생략이 가능하다.

- 경로 추적시 해당 디렉터리에 index.ts 파일이 있고 경로의 마지막이 디렉터리 이름이면, 타입스크립트 컴파일러는 디렉터리 밑의 index.ts 파일로 해석한다.

 

(2) Ilocation 타입 객체 만들기

- Icoordinates 타입 속성을 포함하는 Ilocation 타입 구성하기

 

Src/model/location/Ilocation.ts

import {ICoordinatesfrom '../coordinates'

 

export type ILocation = {

    country: string

    city?: string

    address?: string

    coordinates?: ICoordinates

}

 

Src/model/location/makeILocation.ts

import {ILocationfrom './ILocation'

import {ICoordinatesmakeICoordinatesfrom '../coordinates'

 

export const makeILocation = (

    country: string,

    city: string,

    address: string,

    coordinates: ICoordinates

): ILocation =>({countrycityaddresscoordinates})

 

Src/model/location/makeRandomILocation.ts

import {ILocationfrom './ILocation'

import {makeILocationfrom './makeILocation'

import {makeRandomICoordinatesfrom '../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 {ILocationfrom './ILocation'

import {makeILocationfrom './makeILocation'

import {makeRandomILocationfrom './makeRandomILocation'

 

export {ILocationmakeILocationmakeRandomILocation}

 

src/location-test.ts

import {makeRandomILocationILocationfrom './model/location'

 

const location: ILocation = makeRandomILocation()

console.log(location)

 

(3) Iperson 타입 객체 만들기

Src/model/person/Iperson.ts

import {ILocationfrom '../location'

 

export type IPerson = {

    name: string

    age: number

    title?: string

    location?: ILocation

}

 

export {ILocation}

 

src/model/person/makeIPerson.ts

import {IPersonILocationfrom './IPerson'

 

export const makeIPerson = (

    name: string,

    age: number,

    title?: string,

    location?: ILocation

=> ({nameagetitlelocation})

 

export {IPersonILocation}

 

src/model/person/makeRandomIPerson.ts

import {IPersonmakeIPersonfrom './makeIPerson'

import {makeRandomILocationfrom '../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 {IPersonmakeIPersonfrom './makeIPerson'

import {makeRandomIPersonfrom './makeRandomIPerson'

 

export {IPersonmakeIPersonmakeRandomIPerson}

 

src/person-test.ts

import {IPersonmakeRandomIPersonfrom './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.propR.assoc 함수

- R.prop‘property’의 앞 네 글자를 따서 만든 이름으로, 객체의 특정 속성값을 가져오는 함수이다. 이런 동작을 하는 함수를 게터(getter)라고 한다.

 

import * as R from 'ramda'

import {IPersonmakeRandomIPersonfrom'./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 {IPersonmakeRandomIPersonfrom './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(lensnewValue)

export const setterUsingFunc = (lens) => <TR>(func: (T) => R) => R.over(lensfunc)

 

(5) R.lensPath 함수

- 람다 라이브러이에서는 객체의 중첩 속성(nested property)경로(path)’라고 하며, longitude처럼 긴 경로의 속성을 렌즈로 만들려면 R.lensPath 함수를 사용

 

렌즈 = R.lensPath([‘location’, ‘coordinates’, ‘longitude’])

lents path를 사용해 중첩속성 longitude를 갖고와서 활용한 예

import * as R from 'ramda'

import {gettersettersetterUsingFuncfrom './lens'

import {IPersonmakeRandomIPersonfrom './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(

    longitudegetLongitude(newPerson), getLongitude(anotherPerson)

)

 

9-8) 객체 다루기

(1) R.toPairsR.fromPairs 함수

- R.toPairs 함수는 객체의 속성들을 분해해 배열로 만들어준다. 이때 배열의 각 아이템은 [string, any]타입의 튜플이다.

R.toPairs 함수 사용 예

import * as R from 'ramda'

import {IPersonmakeRandomIPersonfrom './model/person'

 

const person: IPerson = makeRandomIPerson()

const pairs: [stringany][] = R.toPairs(person)

console.log('pairs'pairs)

 

 

- R.fromPairs 함수는 [:] 형태의 아이템을 가진 배열을 다시 객체로 만들어 준다.

R.fromPairs 함수 사용 예

import * as R from 'ramda'

import {IPersonmakeRandomIPersonfrom './model/person'

 

const pairs :[stringany][] = R.toPairs(makeRandomIPerson())

const person: IPerson = R.fromPairs(pairsas IPerson

console.log('person'person)

 

 

(2) R.keysR.values 함수

- R.keys 함수는 객체의 속성 이름만 추려서 string[] 타입 배열로 반환한다.

R.keys 함수 사용 예

import * as R from 'ramda'

import {makeRandomIPersonfrom './model/person'

 

const keys: string[] = R.keys(makeRandomIPerson())

console.log('keys'keys)

 

R.values 함수 사용 예

import * as R from 'ramda'

import {makeRandomIPersonfrom './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 {IPersonmakeRandomIPersonfrom './model/person'

 

const originalperson: IPerson = makeRandomIPerson()

const keys: string[] = R.keys(originalperson)

const values: any[] = R.values(originalperson)

const zippedPerson: IPerson = R.zipObj(keysvaluesas IPerson

console.log('originalPerson'originalperson'zippedPerson:'zippedPerson)

 

 

(4) R.mergeLeftR.mergeRight 함수

- R.mergeLeftR.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(leftright)

console.log(person)

 

mergeRight 함수의 사용예

import * as R from 'ramda'

 

const left = {name:'Jack'}, right = {name: 'Jane', age: 32}

const person = R.mergeRight(leftright)

console.log(person)

 

(5) R.mergeDeepLeftR.mergeDeepRight 함수

- mergeLeftmergeRight 함수는 객체의 속성에 담긴 객체를 바꾸지는 못한다. 즉 속성 안의 객체들의 값을 바꿔주진 못한다.

하지만, mergeDeepLeftmergeDeepRight는 경로(path)의 속성값들도 바꿀수 있다.

 

mergeDeepRight 함수 사용 예

import * as R from 'ramda'

import {IPersonmakeRandomIPersonfrom './model/person'

import {ILocationmakeRandomILocationfrom './model/location'

import {ICoordinatesmakeRandomICoordinatesfrom './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[] = [34]

const newArray = R.prepend(1)(array)

console.log(arraynewArray)

 

R.append 함수는 배열의 맨 뒤에 아이템을 삽입한다.

R.append 함수 사용 예

import * as R from 'ramda'

 

const array: number[] = [34]

const newArray = R.append(1)(array)

console.log(arraynewArray)

 

 

(2) R.flatten 함수

- 복잡합 배열을 1차원의 평평한 배열로 바꿔줌

flatten함수 사용 예

import * as R from 'ramda'

 

const array = R.range(12+1).map((x: number) => {

    return R.range(12 + 1).map((y: number) => {

        return [xy]

    })

})

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(12 + 1).map((x: number) => {

    return R.range(12 + 1).map((y: number) => {

        return [xy]

    })

})

console.log(array)

 

const unnestedArray = R.unnest(array)

console.log(unnestedArray)

 

const twoUnnestedArray = R.pipe(R.unnestR.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(15 + 1).map(makeRandomNumber(100))

const sortedArray = R.sort(a:numberb: number): number => a - b)(array)

 

console.log(arraysortedArray)

 

(5) R.sortBy 함수

- 배열에 담긴 아이템이 객체일때, 특정 속성값에 따라 정렬해야하므로 이때 사용하는 함수가 sortBy이다.

정렬된 배열 = R.sortBy(객체 속성을 얻는 함수)(배열)

 

sortBy함수 사용 예

import * as R from 'ramda'

import {IPersonmakeRandomIPersonfrom './model/person'

import {displayPersonsfrom './displayPersons'

 

const persons: IPerson[] = R.range(14 + 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 {IPersonfrom './model/person'

 

export const displayPersons = (prefix: string) => R.pipe(

    R.map((person: IPerson) => ({name:person.name, age: person.age})),

    R.tap(o => console.log(prefixo))

as any

 

(6) R.sortWith 함수

- sortBy 함수는 오름차순, 내림차순 정렬을 하지 못하고 항상 오름차순으로만 정렬하지만, R.sortWith 함수는 R.ascend, R.descend 함수와 함꼐 사용되어 오름차순, 내림차순 정렬을 할 수 있다.

 

R.sortWith, R.descend 사용 예

import * as R from 'ramda'

import {IPersonmakeRandomIPersonfrom './model/person'

import {displayPersonsfrom './displayPersons'

 

const persons: IPerson[] = R.range(14 + 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 = [123]

 

R.pipe(

    R.chain(n => [nn]),

    R.tap(n => console.log(n))

)(array)

 

R.pipe(

    R.chain(R.appendR.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 = (firstFnsecondFn) => (x) => firstFn(secondFn(x), x)

 

(3) R.flip 조합자

- R.flip2차 고차 함수의 매개변수 순서를 서로 바꿔준느 역할을 함

 

(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 = [123]

const result = callAndAppend(input

 

 

  Comments,     Trackbacks
Do it! 타입스크립트 프로그래밍 8장

Chap08 함수 조합의 원리와 응용

8-1) 함수형 프로그래밍이란?

- 함수형 프로그래밍은 순수 함수와 선언형 프로그래밍의 토대 위에 함수 조합(function composition)과 모나드 조합(monadic composition)으로 코드를 설계하고 구현하는 기법이다.

함수형 프로그래밍은 다음 세 가지 수학 이론에 기반을 두고 있다.

1. 람다 수학(ramda calculus): 조합 논리와 카테고리 이론의 토대가 되는 논리 수학

2. 조합 논리(combinatory logic): 함수 조합의 이론적 배경

3. 카테고리 이론(category theory): 모나드 조합과 고차 타입의 이론적 배경

 

- 함수형 프로그래밍 언어는 한때 인공지능의 언어로 불렸던 LISP에 기반을 두고 있다. LISP는 메타 언어(meta language)로 진화되었고, 메타언어는 다시 하스켈(Haskell) 언어로 발전되었음

 

- 하스켈 언어는 스칼라 언어에 의해 C언어 와 비슷한 구문을 갖게 되었으며, 타입스크립트는 스칼라 언어 구문을 자바스크립트 친화적으로 발전했음

 

- 타입스크립트는 함수형 언어에서 중요한 패턴 매칭과 고차 타입이라는 기능을 생략해 구문을 쉽게 만듦

 

8-2) 제네릭 함수

(1) 타입스크립트의 제네릭 함수 구문

- 타입스크립트에서 제네릭 타입은 함수와 인터페이스, 클래스, 타입 별칭에 적용할 수 있으며, 꺾쇠 괄호 <> 타입을 감싼 <T>, <T, Q>처럼 표현함.

Function g1<T>(a: T): void {}

Function g2<T, Q>(a: T, b: Q): void {}

 

- 제네릭 타입으로 함수를 정의하면 어떤 타입에도 대응이 가능.

화살표 함수에 제네릭 타입을 적용한 예

Const g3 = <T>(a: T): void => {}

Const g4 = <T, Q>(a: T, b: Q): void => {}

 

타입 별칭(type-alias)에 제네릭 타입을 적용한 예

type Type1Func<T> = (T) => void

type Type2Func<T, Q> = (T, Q) => void

type Type3Func<T, Q, R> = (T, Q) => R

 

(2) 함수의 역할

- 수학에서 함수는 값 x에 수식을 적용해 또 다른 y 값을 만드는 역할을 하며, 함수를 f라 표기하면 다음과 같이 관계를 표현할 수 있음

x ~> f ~> y

 

- 프로그래밍 언어로 수학 함수를 구현할 때는 변수 xy의 타입을 고려해야함.

(x: T) ~-> f -> (y: R)

 

- 수학에서는 이런 관계를 일대일 관계(one-to-one relationship)라고 하고, 이런 동작을 하는 함수 f를 매핑(mapping) 줄여서 맵(map)이라고 표현함.  타입스크립트로 일대일 맵 함수를 만든다면 타입 T인 값을 이용해 타입 R인 값을 만들어 주어야 하므로, 다음과 같이 표현 가능함

Type MapfFunc<T, R> = (T) => R

 

(3) 아이덴티티 함수

- 맵 함수의 가장 단순한 형태는 입력값(x)을 그대로 반환하는 것으로, 입력과 출력 타입이 같은 형태

 

- 함수형 프로그래밍에서는 identitiy 또는 I라는 단어가 포함됨.

Identity 함수의 예

Const numberIdentity: IdentitiyFunc<number> = (x: number): number => x

Const stringIdentity: IdentityFunc<string> = (x: string): string => x

Const objectIdentity: IdentityFunc<object> = <x: object): object => x

Const arrayIdentity: IdentityFunc<any[]> = (x: any[]): any[] => x

 

8-3) 고차 함수와 커리

- 함수에서 매개변수의 개수를 애리티(arity)라고 한다. F()은 애리티가 0인 함수, f(X)는 애리티가 1인 함수, f(x, y)는 애리티가 2인 함수임

 

- 만약 함수 f, g, h의 애리티가 모두 1이라면 다음과 같이 연결해서 사용가능

x ~> f ~> g ~> h ~> y

 

- 이를 프로그래밍 언어로 표현하면 다음과 같음

y = h(g(f(x)))

 

- 함수형 프로그래밍에서는 composepipe라는 이름의 함수를 사용해 compose(h, g, f) 또는 pipe(f, g, h)형태로 f, g, h 함수들을 조합해 새로운 함수를 만들 수 있음

 

 

(1) 고차 함수란?

- 어떤 함수가 또 다른 함수를 반환할 때 그 함수를 고차 함수(high-orderfunction)이라고 함

 

- 단순히 값을 반환하는 함수를 1차 함수라고 하면, 1차 함수를 반환하면 2차 고차함수, 2차 함수를 반환하면 3차 고차 함수라고 표현함

 

1차 함수의 예

import {FirstOrderFuncfrom './function-signature'

 

export const inc: FirstOrderFunc<numbernumber= (x: number): number => x + 1



 

2차 고차 함수의 예

import {FirstOrderFuncSecondOrderFuncfrom './function-signature'

 

export const add: SecondOrderFunc<numbernumber=

    (x: number): FirstOrderFunc<numbernumber=>

    (y: number): number => x + y

 

2차 고차 함수의 사용 예

import {addfrom './second-order-func'

console.log(

    add(1)(2)

)

 

결괏값: 3

 

- 2차 고차 함수를 호출할때는 add(1)(2) 처럼 호출 연산자를 두번 연속해서 사용하며 이를 함수 프로그래밍 언어에서는 커리(curry)라고 함.

 

3차 고차 함수의 예

import {FirstOrderFuncSecondOrderFuncThirdOrderFuncfrom './function-signature'

 

export const add3: ThirdOrderFunc<numbernumber=

    (x: number): SecondOrderFunc<numbernumber=>

    (y: number): FirstOrderFunc<numbernumber=>

    (z: number): number => x + y + z

 

(2) 부분 적용 함수와 커리

- 고차 함수들은 자신의 차수만큼 함수 호출 연산자를 연달아 사용함 (add(1)(2) )

 

- 이때, 자신의 차수보다 함수 호출 연산자를 덜 사용하면 부분 적용 함수(partially applied function), 짧게 말하면 부분 함수(partial function)라고함

 

부분함수 적용 예

import {FirstOrderFuncSecondOrderFuncfrom './function-signature'

import {addfrom './second-order-func'

 

const add1: FirstOrderFunc<numbernumber= add(1)

console.log(

    add1(2),

    add(1)(2)

)

 

- add11차 함수 이므로 add1(2) 처럼 함수 호출 연산자를 1개 호출해 일반 함수처럼 호출할 수 있으며, add1add(1)과 같으므로 add1 add(1)로 대체하면 add(1)(2)가 됨.

(3) 클로저

- 고차 함수의 몸통에서 선언되는 변수들은 클로저(closure)라는 유효범위를 가지며, 클로저는 지속되는 유효 범위(persistence scope)’를 의미

 

클로저를 사용한 함수 예

Function add(x: number): (number) => number {

   Return function(y: number): number {

             Return x + y

   }

}

 

- 위 소스에서 add가 반환하는 함수의 내부 범위(inner scope)만 놓고 볼 때 x는 이해할 수 없는 변수이며 범위 안에서 그 의미를 알 수 없는 변수를 자유 변수(free variable)’라고 함

 

- 클로저를 지속하는 유효 볌위라고 하는 이유는 다음처럼 add 함수를 호출하더라도 변수 x가 메모리에서 해제되지 않음.

Const add1 = add(1) // 변수 X 메모리 유지

 

Const result = add1(2) // result3을 저장한 후 변수 X 메모리 해제

- 이처럼 고차 함수가 부분 함수가 아닌 을 발생해야 비로소 자유 변수의 메모리가 해제되는 유효 범위를 클로저라고 함.

 

- 클로저는 메모리가 해제되지 않고 프로그램이 끝날 때까지 지속될 수도 있다.

 

8-4) 함수 조합

- 함수 조합(function composition)은 작은 기능을 구현한 함수를 여러 번 조합해 더 의미 있는 함수를 만들어 내는 프로그램 기반 설계 기법이며, compose 또는 pipe라는 이름의 함수를 제공하거나 만들 수 있다.

 

(1) compose 함수

- compose 함수는 가변 인수 스타일로 함수들의 배열을 입력받으며, 매개변수 x를 입력받는 1차 함수를 반환함.

export const compose = <TR>(...functions: readonly Function[]): Function =>

(x: T): (T) => R => {

    const deepCopiedFunctions = [...functions]

    return deepCopiedFunctions.reverse().reduce((valuefunc) => func(value), x)

}

 

 

compose 함수와 에리티 1인 함수 f, g, h함수의 조합

import {fghfrom './f-g-h'

import {composefrom './compose'

 

const composedFGH = compose(hgf)

console.log(

    composedFGH('x')

)

 

결괏값 : h(g(f(x)))

 

(2) pipe 함수

- pipe 함수는 compose와 매개변수들을 해석하는 순서가 반대이므로, compose와는 달리 functionsreverse 하는 코드가 없음

export const pipe = <TR>(...functions: readonly Function[]): Function => (x:T):

(T) => R => {

    return functions.reduce((valuefunc) => func(value), x)

}

 

pipe 함수 실행 예

import {fghfrom './f-g-h'

import {pipefrom './pipe'

 

const piped = pipe(fgh)

console.log(

    piped('x')

)

 

(3) pipe compose 함수 분석

- 먼저 pipe함수는 pipe(f), pipe(f, g), pipe(f, g, h)처럼 가변 인수 방식으로 동작하므로 매개변수를 다음처럼 설정

Export const pipe = (…functions)

 

- 가변인수 functions는 인자마다 함수 타입이 다를 수 있기 때문에 자바스크립트 타입 Function들의 배열인 Function[]으로 설정함.

Export const pipe = (…functions: Function[])

 

- pipe 함수는 functions 배열을 조합해 어떤 함수를 반환해야 하므로 반환 타입은 Function으로 설정

Expoert const pipe = (…functions: Function[]): Function

 

- pipe로 조합된 결과 함수는 애리티가 1이므로, 매개변수 x를 입력받는 함수를 작성하되, 제네릭 타입으로 표현하면 타입 T의 값 x를 입력받아 (T) => R타입의 함수를 반환하는 것이 됨

Export const pipe = <T, R>(…functions: Function[]): Function => (x: T) => (T) => R

 

- 몸통에서는 배열이 제공하는 reducㄷ 메서드를 사용해 변수 x reduce 메서드의 초깃값으로 설정함

Export const pipe = <T, R>(…functions: Function[]): Function => (x: T) => (T) => R => {

Return functions.reduce(<함수>, x)

}

 

- <함수> 부분은 (value, func) 형태의 매개변수 구조를 가져야 하는데, reduce 메서드의 두번째 매개변수(x)는 항상 배열의 아이템이기 때문

Export const pipe=<T, R>(…functions: Function[]): Function =>(x: T) => (T) => R => {

Return functions.reduce((value, func) => func(value), x)

}

 

- 위 코드를 기준으로 functions의 내용이 [f, g, h]라고 가정했으므로 reduce ptjem의 진행 순서별 매개변수값의 변화는 다음과 같음

순서

Value

Func

결괏값

1

x

f

f(x)

2

f(x)

g

g(f(x))

3

g(f(x))

h

h(g(f(x)))

 

- functions 배열의 마지막 아이템인 hreduce메서드의 func 매개변수에 입력되면 최종결괏값은 h(g(f(x)))가 되고 해당 값을 반환함

 

- compose 함수는 pipe 함수와 매개변수 방향이 반대이므로, pipe(f, g, h)compose(h, g, f)와 같음

 

- 일반적으로 생각했을떄 functions.reverse()만 호출하면 나머지는 pipe와 똑같이 작성하면 될 것 같지만, compose는 순수 함수의 모습으로 동작해야한다. 따라서 functions을 전개 연산자로 전개하고 그 내용을 깊은 복사를 하는 변수(deepCopiedFunctions)를 만듦

Export const pipe=<T, R>(…functions: Function[]): Function =>(x: T) => (T) => R => {

Const deepCopiedFunctions = […functions]

deepCopiedFunctions.reverse().reduce((value, func) => func(value), x)

}

 

(4) 부분함수와 함수 조합

- 고차 함수의 부분 함수는 함수 조합에 사용될 수 있음

import {pipefrom './pipe'

 

const add = x => y => x + y

const inc = add(1)

 

const add3 = pipe(

    inc,

    add(2)

)

 

console.log(

    add3(1)

)

 

- 위 소스에서 add2차 고차 함수이므로 inc함수는 add의 부분함수가 되며, add3 pipe함수를 가지고 incadd(2) 두 부분 함수를 조합해 만든 함수임

 

(5) 포인트가 없는 함수

- map(f) 형태의 부분 함수를 만들면 composepipe에서 사용할 수 있으며, 이처럼 함수를 조합을 고려한 함수를 포인트가 없는 함수(pointless function)’라고 함

 

 

 

 

 

  Comments,     Trackbacks
Do it! 타입스크립트 프로그래밍 7장

Ch07 Promiseasync/await 구문

07-1) 비동기 콜백함수

(1) 동기와 비동기 API

- node.js에는 파일 시스템과 관련된 기능을 모아둔 fs패키지를 제공한다. Fs 패키지는 같은 기능을 동기(synchronous)’비동기(asynchronous)’ 버전으로 나누어 제공하며 예를들어 파일 읽는 기능은 동기버전인 readFileSync와 비동기 버전 readFile로 제공함

 

Promiseasync/await 구문을 사용한 예

import {readFileSyncreadFilefrom 'fs'

 

//package.json 파일을 동기 방식으로 읽는 

console.log('read package.json using synchronous api...')

const buffer: Buffer = readFileSync("./package.json")

console.log(buffer.toString())

 

//package.json 파일을 비동기 방식으로 읽는 

readFile("./package.json"(error: Errorbuffer: Buffer) => {

    console.log('read package.json using asynchronous api...')

    console.log(buffer.toString())

})

 

//Promise async/await 구문을 사용한 

const readFilePromise = (filename: string): Promise<string=>

    new Promise<string>((resolvereject) => {

        readFile(filename(error: Errorbuffer: Buffer) => {

            if(error)

                reject(error)

            else

                resolve(buffer.toString())

        })

    }); // Promise 객체를 만들 때는 반드시 세미콜론(;) 있어야 

 

(async () => {

    const content = await readFilePromise('./package.json')

    console.log('read package.json using Promise and async/await...')

    console.log(content)

})()

 

- 운영체제가 제공하는 서비스를 API라고 하며, API는 타입스크립트와 같은 프로그래밍 언어의 함수 형태로 제공되나 API 함수는 일반 함수와 달리 하드디스크에 저장된 파일을 읽는 등 실행 시 물리적 시간이 소요됨

 

- 따라서 파일 내용을 모두 읽을 때 까지 프로그램의 동작을 잠시 멈추는 동기 방식의 API와 프로그램 동작을 멈추지 않는 대신 콜백 함수로 얻게 하는 비동기 방식의 API를 제공함

 

- 비동기 API의 콜백 함수를 특별히 비동기 콜백 함수(asynchronous callback function)’이라고 하며, 일반 함수와 달리 API의 물리적 동작 결과(result)를 수신하는 목적으로만 사용됨

 

(2) readFileSyncreadFile API

- node.js는 운영체제 파일 시스템에 있는 파일을 읽을 수 있으며, node.js에서 파일 읽기는 readFileSync라는 API를 사용해 구현한다. readFileSync는 파일을 읽어서 Buffer 타입으로 전달해줌

 

Import {readFileSync} from ‘fs’

readFileSync(path: string): Buffer

 

- Buffer Node.js가 제공하는 클래스로 바이러니 데이터를 저장하는 기능을 수행하며, Buffer로 데이터 문자열을 만들 때 toString 메소드를 사용.

 

- Node.js에서 API 이름이 ‘xxxxSync’인 것은 모두 동기 방식으로 동작하며 동기 방식 API는 작업이 종료될 때까지 프로그램을 일시적으로 멈추게 하는 특징이 있음

readFileSync 함수 사용 예

import {readFileSyncreadFilefrom 'fs'

 

// package.json 파일의 바이너리 내용

const buffer: Buffer = readFileSync('./package.json')

const content: string = buffer.toString()

console.log(content)

 

Node.js 는 비동기 버전인 readFile도 제공

Import {readFile} from ‘fs’

readFile(파일 경로, 콜백 함수: (error: Error, buffer: Buffer) => void)

 

- readFile은 동기 버전과 달리 예외가 발생하면 이 예외를 콜백 함수의 첫 번쨰 매개변수에 전달해줌.

 

readFile 함수 사용 예

import {readFilefrom 'fs'

 

readFile('./package.json'(err: Errorbuffer:Buffer) => {

    if(errthrow err // 오류 발생  처리 코드

    else {

        const content: string = buffer.toString()

        console.log(content// package.json 파일 내용

    }

})

//코드는 멈추지 않고 계속 실행

 

(3) 단일 스레드와 비동기 API

- 자바스크립트는 단일 스레드(single-thread)로 동작하므로 될 수 있으면 readFileSync와 같은 동기 API를 사용하지 말아야하며 타입 스크립트도 마찬가지임

- 자바스크립트나 타입스크립트에서 동기 API가 실행되면, 운영체제는 동기 API의 작업 결과를 함수의 반환값으로 돌려주어야 하므로, 코드를 일시적으로 멈추기 때문에 프로그램의 반응성(responsiveness)을 떨어뜨림(웹 서버 접속이 멈추는 현상이 발생하기도 함)

 

(4) 콜백 지옥

- 비동기 API를 사용하고 콜백 함수에서 또 다른 비동기 API를 호출하는 코드를 만들면 코드가 매우 복잡해짐

 

7-2) Promise 이해하기

- 자바스크립트 언어에서 PromiseES5 버전에서 정식 기능으로 채택되었으며, 사용하기 위해선 new 연산자로 Promise 객체를 만들어야 함.

const promise = new Promise(콜백 함수)

- Promise의 콜백 함수는 resolve reject라는 두개의 매개변수를 가짐

(resolve, reject) => {}

- 타입스크립트에서는 제네릭 클래스 형태로 사용

Const numPromise: Promise<number> = new Promise<number>(콜백함수)

Const strPromise: Promise<string> = new Promise<string>(콜백함수)

Const arrayPromise: Promise<number[]> = new Promise<number[]>(콜백함수)

- 타입스크립트는 Promise 콜백 함수는 다음처럼 resolve reject 함수를 매개변수로 받는 형태

new Promise<T>((

 resolve: (successValue: T) => void,

 reject: (any) => void

) => {

})

(1) resolve reject 함수

프로미스를 사용하는 코드 작성 예

import {readFilefrom 'fs'

 

export const readFilePromise = (filename: string): Promise<string=>

    new Promise<string>((

        resolve: (value: string) => void,

        reject: (error: Error) => void=> {

            readFile(filename(err:Errorbuffer: Buffer) => {

                if(errreject(err)

                else resolve(buffer.toString())

            })

        })

 

프로미스를 사용해 실행한 예

import {readFilePromisefrom './readFilePromise'

 

readFilePromise('./package.json')

    .then((content: string) => {

        console.log(content//package.json 파일을 읽은 내용

        return readFilePromise('./tsconfig.json')

    })

    .then((content: string) => {

        console.log(content)

        return readFilePromise('.')

    })

    .catch((err: Error) => console.log('error:'err.message))

    .finally(() => console.log('프로그램 종료'))

 

- readFilePromise에서 resolve함수를 호출한 값은 then메서드의 콜백함수 쪽에 전달되고, reject 함수를 호출한 값은 catch 메서드의 콜백 함수 쪽에 전달된다.

 

(2) Promise.resolve 메서드

- Promise 클래스는 resolve라는 클래스 메서드(정적 메서드)를 제공하며 앞서 Promise 객체를 생성할 때 resolve 함수를 호출했다.

 

- Promise.resolve는 이를 클래스 메서드로 구현한 것이며 Promise.resolve() 형태로 호출하면 항상 이 then 메서드에서 얻을 수 있다.

 

resolve 메서드를 사용한 예

Promise.resolve(1)

    .then(value => console.log(value)) 

 

Promise.resolve('hello')

    .then(value => console.log(value))

 

Promise.resolve([123])

    .then(value => console.log(value))

 

Promise.resolve({name: 'Jack', age: 32})

    .then(value => console.log(value))

 

(3) Promise.reject 메서드

- Promise.reject(Error 타입 객체)를 호출하면 이 ‘Error 타입 객체는 항상 catch 메서드의 콜백 함수에서 얻을 수 있습니다.

 

(4) then-체인

- Promisethen 인스턴스 메서드를 호출할 때 사용한 콜백 함수는 값을 반환할 수 있으며, then에서 반환된 값은 또 다른 then 메서드를 호출해 값을 수신할 수 있다.

 

- then 메서드는 반환된 값이 Promise 타입이면 이를 해소(resolve)한 값을 반환하며, 만약 거절(reject)당한 값일 때는 catch 메서드에서 이 거절당한 값을 얻을 수 있다.

 

- 이렇게 Promise 객체에 then 메서드를 여러 번 호출하는 코드 형태를 ‘then-체인이라고 한다.

Then 체인 코드 사용 예

Promise.resolve(1)

    .then((value: number)=> {

        console.log(value)

        return Promise.resolve(true)

    })

    . then((value: boolean) => {

        console.log(value)

        return [123]

    })

    .then((value: number[]) => {

        console.log(value)

        return { name: 'jack', age: 32}

    })

    .then((value: {name: stringage: number}) => {

        console.log(value)

    })

 

    

 

(5) Promise.all 메서드

- Promise 클래스는 all 이라는 이름의 클래스 메서드를 제공

All(프로미스 객체 배열: Promise[]): Promise<해소된 값들의 배열(혹은 any)>

 

- Promise.all 메서드는 Promise 객체들을 배열 형태로 받아, 모든 객체를 대상으로 해소된(resolve)값들의 배열로 만들어줌

- 이렇게 구성된 또 다른 Promise 객체를 반환하므로 해소된 값들은 then 메서드를 호출해서 얻어야함.

- 배열에 담긴 Promise 객체가 객체 중 거절(reject) 객체가 발생하면 더 기다리지 않고 해당 거절 값을 담은 값(reject value)을 담은 Promise.reject 객체를 반환

 

promise.all을 사용한 예시

const getAllResolvedResult = <T>(promises: Promise<T>[]) => Promise.all(promises)

 

getAllResolvedResult<any>([Promise.resolve(true), Promise.resolve('hello')])

    .then(result => console.log(result))

 

    getAllResolvedResult<any>([Promise.reject(new Error('error')), Promise.resolve(1)])

    .then(result => console.log(result))

    .catch(error => console.log('error:'error.message))

 

(6) Promise.race 메서드

- Array 클래스는 배열의 내용을 하나라도 조건을 만족하면 true를 반환하는 some이라는 인스턴스 메서드를 제공

 

- Promise.race 클래스 메서드는 배열에 담긴 프로미스 객체 중 하나라도 해소(resolve)되면 이 값을 담은 Promise.resolve 객체를 반환하며, 거절 값이 가장 먼저 발생하면 Promise.reject 객체를 반환함.

const isAnyTrue = (values: boolean[]) => values.some((value => value == true))

 

console.log(

    isAnyTrue([falsetruefalse]),

    isAnyTrue([falsefalsefalse])

)

 

Race(프로미스 객체 배열: Promise[]): Promise<가장 먼저 해소된 객체의 값 타입(혹은 Error)>

 

 

7-3) async await 구문

- 2013년 마이크로소프트가 C# 5.0을 발표하면서 asyncawait 구문을 제공했으며, 이후 자바스크립트를 포함, 많은 프로그래밍 언어가 이 구문을 차용

async/await 구문 예

const test = async () => {

    const value = await Promise.resolve(1)

    console.log(value)

}

test()

 

(1) await 키워드

- await 키워드는 피연산자(operand)의 값을 반환해주지만, 피연산자가 Promise 객체이면 then 메서드를 호출해 얻은 값ㅇ르 반환해 줌

Let value = await Promise 객체 혹은 값

 

(2) async 함수 수정자

- , await 키워드는 항상 async라는 이름의 함수 수정자(function modifier)가 있는 함수 몸통에서만 사용할 수 있다.

Const test1 = async() => {

   Await Promise 객체 혹은 값

}

화살표 함수 형태로 async 함수를 구현한 예

export const test1 =  async () => {

    let value = await 1

    console.log(value)

    value = await Promise.resolve(1)

    console.log(value)

}

function 함수 형태로 async 함수를 구현한 예

export async function test2() {

    let value = await 'hello'

    console.log(value)

    value = await Promise.resolve('hello')

    console.log(value)

}

 

 

 

(3) async 함수의 두 가지 성질

- async 함수 수정자가 붙은 함수는 다음과 같은 특징이 있음

일반 함수처럼 사용할 수 있다.

Promise 객체로 사용할 수 있다.

 

(4) async 함수가 반환하는 값의 의미

- async 함수는 값을 반환할 수 있으며 반환값은 Promise 형태로 변환되므로 then 메서드를 호출해 async 함수의 반환값을 얻어야 한다.

 

(5) async 함수의 예외 처리

- async 함수에서 다음처럼 예외가 발생하면 프로그램이 비정상으로 종료됨

Const asyncException = async () => {

   Throw new Error(‘error’)

}

asyncException()

 

- 예외로 비정상적인 종료를 막으려면 asyncException을 함수 호출방식이 아닌, asyncException함수가 반환하는 프로미스 객체의 catch 메서드를 호출하는 형태로 코드를 작성해야 한다.

 

const asyncException = async () => {

    throw new Error('error')

}

asyncException()

   .catch(err => console.log('error:'err.message))

 

 

- await 역시 Promise.reject 값이 발생하면 프로그램이 비정상으로 종료하므로 다음과 같이 해서 방지할 수 있다.

 

const awaitReject = async() => {

    await Promise.reject(new Error('error'))

}

 

awaitReject()

    .catch(err => console.log('error:'err.message))

 

(6) async 함수와 Promise.all

비동기 APIreadFile을 프로미스로 만든 readFilePromise.tsasync에 적용한 예

import {readFilePromisefrom './readFilePromise'

 

const readFilesAll = async (filenames: string[]) => {

    return await Promise.all (

        filenames.map(filename => readFilePromise(filename))

    )

}

 

readFilesAll(['./package.json''./tsconfig.json'])

   .then(([packageJsontsconfigJson]: string[]) => {

       console.log('<package.json>: 'packageJson)

       console.log('<tsconfig.json>: 'tsconfigJson)

   })

   .catch(err => console.log('error:'err.message))

 

 

 

 

 

 

 

 

  Comments,     Trackbacks
Do it! 타입스크립트 프로그래밍 6장

Ch06 반복기와 생성기

6-1) 반복기 이해하기

(1) 반복기와 반복기 제공자

- for…of 구문은 타입에 무관하게 배열에 담긴 값을 차례로 얻는 데 활용되며, 다른 프로그래밍 언어에서도 반복기(iterator)라는 주제로 흔히 찾아볼 수 있음

 

- 반복기는 일반적으로 다음과 같은 특징이 있음

1. next라는 이름의 메서드를 제공

2. next 메서드는 valuedone이라는 두 개의 속성을 가진 객체를 반환

 

반복기제공자(iterable)의 예

export const createRangeIterable = (from: numberto:number) => {

    let currentValue = from

    return {

        next() {

            const value = currentValue < to ? currentValue++ : undefined

            const done = value == undefined

            return {valuedone}

        }

    }

}

 

(2) 반복기는 왜 필요한가?

- 반복기 제공자는 어떤 범위의 값을 한꺼번에 생성해서 배열에 담지 않고 값이 필요할 때만 생성함

 

-  직접 실습한 range 함수와 createRangeIterator 함수를 비교해보면 createRangeIterator는 값이 필요한 시점에 비로소 생성하지만, range 함순느 값이 필요한 시점보다 이전에 미리 생성한다는 차이점이 있다. 따라서 시스템 메로이 효율상 createRangeIterator 함수가 메모리를 훨씬 적게 소모함.

 

(3) for…of 구문과 [Symbol.iterator] 메서드

- range 함수는 for…of 구문의 of 뒤에 올 수 있지만 createRangeIterator는 똑같이 적용하면, ‘[Symbol.iterator]() 메서드가 없다라는 오류가 발생함

 

- 이를 개선해 다음과 같이 RangeIterable 함수를 만들 수 있음

export class RangeIterable {

    constructor(public from:numberpublic to: number) {}

    [Symbol.iterator]() {

        const that = this

        let currentValue = that.from

        return {

            next() {

                const value = currentValue < that.to ? currentValue++ : undefined

                const done = value == undefined

                return {valuedone}

            }

        }

    }

}

(4) Iterable<T>Iterator<T> 인터페이스

- 타입스크립트는 반복기 제공자에 Iterable<T>Iterator<T> 제네릭 인터페이스 사용 가능

Class 구현 클래스 implements Iterable<생성할 값의 타입> {}

- 또한 Iterator<T>는 반복기가 생성할 값의 타입을 명확하게 해줌

[Symbol.iterator](): Iterator<생성할 값의 타입> {}

 

Iterable<T>Iterator<T>를 사용해 구현한 예

export class StringIterable implements Iterable<string> {

    constructor(private strings: string[] = [], private currentIndex: 

number = 0) {}

    [Symbol.iterator](): Iterator<string> {

        const that = this

        let currentIndex = that.currentIndexlength = that.strings.length

 

        const iterator: Iterator<string= {

            next(): {value: stringdone: boolean} {

                const value = currentIndex < length ? 

that.strings[currentIndex++: undefined

                 const done = value == undefined

                  return {valuedone}

            }

        }

        return iterator

    }

}

 

6-2) 생성기 이해하기

- ESNext 자바스크립트와 타입스크립트는 yield라는 키워드를 제공

- yield return 키워드처럼 값을 반환하며, function* 키워드를 사용한 함수에서만 호출 가능하며 function*로 만든 함수를 생성기(generator)라고 함

생성기 예시

export function* generator() {

    console.log('generator started...')

    let value = 1

    while(value < 4)

        yield value++

    console.log('generator finished')

}

 

테스트 코드 예시

import {generatorfrom './generator'

for(let value of generator())

    console.log(value)

 

 (1) setInterval 함수와 생성기의 유사성

- 생성기가 동작하는 방식을 세미코루틴(semi-coroutine, 반협동 루틴)’이라고 하며, 세미코루틴은 타입스크립트처럼 단일 스레드(single-thread)로 동작하는 프로그래밍 언어가 마치 다중스레드(multi-thread)로 동작하는 것처럼 보이게 하는 기능을 함

 

- setInterval 함수의 동작 방식은 C++ 언어의 스레드 동작 방식과 흡사한 면이 있다.

 

Cf) 세미코루틴과 코루틴의 차이

- 반도체(semiconductor)에서 반도체란 전기를 절반만 통과시키는 도체라는 의미이며, 여기서 반은 반대(anti)의 의미가 아닌 절반(semi)의 의미임

- 이처럼 생성기를 세미코루틴(semi-coroutine)이라고 하는데 절반만 코루틴이라는 뜻이다.

- 1958년부터 꾸준히 학문적으로 연구해온 주제로 클로저(Clojure)는 코루틴을 최초로 프로그래밍 문법으로 탑재한 언어이며, 구글의 Go언어는 고루틴(Goruotine)이라는 용어를 쓰며 코루틴과 맥락을 같이한다.

- 코루틴은 애플리케이션 레벨의 스레드로, 과거에는 스레드 개수가 제한되어있어, 스레드를 과ㅅ다하게 소비하면 운영체제에 무리를 주기 때문에 코루틴을 연구하기 시작했다.

 

- 코루틴은 스레드이므로 일정 주기에 따라 자동으로 반복해서 실행되지만, 생성기는 절반만 코루틴이므로 반복해서 실행은 되지만 자동으로 실행되지 못하는 코루틴이다.

 

- 생성기는 사용하는 쪽 코드에서 생성기가 만들어 준 반복자의 next 메서드가 호출될 때만 한번 실행됨 즉, 자동으로 반복 실행되지 않으므로 세미코루틴이라고 함.

 

(2) function* 키워드

- function* 키워드로 선언된 함수가 생성기인데, 생성기는 오직 function* 키워드로만 만들 수 있으므로 화살표 함수로 생성기를 만들 수 없음.

 

주의할 점 : 생성자를 만들 때 선언하는 function* function*을 붙인게 아니라 function* 자체가 하나의 키워드다. 따라서 화살표 함수 형태로는 생성기를 만들 수 없으며, function* 사이에는 공백은 없어도 되고 있어도 상관 없다.

 

(3) yield 키워드

- yield는 연산자(operator) 형태로 동작하며 2가지 기능을 한다.

1. 반복기를 자동으로 만들어 준다

2. 반복기 제공자 역할도 수행한다

 

function* 키워드를 이용해 생성기 형태로 만든 함수 예

export function* rangeGenerator(from: numberto: number) {

    let value = from

    while(value < to) {

        yield value++

    }

}

 

(4) 반복기 제공자의 메서드로 동작하는 생성기 구현

- 생성기는 반복기를 제공하는 반복기 제공자로서 동작함

생성기를 사용해 구현한 코드 예

export class IterableUsingGenerator<Timplements Iterable<T> {

    constructor(private values: T[] = [], private currentIndex: number = 0) {}

    [Symbol.iterator= function* () {

        while(this.currentIndex < this.values.length)

            yield this.values[this.currentIndex++]

    }

}

 

(5) yield* 키워드

- 타입스크립트는 yield 뒤에 *을 붙인 yield* 키워드도 제공하며, yield는 단순히 값을 대상으로 동작하지만, yield*는 다른 생성기나 배열을 대상으로 동작함

 

(6) yield 반환값

- yield 연산자는 값을 반환함

 

* yield 반환 값 함수 예시

export function* gen() {

    let count = 5

    let select = 0

    while(count--) {

        select = yield `you select ${select}`

    }

}

export const random = (maxmin=0) => Math.round(Math.random() * (max-min)) + min

  Comments,     Trackbacks
Do it! 타입스크립트 프로그래밍 5장

Ch05 배열과 튜플

5-1) 배열 이해하기

- 자바스크립트에서 배열은 Array 클래스의 인스턴스이며 다음처럼 선언함

let 배열 이름 = new Array(배열 길이)

let array = new Array

array.push(1); array.push(2); array.push(3)

console.log(array)

 

- 배열에 담긴 각각의 값을 아이템(item) 또는 원소(element)라고 하며, 위의 예제 배열에는 3개의 아이템을 담고 있다.

 

(1) [ ] 단축 구문

- 자바스크립트는 [ ] 단축 구문을 제공하며, 이를 이용해 한번에 배열을 만들 수도 있다.

let numbers = [123]

let strings = ['Hello''World']

console.log(numbersstrings)

 

(2) 자바스크립트에서 배열은 객체다

- 자바스크립트에서 배열은 다른 언어와 다르게 객체임. Array클래스의 인스턴스이기 때문

- Array클래스에서 제공하는 여러 메서드 중 Array.isArray는 매개변수로 전달받은 심벌이 배열인지 객체인지 알려줌

 

(3) 배열의 타입

- 타입스크립트에서 배열의 타입은 아이템 타입[]’.

let numArray: number[] = [123]

let strArray: string[] = ['Hello''World']

 

type IPerson = {name: stringage?: number}

let personArray: IPerson[] = [{name: 'Jack'}, {name: 'Jane', age: 32}]

 

 

(4) 문자열과 배열 간 변환

- 어떤 프로그래밍 언어는 문자열(string)을 문자(character)들의 배열(array)로 간주하지만 타입스크립트에선 문자 타입이 없고 문자열의 내용 또한 변경할 수 없다.

- 문자열을 가공하려면 문자열을 배열로 전환해야함 (Stringsplit 클래스 이용)

split(구분자: string): string[]

export const split = (str: stringdelim: string = ''): string[] => str.split(delim)

 

split 함수는 매개변수로 전달받은 문자열과 구분자를 이용해 String 클래스의 split 메서드를 호출함으로써 string[] 타입의 배열로 만들어줌

import {splitfrom './split'

console.log(

    split('hello'),

    split('h_e_l_l_o''_')

)

 

[ 'h', 'e', 'l', 'l', 'o' ] [ 'h', 'e', 'l', 'l', 'o' ]

 

 

- string[] 타입의 배열을 다시 string으로 변환하려면 Array 클래스의 join 메서드를 사용

join(구분지: string): string

 

(5) 인덱스 연산자

- 배열이 담고 있는 아이템 중 특정 위치에 있는 아이템을 얻고자 할 때는 인덱스 연산자(index operator)[인덱스]를 사용

const numbers: number[] = [12345]

for(let index = 0index < numbers.length; index++) {

    const item: number = numbers[index]

    console.log(item)

}

 

(6) 배열의 비구조화 할당

- 객체뿐 아니라 배열에도 비구조화 할당을 적용할 수 있음. 배열의 비구조화 할당문에는 [] 기호 사용

 

(7) for … in

- ESNext 자바스크립트와 타입스크립트는 for 문을 좀 더 쉽게 사용하도록 for…in 문을 제공

for(변수 in 객체) {

}

let names = ['Jack''Jane''Steve']

 

for (let index in names) {

    const name = names[index]

    console.log(`[${index}]: ${name}`)

}

 

[0]: Jack

[1]: Jane

[2]: Steve

 

- 만약 for…in문에 객체를 사용할 때는 객체가 가진 속성을 대상으로 순회함

let jack = {name: 'Jack', age: 32}

for(let property in jack)

console.log(`${property}: ${jack[property]}`)

 

name: Jack

age: 32

 

(8) for…of

- for…in문이 배열의 인덱스 값으로 대상을 순화한다면, for…of는 배열의 아이템값을 대상으로 순회함

for(let name of ['Jack''Jane''Steve'])

    console.log(name)

 

(9) 제네릭 방식 타입

- 배열을 다루는 함수 작성시 number[]처럼 타입이 고정된 함수가 아닌 T[]처럼 배열의 아이템 타입을 한꺼번에 표현하는 것이 편리함

- 타입을 T와 같이 일종의 변수(타입 변수)로 취급하는 것을 제네릭(generics)타입이라고 함

 

Ex) 예를 들어 배열의 길이를 얻는 함수로 arrayLength를 선언한다면 다음과 같이 만들 수 있음

const arrayLength = (array) => array.length

 

이를 다양한 아이템 타입을 가지는 배열에 똑같이 적용하려면 다음과 같이 표현 가능

const arrayLength = (array: T[]): number => array.length

 

그러나 이러면 컴파일러에게 T가 타입 변수라고 알려줘야 함으로 다음과 같이 표현한다.

const arrayLength = <T>(array: T[]): number => array.length

 

배열이 비어있는지 확인함수는 다음과 같이 표현 가능하다

export const isEmpty = <T>(array: T[]): boolean => arrayLength<T>(array== 0

 

Ex) import {arrayLengthisEmptyfrom './arrayLength'

let numArray: number[] = [123]

let strArray: string[] = ['Hello''World']

 

type IPerson = {name: stringage?: number}

let personArray: IPerson[] = [{name: 'Jack'}, {name: 'Jane', age: 32}]

 

console.log(

 

    arrayLength(numArray),

    arrayLength(strArray),

    arrayLength(personArray),

    isEmpty([]),

    isEmpty([1])

)

 

Result: 3 2 2 true false

 

(10)제네릭 함수의 타입 추론

제네릭 형태로 구현된 함수는 원칙적으로 다음과 같이 명시해주어야함

함수 이름<타입 변수>(매개변수)

그러나 타입스크립트는 다음 예시(4)처럼 타입 변수 부분을 생략할 수 있게함.

const identity = <T>(n: T): T => n

console.log(

    identity<boolean>(true),

    identity(true)

)

 

이 경우, 타입스크립트는 타입 추론을 통해 생략된 타입을 찾아냄

(11) 제네릭 함수의 함수 시그니처

- const f = <T>(cb: (arg: T, i?: number) => number): void => {}

다음과 같은 형태로 변수를 삽입해서 변수에 타입을 명시해 해결함

 

(12) 전개 연산자

- 배열에도 전개연산자 적용이 가능ㅎ며, 예시처럼 전개 연산자를 사용해 두 배열과 특정 값을 동시에 결합할 수 있다.

 

(13) range 함수 구현

- 배열에 전개 연산자를 적용하면 ramda 외부 패키지가 제공하는 R.range와 같은 방식으로 range 함수를 구현할 수 있다.

export const range = (from: numberto: number): number[] =>

    from < to ? [from...range(from + 1to)] : []

 

import {rangefrom './range'

let numbers: number[] = range(19 + 1)

console.log(numbers)

 

 

 

 

 

 

 

 

 

 

 

 

5-3) 배열의 map, reduce, filter 메서드

(1) filter 메서드

- 배열의 타입이 T[]일 때 배열의 filter 메서드는 다음과 같은 형태로 설계되었음

Filter(callback: (value: T, index?: number): boolean ): T[]

Filter method의 사용 예

import {rangefrom '../../ch05-1/src/range'

 

const array: number[] = range(110+1)

 

let odds: number[] = array.filter((value) => value % 2 != 0)

let evens: number[] = array.filter((value) => value % 2 == 0)

console.log(oddsevens)

 

 

- filter 메서드는 두번째 매개변수에 index라는 선택 속성을 제공

index값을 사용해 배열을 반(half)으로 나눈 예

import {rangefrom '../../ch05-1/src/range'

 

const array: number[] = range(110+1)

const half = array.length / 2

 

let belowHalf: number[] = array.filter((vindex) => index < half)

let overHalf: number[] = array.filter((vindex) => index >= half)

console.log(belowHalfoverHalf)

 

(2) map 메서드

- 배열의 타입이 T[] 일 때 배열의 map 메서드는 다음과 같은 형태로 설계되며 filter와 달리 map 메서드는 입력 타입과 다른 타입의 배열을 만들 수 있음

Map(callback: (value: T, index?: number): Q): Q[]

 

Map을 사용해 Number[] 타입 배열을 string[] 타입배열로 가공한 예

import {rangefrom '../../ch05-1/src/range'

 

let names: string[] = range(15 + 1)

    .map((valindex) => `[${index}]: ${val}`)

console.log(names)

 

(3) reduce 메서드

- reduce함수는 다음과 같이 설계됨

Reduce(callback: (result: T, value: T), initialValue: T): T

 

1부터 100까지 더하는 로직을 reduce메소드를 사용해 구현한 예

import {rangefrom './range'

 

let reduceSum: number = range(1100 + 1)

    .reduce((result: numbervalue: number) => result + value0)

console.log(reduceSum)

 

 

5-4) 순수 함수와 배열

- 함수형 프로그래밍에서 함수는 순수 함수(pure function)’라는 조건을 만족해야 함. 그러나 타입스크립트의 Array 클래스에는 순수 함수 조건에 부합하지 않는 메서드가 많음

- 순수 함수란 부수 효과(side-effect)가 없는 함수를 말하는데, 부수 효과란 함수가 가진 고유 목적 이외에 다른 효과가 나타나는 것을 말하며 부작용이라고도 함

- 이러한 부수 효과가 있는 함수는 불순 함수(impure function)이라고 함

- 순수 함수과 되려면 다음과 같은 조건을 만족해야 함

순수 함수의 조건

함수 몸통에 입출력 관련 코드가 없어야한다.

함수 몸통에서 매개변숫값을 변경시키지 않는다.(, 매개변수는 constreadonly 형태로만 사용한다.)

함수는 몸통에서 만들어진 결과를 즉시 반환한다.

함수 내부에 전역 변수나 정적 변수를 사용하지 않는다.

함수가 예외를 발생시키지 않는다.

함수가 콜백 함수로 구현되었거나 함수 몸통에 콜백 함수를 사용하는 코드가 없다.

함수 몸통에 Promise와 같은 비동기 방식으로 동작하는 코드가 없다.

 

순수 함수의 예

Function pure(a: number, b:number): number {return a + b}

 

불순 함수의 예

function impure(array: number[]): void {

array.push(1)

array.splice(0, 1)

}

 

(1) 타입 수정자 readonly

- 타입스크립트는 순수 함수 구현을 위해 readonly 키워드를 제공하며 readonly 타입으로 선언된 매개변숫값을 변경하려면 경고창을 띄워 불순함수가 되는 것을 막는다.

 

(2) 불변과 가변

- constreadonly를 명시하고 있으면 변숫값은 초깃값을 항상 유지하며 이를 불변(immutable) 변수라고 함

 

- 반면 let이나 readonly를 명시하지 않는 변수는 값 변경이 가능하므로 가변(mutable) 변수라고 함

 

(3) 깊은 복사와 얕은 복사

- 프로그래밍 언어에서 어떤 변숫값을 다른 변숫값으로 설정하는 것을 복사(copy)라고 표현하며 복사에는 깊은 복사(deep-copy)’, ‘얕은 복사(shallow-copy)’ 두 종류가 있음

- 순수 함수 구현시에는 매개변수가 불변성을 유지해야하므로, 깊은 복사를 실행해 매개변숫값이 변경되지 않도록 해야함

- 깊은 복사는 대상 변숫값이 바뀔 때 원본 변숫값은 그대로인 형태로 동작

깊은 복사의 예

let original = 1

let copied = original

copied += 2

console.log(originalcopied)

 

결괏값: 1 3

 

- 객체와 배열은 얕은 복사 방식으로 동작함

얕은 복사의 예

const originalArray = [5397]

const shallowCopiedArray = originalArray

shallowCopiedArray[0= 0

console.log(originalArrayshallowCopiedArray)

 

결괏값: [ 0, 3, 9, 7 ] [ 0, 3, 9, 7 ]

 

(4) 전개 연산자와 깊은 복사

- 전개 연산자를 사용해 배열을 복사하면 깊은 복사를 할 수 있음

전개 연산자를 사용해 깊은 복사를 한 예

const oArray = [1234]

const deepCopiedArray = [...oArray]

deepCopiedArray[0= 0

console.log(oArraydeepCopiedArray)

 

결괏값: [ 1, 2, 3, 4 ] [ 0, 2, 3, 4 ]

 

(5) 배열의 sort 메서드를 순수 함수로 구현하기

- Array 클래스의 sort메서드는 배열의 아이템을 오름차순(ascend) 또는 내림차순(descend)로 정렬해줌

 

- , sort 메서드는 원본 배열의 내용을 변경하므로 readonly 타입을 적용해 순수함수로써 sort 역할을 하는 메서드를 만들어 봄

export const puerSort = <T>(array: readonly T[]): T[] => {

    let deepCopied = [...array]

    return deepCopied.sort()

}

 

pureSort의 사용 예

import {pureSortfrom './pureSort'

 

let beforeSort = [ 629 ,0]

const afterSort = pureSort(beforeSort)

console.log(beforeSortafterSort)

결괏값: [ 6, 2, 9, 0 ] [ 0, 2, 6, 9 ]

 

(6) 배열의 filter 메서드와 순수한 삭제

- 배열에서 특정 아이템을 삭제할 때는 slice 메서드를 사용하지만, 원본 배열의 내용을 변경하므로 순수 함수에서는 사용 불가함

 

- 대신 filter 메서드를 사용할 수 있음. 배열이 제공하는 filtermap 메서드는 sort와 달리 깊은 복사 형태로 동작하므로 filter 사용하면 원본 배열의 내영을 훼손하지 않으면서 조건에 맞지 않는 아이템을 삭제 가능함

 

filter 사용해 특정 아이템을 삭제하는 함수 예

export const pureDelete = <T>(array: readonly T[], cb: (val: Tindex?: number) => 

boolean): T[] => array.filter((valindex) => cb(valindex== false)

 

배열과 객체가 섞인 원본을 훼손하지 않으면서 배열만 제거한 배열(objectOnly)를 만든 예

import {pureDeletefrom './pureDelete'

 

const mixedArray: object[] = [

    [], {name:'Jack'}, {name: 'Jane', age: 32}, ['description']

]

const objectOnly: object[] = pureDelete(mixedArray(val) => Array.isArray(val))

console.log(mixedArrayobjectOnly)

 

(7) 가변 인수 함수와 순수 함수

- 함수를 호출할 때 전달 인수의 개수를 제한하지 않는 것을 가변 인수(variadic arguments)라고 하며 매개변수 앞의 은 잔여나 전개 연산자가 아니라 가변 인수를 표현하는 구문임

 

5-5) 튜플 이해하기

- 파이썬 등에는 튜플이 존재하지만, 자바스크립트에서는 튜플이 없으며 단순히 배열의 한 종류로 취급됨.

let tuple: any[] = [true'the result is ok']

 

- 그러나 any[] 형태는 타입 기능을 무력하므로 다르게 선언할 수 있음

const array: number[] = [123 ,4]

const tuple: [booleanstring= [true'the result is ok']

 

(1) 튜플에 타입 별칭 사용하기

- 일반적으로 튜플 사용시에는 타입 별칭(alias)로 튜플의 의미를 명확하게 함

 

  Comments,     Trackbacks
Do it! 타입스크립트 프로그래밍 4장

Chap04 함수와 메서드

4-1) 함수 선언문

- 자바스크립트에서 함수는 function 키워드로 만드는 함수와 => 기호로 만드는 화살표 함수 두 가지가 있다.

function 함수

Function 함수 이름(매개변수1, 매개변수2[, …]) {

함수 몸통

}

타입스크립트 함수 선언문은 자바스크립트 함수 선언문에서 매개변수와 함수 반환값(return type)에 타입 주석을 붙이는 다음 형태로 구성

Function 함수 이름(매개변수1: 타입1, 매개변수2: 타입2[, …]) : 반환값 타입 {

함수 몸통

}

 

Ex) function add(a: numberb: number): number {

    return a + b

}

 

매개변수와 인수, 인자

- 일반적으로 parameter매개변수라 하고, argument인수혹은 인자라고 한다.

- 매개변수는 함수 선언문에서 함수 이름 뒤 괄호 안에 선언하는 변수이며, 인수는 함수를 호출할 때 전달하는 값임

- 아래 예시에서 a, b는 매개변수이고, 12는 인수임.

function add(a: numberb:number): number {

    return a+b

}

 

let result = add(12)

 

 

(1) 매개변수와 반환값의 타입 주석 생략

- 변수 때와 마찬가지로 함수 선언문에서 매개변수와 반환값에 대한 타입 주석을 생략할 수 있음

- , 변수 떄와 달리 함수의 매개변수 타입과 반환 타입을 생략하는 것은 바람직하지 않음(구현의도를 알기 어렵고 잘못 사용하기 쉽기때문)

 

(2) void 타입

- 값을 반환하지 않는 함수는 반환 타입이 void. Void는 반환 타입으로만 사용 가능

function printMe(name: stringage:number): void {

    console.log('name: ${name}, age: ${age}')

}

 

(3) 함수 시그니처

- 함수의 타입은 함수 시그니처(function signatrue)라고하며 다음과 같이 표현함

(매개변수1 타입, 매개변수2 타입[, …]) => 반환값 타입

let printMe: (stringnumber) => void = function (name:stringage: number): void {}

 

-만약 매개변수가 없으면 단순히 ()로 표현함. () => void는 매개변수도 없고 반환값도 없는 함수 시그니처

 

(4) type 키워드로 타입 별칭 만들기

- 타입스크립트는 type이라는 키워드를 제공하며, 기존에 존재하는 타입을 단순히 이름만 바꿔서 사용할 수 있게 해주는데 이를 타입 별칭(type alias)라고 함

type 새로운 타입 = 기존 타입

 

- 함수 타입인 함수 시그니처를 명시하면 매개변수의 개수나, 타입, 반환타입이 다른 함수를 선언하는 잘못을 미연에 방지할 수 있음

 

(5) undefined 관련 주의 사항

-  undefined 타입은 타입스크립트 타입 계층도에서 가장 최하위 타입임

- undefined 타입을 매개변수로 호출하면 object를 상속하는 자식 타입으로 간주하기 때문에 구문 오류가 생기지 않지만, 실행하면 읽을 수 없다고 발생함

- 이를 방지하기 위해 undefined인지 판별하는 코드를 작성해야함

interface INameable {

    name: string

}

 

function getName(o: INameable) {

    return o != undefined ? o.name : 'unknown name'

}

 

let n = getName(undefined)

 

console.log(n)

console.log(getName({name: 'Jack'}))

 

 

인터페이스에 선택 속성이 있는 경우의 예

interface IAgeable {

    age?: number

}

function getAge(o: IAgeable) {

    return o != undefined && o.age ? o.age : 0

}

 

console.log(getAge(undefined))

console.log(getAge(null))

console.log(getAge({age:32}))

 

undefinednull

- 자바스크립트에는 undefinednull이라는 키워드가 있으며, 과거와는 달리 ESNext자바스크립트와 타입스크립트에서는 이 두 값은 완전히 같다.

 

(6) 선택적 매개변수

- 매개변수에서도 선택 속성처럼 물음표를 붙여 선택적 매개변수(optional parameter)를 표현할 수 있다.

function fn(arg1: stringarg?: number): void {}

 

4-2) 함수 표현식

(1) 함수는 객체다

- 자바스크립트는 함수형 언어 스킴(scheme)’과 프로토타입(prototype) 기반 객체지향 언어 셀프(self)’를 모델로 만들어짐

- 따라서 두가지 언어의 특징을 모두 포함하며, 타입스크립트도 마찬가지임

- 자바스크립트에서 함수는 Function 클래스의 인스턴스(instance)

- 함수 선언문에서 함수 이름을 제외한 function(a,b) {return a+b}와 같은 코드를 함수 표현식(function expression)이라고 함.

 

(2) 일등 함수(first-class function)
-
프로그래밍 언어가 일등 함수 언어를 제공하면 함수형 프로그래밍 언어(functional programming language)’라고 한다.

- 일등 함수란, 함수와 변수를 구분(혹은 차별)하지 않는다는 의미임

- 예시처럼 변수 f는 변수인지 함수인지 구분하기 어려움, 타입스크립트는 이런식으로 변수와 함수를 차별하지 않음

 

Ex) let f = function(a,b) { return a+ b}

f = function(ab) {return a-b}

 

(3) 표현식

- 프로그래밍 언어에서 표현식이라는 용어는 리터럴(literal), 연산자(operator), 변수, 함수 호출(function call) 등이 복합적으로 구성된 코드 형태를 의미

 

(4) 함수 표현식

- 위 예시처럼 변수 f에는 function(a, b) {return a + b}를 값처럼 대입하는데 이 function(a, b) {return a + b;} 부분을 함수 표현식(function expression)이라고 함

 

(5) 계산법

- 컴파일러는 표현식을 만나면 계산법을 적용해 어떤 값을 만들며, 조급한 계산법(eager evaluation)과 느긋한 계산법(lazy evaluation) 두 가지가 있음

- 조급한 계산법은 1+2 처럼값이 직접적으로 대입되어 있으므로 3이라는 결괏값을 도출함

- function(a, b) {return a+b}같은 함수 표현식을 만나면 느긋한 계산법을 적용해 계산을 보류함

 

(6) 함수 호출 연산자

- 변수가 함수 표현식을 담고 있다면, 변수 이름 뒤에 함수 호출 연산자(function call operator)()를 붙여서 호출할 수 있음.

- ‘함수 호출이란 함수 표현식의 몸통 부분을 실행한다는 의미

 

(7) 익명 함수

- 함수 표현식(function expression)은 익명(혹은 무명) 함수와는 다른 표현임

 

(8) const 키워드와 함수 표현식

- 함수 표현식을 담는 변수는 let보다 const 키워드로 선언하는 것이 바람직함 (변동이 없기 때문)

 

4-3) 화살표 함수와 표현식 문

- ESNext 자바스크립트와 타입스크립트는 function 키워드가 아닌 => 기호로 만드는 화살표 함수도 제공

Const 함수 이름 = (매개변수1: 타입1, 매개변수2: 타입2[, …]) : 반환 타입 => 함수 몸통

- 화살표 함수의 몸통은 function과는 달리 중괄호를 사용할수도, 생략할 수도 있다.

- 중괄호 사용 여부에 따라 타입스크립트 동작 방식이 실행문 방식(execution statement)와 표현식 문(expression statement) 방식으로 나뉨.

 

(1) 실행문과 표현식 문

- 프로그래밍 언어는 C를 대표로 하느 실행문 지향 언어와, 스칼라를 대표로 하는 표현식 지향 언어로 나뉘는데 자바스크립트는 실행문 지향 언어이지만, ESNext와 타입스크립트는 실행문과 표현식을 동시에 지원하며 이런 언어를 다중 패러다임 언어(multi-paradigm language)라고 함

 

- 실행문은 return 키워드를 사용하지만 표현식 문은 꼭 사용하지 않아도 괜찮음

 

(2) 복합 실행문

- if와 같은 구문을 만족하면 한 줄의 실행문만을 실행하는 형태로 설계

- 대부분 언어에서는 중괄호 {}를 사용해 여러 실행문을 한 개 처럼 인식하게 함

(3) 함수 몸통과 복합 실행문

function f() {

    let x = 1y = 2

    let result = x + y + 10

}

 

(4) return 키워드

- 실행문은 CPU에서 실행된 결과를 알려주지 않으므로 return 키워드를 도입

- 그러나 return 키워드는 반드시 함수 몸통에서만 사용할 수 있음

 

(5) 표현식 문 스타일의 화살표 함수 구현

- function 스타일 함수 isGreater를 화살표 함수로 구현한 예

const isGreater = (a: number b: number): boolean => {

    return a > b;

}

 

- 중괄호와 return 키워드를 삭제해서 구현 가능

const isGreater = (a: number b: number): boolean => a > b

 

(6) 표현식과 표현식 문의 차이

let a = 1b = 0

if(a > bconsole.log('a is greater than b')

const isGreater = (a: numberb: number): boolean => a > b

 

- 위 예시에서 a > b코드(2)C언어에서 표현식이라고 했기 때문에 모든 C언어와 같은 의미로 표현식이라고 생각하며 C 언어의 관점에서 실행문의 일부일 뿐 그 자체가 실행문인건 아님

- 반면 표현식 지향 언어의 관점에서 a>b 코드(3)는 그 자체가 실행문임. 따라서 표현식을 2가지 형태로 사용하기 때문에 표현식(expression)과 표현식 문(expression statement)으로 구분함

(7) 실행문을 만드는 세미콜론

- C언어는 모든 문장이 반드시 세미콜론 ; 으로 끝나야하며, ES5 자바스크립트 또한 C언어 구문을 참조해 만들었으므로 모든 문장 끝에 세미콜론이 있어야함.

- 그러나 ESNext 자바스크립트와 타입스크립트에서는 세미콜론 생략 가능하며, 타입스크립트에서는 관습적으로 표현식 문에는 세미콜론을 붙이지 않음.

 

4-4) 일등 함수 살펴보기

(1) 콜백 함수

- 함수 표현식을 매개변수로 받아 이 매개변수로 동작하는 함수를 콜백 함수(callback function)이라고 함

const f = (callback: () => void): void => callback()

 

- init 함수 사용 예

export const init = (callback: () => void): void => {

    console.log('default initialization finished.')

    callback()

    console.log('all initialization finished.')

}

 

import {initfrom './init'

init(() => console.log('custom initialization finished.'))

 

 

(2) 중첩 함수

- 함수형 언어에서 함수는 변수에 담긴 함수 표현식이므로 함수 안에 또 다른 함수를 중첩(nested)해서 구현할 수 있음

 

 

const calc = (value: numbercb: (number) => void): void => {

    let add = (ab) => a + b

    function multiply(ab) {return a * b}

 

    let result = multiply(add(12), value)

    cb(result)

}

calc(30(result: number) => console.log(`result is ${result}`)) 

 

(3) 고차 함수와 클로저, 그리고 부분 함수

- 고차 함수(high-order function)는 또 다른 함수를 반환하는 함수를 말함

- 고차 함수의 구조는 1차적으로 함수를 호출하고 그 내부에 또 그 함수를 반환하는 함수값을 만들어 호출하는 방식이다.

 

4-5) 함수 구현 기법

(1) 매개변수 기본값 지정하기

- 선택적 매개변수는 항상 그 값이 undefined로 고정됨.

- 함수 호출 시 인수를 전달하지 않더라도 매개변수에 어떤 값을 설정하고 싶다면 매개변수의 기본값을 지정할 수 있으며 이를 디폴트 매개변수라고 함

(매개변수: 타입 = 매개변수 기본값)

export type Person = {name: stringage: number}

 

export const makePerson = (name: stringage: number = 10): Person => {

    const person = {name: name, age: age}

    return person

}

console.log(makePerson('Jack'))

console.log(makePerson('Jane'33))

 

예시처럼 1번째 makePerson functionage값을 전달받지 못했으므로 default 값인 10이 출력된다.

출력값:

{ name: 'Jack', age: 10 }

{ name: 'Jane', age: 33 }

 

 

(2) 객체 생성 시 값 부분을 생략할 수 있는 타입스크립트 구문

- 타입스크립트는 다음처럼 매개변수의 이름과 똑같은 이름의 속성을 가진 객체를 만들 수 있음

const makePerson = (name: stringage: number) => {

    const person = {nameage// {name: name, age: age} 단축표현

}

 

 

(3) 객체를 반환하는 화살표 함수 만들기

- 화살표 함수에서 객체를 반환하고자 할때는 화살표 뒤에 중괄호{}만 쓸 것이 아니라 객체를 소괄호()로 감싸주어야 함.

(그렇지 않을경우, 객체가 아닌 복합 실행문으로 해석)

 

위 예시를 다음과 같이 표현 가능하다.

export type Person = {name: stringage: number}

 

export const makePerson = (name: stringage: number = 10): Person => ({nameage})

console.log(makePerson('Jack'))

console.log(makePerson('Jane'33))

 

(4) 매개변수에 비구조화 할당문 사용하기

- 함수의 매개변수 또한 변수의 일종이므로 매개변수에 비구조화 할당문을 적용할 수 있다.

 

export type Person = {name: stringage: number}

 

const printPerson = ({nameage}: Person): void =>

    console.log(`name: ${name}, age: ${age}`)

 

printPerson({name: 'Jack', age: 10})

 

(5) 색인 키와 값으로 객체 만들기

- ESNext 자바스크립트에서 색인 키, 값으로 객체만들 떄는 다음과 같은 코드로 작성 가능

Const makeObject = (key, value) => ({[key]: value})

 

- 타입스크립트에서 {[key]: value} 형태의 타입을 색인 기능 타입이라고 하며, 다음과 같은 형태로 key, value의 타입을 명시함

Type KeyType = {

           [key: string]: string

}

 

4-6) 클래스 메서드

(1) function 함수와 this 키워드

- function 키워드로 만든 함수는 Function이란 클래스의 인스턴스이며, 함수는 객체임

- 객체지향 언어에서 인스턴스는 this 키워드를 사용할 수 있음. (, 화살표 함수에서는 this 키워드를 사용할 수 없음)

 

(2) 메서드란?

- 타입스크립트에서 메서드(method)function으로 만든 함수 표현식을 담고 있는 속성

- 예시에서 클래스 Avaluemethod라는 두 개의 속성을 가지며, value에는 1, method() => void 타입의 함수 표현식을 설정

export class A {

    value: number = 1

    method: () => void = function(): void {

        console.log(`value: ${this.value}`)

    }

}

A라는 클래스를 구성하고,

 

import {Afrom './A'

let a: A = new A

a.method()

 

A import해 새로운 변수 a에다가 A 객체를 삽입한 후, a에 있는 method 속성을 호출하면,

Method 속성은 그 객체에 있는 value속성값을 호출해 출력하므로 value: 1이라고 표현된다.

 

(3) 클래스 메서드 구문

- 타입스크립트는 클래스 속성 중 함수 표현식을 담는 속성은 function 키워드를 생략이 가능한 단축 구문(shorthand)를 제공함

단축 구문을 적용해 A클래스를 구현한 예시 B

export class B {

    constructor(public value: number = 1) {}

    method(): void {

        console.log(`value: ${this.value}`)

    }

}

 

(4) 정적 메서드

- 클래스 속성은 static 수정자를 속성 앞에 붙여서 정적으로 만들 수 있었으며, 메서드 또한 속성이므로 이름 앞에 static 수정자를 붙여 정적 메서드를 만들 수 있음

(5) 메서드 체인

- 제이쿼리(jQuery)와 같은 라이브러리는 객체의 메서드를 이어서 계속 호출하는 방식으로 작성할 수 있으며, 이를 메서드 체인(method chain)이라고 함

- 메서드 체인을 구성하려면 항상 this를 반환해야함.

export class calculator {

    constructor(public value: number = 0) {}

    add(value: number) {

        this.value += value

        return this

    }

    multiply(value: number) {

        this.value *= value

        return this

    }

}

 

import {calculatorfrom './method-chain'

 

let calc = new calculator

let result = calc.add(1).add(2).multiply(3).multiply(4).value

console.log(result)

결괏값 : 36

(1+2*3*4) <- 순서대로 실행함.

 

 

  Comments,     Trackbacks