사실 완전한 FP라는 것은 운용가능하진 않습니다. IO가 항상 발생하기 때문입니다.

DB, disk, rest api, grpc 등등 IO를 유발하는 작업들을 사용하기 때문입니다.
이러한 것들을 순수하지 않은 함수들(impure)이라고 하는데 부작용(side effect) 를 만들기 때문입니다.

이러한 부작용을 제어하려면 FP인 부분과 아닌 부분을 나눌 필요가 있습니다.
IO 경계(boundary)와 서비스와 도메인 로직을 분리합니다.
모든 것이 정확하게 분리가 되지는 않지만, 도메인 로직과 IO 경계를 분리하는 것이 핵심입니다.

다음 예시에서는 express에서 res, req를 받는 부분과, domain로직이 들어가는 부분, 여러 infra를 활용하여 pipeline을 만드는 service, 직접적으로 IO를 유발하는 infra로 나뉜 예시입니다.

flowchart TD
    %% Node Definitions
    Infra["<b>[ Infrastructure Effects ]</b><br/>HTTP / DB / FS / Cache / MQ<br/><br/><i>IO boundary using TaskEither</i>"]

    App["<b>[ Application Services ]</b><br/><br/><i>Composed effectful pipelines</i>"]

    Domain["<b>[ Domain Logic ]</b><br/><br/><i>Pure functions, ADTs, rules</i>"]

    %% Relationships
    Infra <-->|"DI (Functional)"| App
    App <-->|"pure values"| Domain

    %% Styling
    %% Red/Orange for Side Effects/Infrastructure
    classDef dirty fill:#ffe6e6,stroke:#b30000,stroke-width:2px,color:#000;
    %% Yellow/Neutral for Orchestration
    classDef glue fill:#fff9c4,stroke:#fbc02d,stroke-width:2px,color:#000;
    %% Green for Pure/Safe Domain
    classDef pure fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px,color:#000;

    %% Apply Classes
    class Infra dirty
    class App glue
    class Domain pure

여기서 핵심은 의존성은 반드시 한 방향으로만 흘러야 합니다.

상화에 따라 이름은 다르지만.
domain 혹은 library는 fp의 형태를 가진 코드들로 구성되며, service를 호출하여 사용합니다.
보통 service는 singleton인 경우가 많습니다.

infra는 io를 유발하는 경계에 위치하며, 이것이 외부와 상호작용을 대신 처리해 줍니다.

flowchart LR
    %% Nodes
    Infra["<b>[ Infrastructure ]</b><br/><i>Impure / Outer Shell</i>"]
    App["<b>[ Application ]</b><br/><i>Orchestration Layer</i>"]
    Domain["<b>[ Domain ]</b><br/><i>Pure Core / Inner</i>"]

    %% The Dependency Rule (Source Code Imports)
    %% In functional architecture, the outer layer depends on the inner layer.
    Infra -->|imports / depends on| App
    App -->|imports / depends on| Domain

    %% Styling to match previous color coding
    classDef dirty fill:#ffe6e6,stroke:#b30000,stroke-width:2px;
    classDef glue fill:#fff9c4,stroke:#fbc02d,stroke-width:2px;
    classDef pure fill:#e8f5e9,stroke:#2e7d32,stroke-width:2px;

    class Infra dirty
    class App glue
    class Domain pure

    %% Arrow Styling (Thick to emphasize strict rule)
    %% linkStyle 0,1 stroke-width:3px,fill:none,stroke:#333;

모든 FP 코드베이스가 이렇게 되는 것은 아닙니다.

제가 작업을 했던 코드베이스에서는 사용자의 요청에 관한 정보가 있는 graphql layer과 사용자 정보 없이 로직만 담당하는 lib, 그리고 io를 유발하는 service로 나누어서 분리를 했습니다.

단순한 예시

src/
 ├─ domain/
 │   ├─ model.ts           (ADTs & types)
 │   ├─ rules.ts           (Pure business logic)
 │   └─ validation.ts      (Pure validators)
 │
 ├─ app/
 │   └─ userService.ts     (TaskEither pipelines)
 │
 ├─ infra/
 │   ├─ dbRepo.ts          (TaskEither DB access)
 │   └─ httpClient.ts      (TaskEither external API)
 │
 └─ http/
     └─ userController.ts  (Express handlers)

예시

// Controller (Express)
const activateUserHandler = (req, res) =>
  activateUser(req.params.id)().then(result =>
    result._tag === "Right"
      ? res.status(200).json(result.right)
      : res.status(400).json({ error: result.left })
  );


// Domain
type ActivateError = "Undersage" | "AlreadyInactive";

const validateActivation = (u: User): Either<ActivateError, User> =>
    u.age < 18 ? left("Underage")
    : u.status !== "active" ? left("AlreadyInactive")
    : right(u)

// Application
const activateUser = (id: string): TaskEither<AppError, User> =>
    pipe(
      findUser(id),
      flatMapTE(user =>
          validateActivation(user)._tag === "Left"
                ? leftTE({ type: "DOMAIN", reason: validateActivation(user).left })
                : rightTE(user)
                  ),
      flatMapTE(saveUser)
        );


// Infra
const findUser = (id: string) : TaskEither<InfraError, User> => ...;
const saveUser = (u: User): TaskEither<InfraError, User> => ...;

이번엔 비동기 실행을 FP 스타일로 하는 법을 알아봅시다.

원래 ts에서는 Promise를 사용해서 비동기 실행을 했습니다.

하지만 Promise에는 FP에서 사용하기에는 몇가지 단점이 따릅니다.

  • 즉시 실행 : 제어되지 않고 즉시 실행됩니다.
  • 취소 불가
  • sync코드와의 합칠 때 생기는 불편함 : await, then 같은 함수들과 같이 사용해야 합니다.
  • 합성의 어려움

Task는 다음과 같은 특성으로 위의 단점을 극복합니다.
- 지연 실행
- 합성 가능
- 순수 값
- 지연 실행으로 인한 부작용을 지연 가능

Task의 정의.

export type Task<A> = () => Promise<A>;

Constructors

    export const of = <A>(value: A): Task<A> =>
        () => Promise.resolve(value);

    export const fromPromise = <A>(f: () => Promise<A>): Task<A> =>
        () => f();

유틸리티

export const map = 
      <A, B>(f : (a: A) => B) =>
      (task: Task<A>): Task<B> =>
      () => task().then(f);

export const flatMap = 
      <A, B>(f: (a: A)=> Task<B>) =>
      (task: Task<A>): Task<B> =>
      () => task().then((a) => f(a)());
// e.g
task().then(...)

예시

const fetchUser: Task<User> = () =>
    fetch("/api/user").then(res => res.json());

const updateName = (u: User): User => ({ ...u, name: u.name.toUpperCase() });

const saveUser = (u: User): Task<string> => () =>
    db.save(u).then(() => "OK");

const pipeline = flow(
  fetchUser,
  flatMap(u => of(updateName(u))),
  flatMap(saveUser),
  );

pipeline().then(console.log);

TaskEither

Either을 Task에서 사용하기 위한 방법입니다.

export type TaskEither<L, R> = Task<Either<L, R>>;

비동기 연산이 L으로 실패하거나 R으로 성공하는 것을 나타냅니다.

생성자.

export const rightTE = <R, L = never>(r: R) : TaskEither<L, R> =>
    () => Promise.resolve(right(r));
export const leftTE = <L, R = never>(l: L): TaskEither<L, R> =>
    () => Promise.resolve(left(l));

export const tryCatch = <L, R>(
  f: () => Promise<R>,
  onError: (e: unknown) => L
): TaskEither<L, R> =>
    () =>
    f.().then(right).catch((e) => left(onError(e)));

유틸리티

export const mapTE =
      <R, R2>(f: (r: R) => R2) =>
      <L>(te: TaskEither<L, R>): TaskEither<L, R2> =>
      () => te().then(e => e._tag === "Right" ? right(f(e.right)) : e);

export const mapLeftTE =
      <L, L2>(f: (l: L) => L2) =>
      <R>(te: TaskEither<L, R>): TaskEither<L2, R> =>
      () => te().then(e => e._tag === "Left" ? left(f(e.left)) : e);

export const flatMapTE = 
      <R, L, R2>(f: (r: R) => TaskEither<L, R2>) =>
      (te: TaskEither<L, R>): TaskEither<L, R2> =>
      () => te().then(e => e._tag === "Right" ? f(e.right)() : e);

여러가지 예시들

const fetchJson = (url: string): TaskEither<string, any> =>
    tryCatch(
        () => fetch(url).then((r) => r.json()),
        () => "netwokr_error"
        );

const retry = 
    (attempts: number) =>
    <L, R>(te: TaskEither<L, R>): TaskEither<L, R> =>
    () =>
        te.().then((e) => {
            if (e._tag === "Right" || attemtps <= 0 ) return e;
            return retry(attempts -1)(te)();
         });

const task = flow(fetchJson, retry(3))("https://api.example.com/data");

task().then(console.log);
const findUser = (id: string): TaskEither<string, User> =>
    tryCatch(
    () => db.queryUser(id),
    () => "db_failure"
);


const validate = (u: User): Either<string, User> =>
    u.active ? right(u) : left("inactive_user");

const validateTE = <L, R>(f: (r: R) => Either<L, R>) =>
    flatMapTE((r: R) => rightTE(r)._tag ? tryCatch(() => Promise.resolver(r), () => f(r)) : leftTE(f(r)));


const validateUser = flatMapTE((u: User) =>
    validate(u)._tag === "Right" > rightTE(u) : leftTE(validate(u).left)
);
const handleGetUser = async (req, res) =>
    pipe(
        findeUser(req.params.id),
        flatMapTE(u => validate(u)._tag === "Right" ? rightTE(u) : leftTE("inactive")),
        mapTE(user => ({ id: user.id, name: user.name }))
        )().then(result => {
            if (result._tag === "Right") {
                res.status(200).json(result.right);
            } else {
                res.status(400).json({ error: result.left });
            }
        });
// Sequential
const result = await flow(task1, flatMap(task2))();
// Parallel
const parallel = <A>(tasks: Task<A>[]): Task<A[]> =>
    () => Promise.all(tasks.map(t => t()));

이번 게시글에서는 Either ( 혹은 Result)에 대해서 다루겠습니다.

FP에서는 exception, null을 선호하지 않기 때문에 오류 처리를 조금 다른 방식을 차용해야합니다.

try { 
  const user = await getUser(id);
  doSomething(user);
} catch (e) {
  console.error(e);
}

위와 같은 코드의 단점

  • 에러가 발생 지점과 먼 곳에서 처리됩니다.
  • 오류 발생하는 함수들을 합성하기 어려워집니다.
  • 타입을 보고 어떤 함수가 오류를 내는지 추론하기 어렵습니다.

타입 선언

type Left<L> = { _tag: "Left"; left: L};
type Right<R> = { _tag: "Right"; right: R};

export type Either<L, R> = Left<L> | Right<R>;

생성자

export const left = <L, R = never>(l: L): Either<L, R> => ({
  _tag: "Left",
  left:l,
});

export const right = <R, L = never>(r: R): Either<L, R> => ({
  _tag: "Right",
  right: r,
});

Either 관련 유틸리티 함수들

// 원래는 either을 만드는 함수가 아니지만 either type으로 만들어주는 경우
export const map = 
      <R, R2>(f: (r:R) => R2) =>
      <L>(e: Either<L, R>): Either<L, R2> =>
          e._tag === "Right" ? right(f(e.right)) : e;

export const mapLeft = 
      <L, L2>(f: (l:L) => L2) =>
      <R>(e: Either<L, R>): Either<L2, R> =>
        e._tag === "Left" ? left(f(e.left)) : e;


// f가 원래 either을 결과로 반출하는 함수일 때
export const flatMap = 
      <R, L, R2>(f: (r: R) => Either<L, R2>) =>
      (e: Either<L, R>): Either<L, R2> =>
        e._tag === "Right" ? f(e.right) : e;

export const getOrElse = 
    <L, R>(fallback: (l: L) => R ) =>
    (e: Either<L, R>): R=>
        e._tag == "Right" ? e.right :fallback(e.left);


export const fold =
    <L, R, B>(onLeft: (l : L) => B, onRight: (r: R) => B) =>
    (e: Either<L, R>):B =>
        e._tag === "Left" ? onLeft(e.left) : onRight(e.right);

예시들

function parseAge(s: string): number{
    const n = Number(s);
    if (isNaN(n)) throw new Error("Invalid number");
    return n;
}

const parseAge = (s:string): Either<string, number> => {
    const n  =Number(s);
    return isNaN(n) ? left("Invalid number") : right(n);
}



const nonEmpty = (s: string): Either<string, string> =>
    s.trim() === "" ? left("Empty") : right(s);

const mustBeNumber = (s: string): Either<string, number> => {
    const n = Number(s);
    return isNaN(n) ? left("NaN") :right(n)


};

export const flow = <A, B, C, D>(
    f: (a: A) => B,
    g: (b: B) => C,
    h: (c: C) => D
) => (a: A): D => h(g(f(a)));


const parsePositiveNumber = flow(
    nonEmpty,
    flatMap(mustBeNumber),
    flatMap((n) => (n > 0 ? right(n) : left("Not positive")))
);

type AuthError =
  | { type: "UserNotFound"; email: string}
  | { type: "InvalidPassword"; attempts: number};

const authenticate = (email: string, pwd: string): Either <AuthError, User> =>{
    const user = db.findUser(email);
    if (!user) return left({ type:"UserNotFound",email});
    if (!verifyPassword(pwd, user.hash)){
        return left({ type: "InvalidPassword", attempts: user.attempts +1});

    }
    return right(user);
}

const saftParse = (text: string): Either<string, any> => {
    try {
        return right(JSON.parse(text))
    } catch (e) {
        return left("Invalid JSON")
    }
}

데이터를 두 가지로 나뉘어서, 함수에서 다른 함수로 데이터를 넘겨줄 때에 문제 없는 값과 문제있는 값으로 분류하여 주는 방식입니다. 이렇게 하면 error을 raise하면서 생기는 복잡한 제어 흐름을 사용하지 않고 처음부터 끝까지 이어서 제어흐름이 있게 됩니다.

type Option<T> = 
  | { _tag: "Some"; value: T}
  | { _tag: "None" };

FP에서는 some과 none이라는 단어를 사용합니다.
말 그대로 Some은 어떤 값이 있다는 말이고, None은 값이 없을 때 사용합니다.

nullundefined보다는 명확한 단어를 통해서
오류 제거, 가독성, 디버깅 개선 등을 할 수 있습니다.

null을 사용하지 않고, 타입을 명시하여 타입 네로잉을 할 수 있습니다.

const user = db.findUser(id);
if ( user == null) {
}

const maybeUser: Option<User> = findUser(id);

switch (mmaybeUser._tag) {
  case "Some": ...
  case "None": ...
}

자주 사용하는 option에 대한 함수들.

// 타입 정의
type None = {_tag: "None"};
type Some<T> = { _tag: "Some"; value: T};

export type Option<T> = None | Some<T>;


// 생성자 constructor 
export const none: Option<never> = { _tag: "None" };
// never은 모든 타입의 하위 타입이기 때문에 모든 경우에 none을 사용할 수 있습니다.

export const some = <T>(value: T): Option<T> => ({
  _tag: "Some",
  value,
});



// nullable 값 변경
export const fromNullable = <T>(value: T | null | undefined): Option<T> =>
    value == null ? none : some(value);

// undefined == null   // true
// 따라서 undefined와 null 모두 none으로 만들 수 있습니다.

옵션 관련 유틸리티

map

export const map = 
  <A, B>(f: (a: A) => B) =>
  (opt: Option<A>): Option<B> =>
    opt._tag === "Some" ? some(f(oopt.value)) : none;

map((x) => x + 1)(some(5));        // Some(6)
map((x) => x + 1)(none):         // none

flatMap (bind)

export const flatMap = 
  <A, B>(f: (a: A) => Option<B> ) =>
  (opt: Option<A>): Option<B> =>
      opt._tag === "Some" ? f(opt.value) : none;
// Option을 받지 않는 함수를 Option을 받는 함수로 변경

getOrElse

export const getOrElse =
  <A>(fallback: A) =>
  (opt: Option<A>): A =>
    opt._tag === "Some" ? opt.value : fallback;

// Define the strategy once
const defaultToZero = getOrElse(0); 

// Apply to different contexts
const val1 = defaultToZero(some(10)); // Returns 10
const val2 = defaultToZero(none);     // Returns 0

fold

export const fold = 
  <A, B>(onNone: () => B, onSome: (value: A) => B) =>
  (opt: Option<A>): B =>
    opt._tag === "Some" ? onSome(opt.value) : onNone();

type User = { id: number; name: string };
type HttpResponse = { status: number; body: string };

const userOpt: Option<User> = some({ id: 1, name: "Alice" });

// We fold the Option<User> into an HttpResponse
const response = fold(
  // onNone: Handle the 404 case
  () => ({ status: 404, body: "User not found" }),

  // onSome: Handle the 200 case and format data
  (u: User) => ({ status: 200, body: `Hello, ${u.name}` })
)(userOpt);
  export const isSome =<A>(opt: Option<A>): opt is Some<A> =>
    opt._tag === "Some";
  export const isNone = <A>(opt: Option<A>): opt is None =>
    opt._tag === "None";



const options: Option<number>[] = [some(1), none, some(2)];

// LOGICAL ERROR in Types:
// The runtime result is correct, but TypeScript still thinks
// the result is 'Option<number>[]', not 'Some<number>[]'.
const filtered = options.filter(opt => opt._tag === "Some");

// This causes a compile error because TS thinks 'opt' might still be 'None'
// filtered[0].value; // Error: Property 'value' does not exist on type 'Option<number>'


// Correct Type Inference:
// TypeScript effectively performs: Array<Option<T>> -> Filter -> Array<Some<T>>
const cleanValues = options.filter(isSome); 

// Now strictly typed as 'Some<number>[]'
console.log(cleanValues[0].value); // Safe access, no error.

비교

const port = process.env.PORT || "3000";
const port = pipe(
  fromNullable(process.env.PORT),
  getOrElse("3000")
);

if (!row.age) return 0;
pipe(
  fromNullable(row.age),
  getOrElse(0)
);



Algebraic Data Types

대수적 자료형이란?

연산을 통해서 정의하는 자료형들을 대수적 자료형이라고 부릅니다.
대표적으로 두가지 방식이 있습니다.

곱 자료형, Product Types, AND
합 자료형, Sum Types, OR

곱 자료형 Produce Types, AND Types

AND 즉, 모든 값들이 있어야 정의되는 자료형을 product type이라고 합니다.
예를 들어서 다음과 같이 각각의 값들을 모아서 만드는 자료형 입니다.

type User =  {
  id: string;
  name: string;
  age: number;
};

합 자료형 Sum Types, OR Types

여러가지의 경우의 수 중 하나를 표현하는 것을 합 자료형이라고 합니다.
Union | 을 사용해서 표현합니다.

type Role = "admin" | "user" | "guest";

type Shape = 
    | { type: "Circle"; radius: number};
    | { type: "Rect"; width: number; height: number};

위 예시는 정의된 것 중 하나의 값만 가질 수 있습니다.

물론 합 자료형은 둘 다를 포함하는 (xor이 아닌 or)을 표현할 수 있습니다.

type LooseShape =
    | { radius: number }
    | { width: number; height: number };

const hybridShape: LooseShape = {
  radius: 10,
  width: 50,
  height: 50
};

Discriminated Unions.

구별된 유니온, 타입 이름을 명시하는 합 자료형입니다.
타입 네로잉에서 유용하게 사용됩니다.

type Circle = { type: "Circle"; radius: number };
type Rect = { type: "Rect"; width: number; height: number };

type Shape = Circle | Rect

패턴 매칭.

typescript는 '완벽'한 FP 언어는 아니기 때문에 패턴 매칭을 따로 할 필요가 있습니다.
이럴 때에는 모든 경우의 수에 대한 스위치를 씁니다.


function area(shape: Shape): number {
  switch (shape.type) {
    case "Circle":
      return Math.PI * shape.radius ** 2;
    case "Rect":
      return shape.width * shape.height;
    default:
      const _exhausitve: never = shape;
      return _exhausitive;
  }
}

_exhaustive: never = shape;가 에러를 발생시키기 때문에 모든 경우의 수에 대응할 수 있습니다.

여러가지 예시들

이러한 곱 자료형을 사용하면
함수의 값이 여러가지가 나올 수 있을 때에 한번에 처리할 수 있습니다.
try-catch문을 사용하지 않고 결과 값을 둘 중 하나로 만들어 버릴 수 있습니다.

function parse(input: string): any {
    try { ...}
    catch (e) { ... }
}

type ParseResult = 
  | { type: "Success"; value: any }
  | { type: "Error"; message: string }

함수가 인자로 받아야 하는 값이나, 반환하는 값들을 합 자료형으로 표현하여 여러가지 케이스에 대해서 대응할 수 있습니다.



type ApiResponse<T> =
  | { status: "success"; data: T }
  | { status: "error"; message: string }
  | { status: "loading" };


type AuthError = 
 | { type: "UserNotFound"; email: string}
 | { type: "InvalidPassord"; attempts: number}
 | { type: "LockedAccount" }


type AuthResult = 
  | { type: "Success"; userId: string }
  | { type: "InvalidCredentials" }
  | { type: "EmailNotVerified"; email: string }
  | { type: "ServerError"; message: string };

function handleAuth(result: AuthResult) {
  switch (result.type) {
    case "Success":
      return { status: 200, userId: result.userId };

    case "InvalidCredentials":
      return { status: 401 };

    case "EmailNotVerified":
      return { status: 403, message: result.email };

    case "ServerError":
      return { status: 500, error: result.message };

    default:
      const _exhaustive: never = result;
      return _exhaustive;
  }
}

Currying이라는 단어가 익숙치 않을텐데, 이는 사람이름 입니다.

Haskell Curry

Curring

Currying은 여러개의 인자를 받는 함수를 하나의 인자만 받는 함수로 바꾸고, 이들을 하나의 체인으로 연결하는 것을 뜻합니다.

const add = (a: number, b: number): number => a + b;
add(1, 2);

const addC = (a: number ) => (b: number) : number => a + b;
addC(1)(2);
const multiply = (factor: number) => (x: number) => factor * x;
const double = multiply(2);
doulbe(10);

Partial Application

부분적용

부분 적용은 함수의 인자가 여러 개 일때 이 인자 중 일부분을 미리 넣어 함수의 인자를 줄이는 방법입니다.

부분적용도 커리와 비슷하게 보일 수도 있는데 커리는 꼭 하나만 남겨야 하며, 함수들을 연결해야합니다.

const add = (a: number, b: number): number => a + b;

const add10 = (b: number) => add(10, b);
add10(5);

const bseAreaFromLength2 = (width: number, height: number): number => {
  return calculateVolume(2, width, height);
};

const bindVolume = calculateVolume.bind(null, 2);
console.log(baseAreaFromLength2(3, 4));  // 24
console.log(bindVolume(3,4)); // 24

여러가지 패턴들

// Function Generators
const multiplyBy = (factor: number) => (x: number) => x * factor;

// Predicate Generators
const hasLength = 
      (len: number) =>
      (s: string) =>
        s.length === len;

// Data Transformers
const prefixAll = 
      (prefix: string) =>
      (list: string[]) =>
        list.map(x => prefix + x);

여러가지 예시들

const fetchWithBase = 
      (base: string) =>
        (path: string) =>
        fetch(base + path);
const apiFetch = fetchWithBase("https://api.example.com");
apiFetch("/users");


const withHeader = 
  (name: string, value: string) =>
  (handler: Handler) =>
    async (req: Req) => {
      req.headers[name] = value;
      return handler(req);
    };

Currying을 통한 point-free 스타일

const trim = (s: string) => s.trim();
const lower = (s: string) => s.toLowerCase();

// pointful 
const normalize = (s: string) => lower(trim(s));

// point-free
const normalize = flow(trim, lower);

함수 합성 Function Composition

함수의 결과를 다른 함수의 인자로 넣어서 하나의 새로운 함수를 만드는 것을 뜻합니다.
수식으로 표현하면 다음과 같습니다.

$$ g \circ f = x \rightarrow g(f(x)) $$

const f = (x: number) => x + 1;
const g = (x: number) => x * 2;
const h = (x: number) => g(f(x));

Compose

오른쪽에서 왼쪽으로 함수를 합성하는 함수입니다.

const compose = 
      <A, B, C>(g: (b: B) => C, f: (a: A) => B) =>
      (a: A): C =>
          g(f(a));

const add1 = (x: number) => x + 1;
const double = (x: number) => x* 2;

const add1ThenDouble = compose(double, add1);
add1ThenDouble(5); // 12

Pipe

왼쪽에서 오른족으로 함수를 합성하는 함수입니다.
파이프는 함수를 반환하진 않고 바로 실행합니다.

const pipe = 
      <A>(a: A) =>
        <B>(ab: (a: A) => B) =>
        <C>(bc: (b: B) => C) =>
          bc(ab(a));


pipe(5)(add1)(double); //12

Flow

왼쪽에서 오른쪽, 단 함수를 반환하는 함수입니다.

const flow = 
      <A, B, C>(ab: (a: A) => B, bc: (b: B) => C) =>
      (a: A): C =>
          bc(ab(a));

const transform = flow(add1, double);
transform(5); // 12

함수 합성의 장점

일반적인 명령형 프로그래밍에서는 한줄 한줄 보내야 할 것을,
FP에서는 한번에 알아보기 쉽게 만들 수 있습니다.

const result1 = parse(input);
const result2 = validate(result1);
const result3 = save(result2);


// FP style

const process = flow(parse, validate, save);
const result = process(input);

이렇게 값 변화를 하나하나 명령형으로 만들지 않고 프로세스를 함수 합성으로 만드는 것을 포인트 프리(point free) 스타일이라고 합니다.

예시들


const nonEmpty = (s: string) => {
  if (s.trim() === "") throw new Error("Empty")
  return s;
}

const validEmail = (s:string) => {
  if(!s.includes("@")) throw new Error("Invalid email");
  return s;
};

const normalizeEmail = flow(nonEmpty, validEmail, s => s.toLowerCase());

//


const withAuth =
  (handler: (req: Req) => Res) =>
  (req: Req): Res => {
    if (!req.user) throw new Error("Unauthenticated");
    return handler(req);
  };

const withLog =
  (handler: (req: Req) => Res) =>
  (req: Req): Res => {
    console.log(req.method, req.path);
    return handler(req);
  };

const handler = (req: Req): Res => ({ ok: true });

const finalHandler = flow(withAuth, withLog)(handler);

포인트 프리 스타일

정의역(domain) \(X\) 에서 공역(codomain) \(Y\)으로 가는 함수 \(f\) 가 있을 때


$$ f : X \rightarrow Y $$


\(x \in X\) 인 점 \(x\)를 \(X\)에 있는 점(point)라고 합니다.

point-ful한 함수의 정의는 함수의 정의에 \(x\)가 들어가는 것을 뜻합니다.


$$h(x) = g(f(x))$$
같은 경우 \(f(x)\)의 정의역이 점으로 표현되고, \(f(x)\)의 결과이자 \(g\)의 정의역 또한 점 이기에 이러한 정의를
point-ful라고 합니다.

 

point-free한 함수의 정의는 반대로 \(x\)의 존재 없이 정의하는 것을 뜻합니다.
$$ h = g \circ f $$ 

 

  // Point-ful
  const getUserName = (user: User) => toUpper(getName(user));


  // Point-free
  import { flow } from 'fp-ts/function';

  const getUserName = flow(getName, toUpper);

일급 객체란?

일급 객체 함수

First Class citizen : 일급 객체
일급 객체 함수는 함수가 값처럼 사용될 수 있는 함수를 뜻합니다.

다음 네 가지가 가능합니다.
함수를 변수 처럼 저장 가능
함수의 인자로 함수를 전달
함수의 반환값으로 함수를 반환
리스트나 객체의 값에 함수를 넣는 것

변수 처럼 함수를 저장

const double = (x: number): number => x * 2;
const isEven = (x: number): boolean => x % 2 === 0;

객체 값에 함수를 삽입

const mathOps = {
    square: (x: number) => x * x,
    cube: (x: number) => x ** 3,
};

리스트에 함수 삽입

const ops = [
    (x: number) => x + 1,
    (x: number) => x * 2,
    (x: number) => x - 3,
];    

고차원 함수

Higher order functions
고차원 함수란 함수가 함수를 매개변수로 받거나 반환값이 함수인 것을 고차원 함수라고 합니다.

const applyTwice = <A>(f: (x: A) => A, value: A): A => f(f(value));

applyTwice( x= > x +1 , 5); // 7


const multiplier = (n: number) =>
    (x: number) => x * n;

const times3 = multiplier(3);

times3(10); // 30

기본 내장 고차원 함수들

[1,2,3].map(x => x* 2); // [2, 4, 6]

[1, 2, 3, 4].filter(x => x % 2 === 0); // [2, 4]


[1, 2, 3].reduce((acc, x) => acc + x , 0); // 6

예시들

const withLogging = <A, B>(fn: (x: A) => B) =>
  (x : A): B => {
    console.log("Input:",x);
    const result = fn(x);
    console.log("Output:", result);
    return result;
  };

const add1 = (n: number) => n + 1;
const loggedAdd1 = withLogging(add1)
loggedAdd1(10);

const withTiming = 
  <A, B>(fn: (a: A) => B) =>
  (a: A): B => {
    const start = performance.now();
    const result = fn(a)
    const end = performance.now();
    console.log(`Function took ${end - start} ms`);
    return result;
  };

const withRetry = (retires: number) =>
    async <A>(fn: () => Promise<A>): Promise<A> => {
      let attempt = 0;
      while(true) {
        try{
          return await fn();
        } catch (e) {
          if (++attempt > retires) throw e;
        }
      }
    };

type User = { id: string; active: boolean };
const users: User[] = [
  { id: "1", active: false },
  { id: "2", active: true },
  { id: "3", active: false },
];

const activate = (u: User): User => ({ ...u, active: true });

const activeUsers = users.map(activate);

+ Recent posts