이 포스팅은 DEV Community에서 참고하여 공부하면서 정리한 문서입니다. 현재 주 개발 언어로 사용 중인 스칼라 코드가 갑작스럽게 등장할 수 있습니다.
fp-ts
fp-ts
는 타입스크립트로 타입이 있는 함수형 프로그래밍을 할 수 있도록 제공하는 라이브러리이다.
타입스크립트에서 제공하지 않는 Option
, Either
, IO
, Task
, Functor
, Applicative
, Monad
등 함수형 타입을 제공한다.
타입 클래스 (Type Class)
타입 클래스란 구현하려는 기능을 나타내는 인터페이스 혹은 API 이다. 타입스크립트에서는 타입 클래스를 인터페이스로 작성할 수 있다.
먼저 동등성(equality)을 평가하는 기능을 가지고 있는 Eq
타입 클래스를 작성해보자.
1
2
3
interface Eq<A> {
readonly equals (x: A, y: A) => boolean
}
타입
A
는Eq
타입 클래스에 속해있으며equals
함수에 정의되어 있다.
스칼라에서는 아래와 같이 트레이트를 사용하여 작성할 수 있다.
1
2
3
trait Eq[A] {
def equals(x: A, y: A): Boolean
}
트레이트는 자바의 인터페이스와 비슷하지만 더 확장된 개념으로 자바에서는 인터페이스를 구현(implementation) 한다고 하지만 스칼라에서는 트레이트를 믹스인(mixin) 한다고 한다.
인스턴스 (Instance)
인스턴스는 타입 클래스의 구현을 제공하며 Eq
클래스 기준으로 A
타입에 대한 구현을 제공한다.
1
2
3
const eqNumber: Eq<number> = {
equals: (x, y) => x === y
}
eqNumber
인스턴스는 몇 가지 규칙을 지켜야 한다.
- 반사성(Reflexivity):
x
가 무엇이든equals(x, x) === true
이어야 한다. - 대칭성(symmetry):
equals(x, y) === true
라면equals(y, x) === true
이어야 한다. - 전이성(Transitivity):
equals(x, y) === true
,equals(y, z) === true
라면equals(x, z) === true
이다.
스칼라에서는 인스턴스를 이렇게 정의할 수 있다.
1
2
3
4
// implicit에 대해서는 아래에서 살펴보자...
implicit val eqNumber: Eq[Int] = new Eq[Int] {
override def equals(x: Int, y: Int): Boolean = x == y
}
다음과 같이 배열에 특정 요소가 존재하는지 평가하는 elem
함수를 정의할 수 있다.
1
2
3
4
5
6
function elem<A>(E: Eq<A>): (a: A, as: Array<A>) => boolean {
return (a, as) => as.some(item => E.equals(item, a))
}
elem(eqNumber)(1, [1, 2, 3]) // true
elem(eqNumber)(4, [1, 2, 3]) // false
elem
함수는Eq
타입 클래스를 구현한eqNumber
인스턴스를 사용하여as
배열에서a
가 존재하는지 평가한다.
이렇게 A
에 대한 Eq
인스턴스를 인자로 넘겨서 함수에서 동등성에 대한 평가를 할 수 있다. 이 예제는 단순한 예제일 뿐이다.
만약 특정 기능을 타입 클래스로 추상화 해두고 필요에 따라 구현해둔다면 로직의 재사용성과 일관성, 함수 합성 등 다양한 함수형 프로그래밍의 장점을 보게 될 것이다.
스칼라에서는 이렇게 할 수 있다.
1
2
3
4
5
6
def elem[A](a: A, as: Seq[A])(implicit E: Eq[A]): Boolean = {
as.exists(item => E.equals(item, a))
}
elem(1, Seq(1, 2, 3)) // true
elem(4, Seq(1, 2, 3)) // false
eqNumber
를 암시적(implicit)으로 정의했기 때문에 생략이 가능하다. 스칼라의 특징 중 하나이다.
복합 타입에 대한 Eq
타입 클래스의 인스턴스도 정의할 수 있다.
1
2
3
4
5
6
7
8
type Point = {
x: number
y: number
}
const eqPoint: Eq<Point> = {
equals: (p1, p2) => p1.x === p2.x && p1.y === p2.y
}
반사성을 고려한다면 아래와 같이 코드를 최적화 할 수 있다.
1
2
3
const eqPoint: Eq<Point> = {
equals: (p1, p2) => p1 === p2 || (p1.x === p2.x && p1.y === p2.y)
}
함수 조합기 (Combinator)
그런데 이렇게 모든 타입의 동등성 평가를 위한 인스턴스를 만드는 것은 너무 번거롭다.
Point
타입의 프로퍼티들은 모두 number
타입이고 우리는 이미 eqNumber
인스턴스를 구현했으므로 이를 사용하여 Point
타입의 동등성 비교를 하는 것이 좋겠다.
fp-ts
는 이런 것들을 하기 위한 함수 조합기(combinator)를 제공한다.
1
2
3
4
5
6
7
8
9
10
import {struct, Eq} from 'fp-ts/Eq'
const eqPoint: Eq<Point> = struct({
x: eqNumber,
y: eqNumber
})
const p1: Point = { x: 1, y: 2 }
const p2: Point = { x: 1, y: 2 }
eqPoint.equals(p1, p2) // true
참고 문서에서 사용한
getStructEq
는 deprecated 되었다. 사실Eq
도fp-ts
에서 제공하는 타입 클래스다.
Point
타입을 가지는 다른 복합 타입이 있다면 eqPoint
를 조합하여 Eq
인스턴스를 정의할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Vector = {
from: Point
to: Point
}
const eqVector: Eq<Vector> = struct({
from: eqPoint,
to: eqPoint
})
const v1: Vector = {
from: { x: 1, y: 2 },
to: { x: 1, y: 2 }
}
const v2: Vector = {
from: { x: 2, y: 4 },
to: { x: 1, y: 2 }
}
eqVector.equals(v1, v2) // false
fp-ts/Eq
패키지의 struct
를 사용하여 이렇게 Eq
인스턴스를 조합할 수 있었다.
배열을 위한 Eq
인스턴스를 만들기 위한 함수 조합기도 제공한다.
1
2
3
4
5
6
7
import {getEq} from 'fp-ts/Array'
const eqArrayOfPoints: Eq<Array<Point>> = getEq(eqPoint)
const points1: Array<Point> = [{x: 1, y: 1}, {x: 2, y: 2}]
const points2: Array<Point> = [{x: 1, y: 1}, {x: 2, y: 2}]
eqArrayOfPoints.equals(points1, points2) // true
만약 어떤 타입이 있을 때 특정 프로퍼티의 동등성 비교로 해당 타입의 동등성을 평가하고 싶다면 contramap
함수 조합기를 사용할 수 있다.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {contramap, Eq} from 'fp-ts/Eq'
import {Eq as eqNumber} from 'fp-ts/number' // 기본 타입의 Eq 인스턴스도 fp-ts에서 기본 제공한다.
type User = {
userId: number
name: string
}
// userId를 평가하여 User의 동등성을 비교한다.
const eqUser: Eq<User> = contramap((user: User) => user.userId)(eqNumber)
const u1 = { userId: 1, name: 'alan' }
const u2 = { userId: 1, name: 'baegoon' }
eqUser.equals(u1, u2) // true
const u3 = { userId: 1, name: 'Bae Sangwoo' }
const u4 = { userId: 2, name: 'Ryu Songyi' }
eqUser.equals(u3, u4) // false
특정 프로퍼티를 비교한다고 설명했지만 정확한 의미는 아니다. contramap
의 시그니처를 보자.
1
const contramap: <A, B>(f: (b: B) => A) => (fa: Eq<A>) => Eq<B>
B
에 대한 동등성 평가를 할 때 특별한 평가 규칙이 있다면 규칙으로 사용될 함수 f
를 제시하고 그 반환 타입인 A
를 평가하는 Eq
인스턴스로 동등성을 평가한다.
다른 예로 위에서 작성한 Vector
타입의 다른 규칙의 Eq
인스턴스를 만들어보자.
1
2
3
4
5
// Vector 타입의 from 프로퍼티만 비교한다. -> eqPoint 인스턴스 사용
const eqVector2: Eq<Vector> = contramap((v: Vector) => v.from)(eqPoint)
// Vector 타입의 from 프로퍼티의 x, y의 합을 비교한다. -> eqNumber 인스턴스 사용
const eqVector3: Eq<Vector> = contramap((v: Vector) => v.from.x + v.from.y)(eqNumber)
contramap
에 대해서 지금은 이 정도로만 간단히 사용법을 알아보는 것으로 마무리하자. (더 많은 이해가 필요하다.)
이렇게 Eq
타입 클래스를 알아보면서 간단히 fp-ts
라이브러리에 대해 알아보고 함수형 프로그래밍에서 타입을 처리하는 법을 간단히 알아봤다.
다음은 대소 비교를 하는 Ord
타입 클래스에 대해 알아보자.