chengaofeng
发布于 2024-12-19 / 9 阅读
0
0

fp-ts的核心模块

核心模块介绍:

  1. Option

  • 用于处理可能为空的值

  • 主要类型:Some(值存在), None(值不存在)

  • 常用函数:

    • some() - 创建Some实例

    • none - 创建None实例

    • isSome() - 检查是否为Some

    • isNone() - 检查是否为None

import { pipe } from 'fp-ts/function';
import { some, none } from 'fp-ts/Option';
import { fold, map, getOrElse } from 'fp-ts/Option';

    console.log(
      pipe(
        some(5),
        map<number, number>(n => n * 2) // Some(10)
      )
    );

    console.log(
      pipe(
        none,
        getOrElse(() => 0) // 返回 0
      )
    );

    console.log(
      // fold: 处理 Some 和 None 两种情况
      pipe(
        some(5),
        fold(
          () => 'nothing', // None 的情况
          value => `got ${value}` // Some 的情况
        )
      )
    );

// 3. 实际应用示例
const findUser = (users: User[], id: number): Option<User> => {
  const user = users.find(u => u.id === id)
  return user ? some(user) : none
}
  1. Either

  • 用于处理可能出错的计算

  • 主要类型:Right(成功), Left(失败)

  • 常用函数:

    • right() - 创建Right实例

    • left() - 创建Left实例

    • isRight() - 检查是否为Right

    • isLeft() - 检查是否为Left

import { pipe } from 'fp-ts/function';
import { Either, left, right } from 'fp-ts/Either';
import { fold, map, getOrElse } from 'fp-ts/Either';

// 1. 创建 Either
const success = right('success'); // Right('success')
const failure = left('error'); // Left('error')

// map: 转换 Right 中的值
pipe(
  right(5),
  map(n => n * 2) // Right(10)
);

// getOrElse: 提供默认值
pipe(
  left('error'),
  getOrElse(() => 0) // 返回 0
);

// fold: 处理 Both Left 和 Right
pipe(
  right(5),
  fold(
    error => `Error: ${error}`, // Left 的情况
    value => `Success: ${value}` // Right 的情况
  )
);

// 3. 实际应用示例
interface User {
  id: number;
  name: string;
}

const validateUser = (input: unknown): Either<string, User> => {
  if (typeof input !== 'object' || input === null) {
    return left('Invalid input');
  }

  const user = input as any;
  if (typeof user.id !== 'number' || typeof user.name !== 'string') {
    return left('Invalid user format');
  }

  return right({ id: user.id, name: user.name });
};

// 使用示例
const result = validateUser({ id: 1, name: 'John' });
pipe(
  result,
  fold(
    error => console.error(error),
    user => console.log(`Valid user: ${user.name}`)
  )
);

主要区别:

  1. Option 用于处理"有值/无值"的情况

  2. Either 用于处理"成功/失败"的情况,且可以携带错误信息

实践建议:

  1. 当只需要处理值是否存在时,使用 Option

  2. 当需要处理错误并携带错误信息时,使用 Either

  3. 总是使用 pipe 操作来链式处理这些类型

  4. 善用 fold 来处理所有可能的情况

  1. Array

  • 数组相关的函数式操作

  • 常用函数:

    • map - 映射数组元素

    • filter - 过滤数组元素

    • fold - 折叠数组

import { pipe } from 'fp-ts/function';
import * as A from 'fp-ts/Array';

// 基础数据
const numbers = [1, 2, 3, 4, 5];
const users = [
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 },
  { id: 3, name: 'Charlie', age: 35 },
];

// 1. 基本操作
// map - 映射数组元素
console.log(
  pipe(
    numbers,
    A.map(n => n * 2)
  )
); // [2, 4, 6, 8, 10]

// filter - 过滤数组元素
console.log(
  pipe(
    numbers,
    A.filter(n => n % 2 === 0)
  )
); // [2, 4]

// reduce/fold - 折叠数组
console.log(
  pipe(
    numbers,
    A.reduce(0, (acc, n) => acc + n)
  )
); // 15

// 2. 数组转换
// flatten - 展平嵌套数组
console.log(
  pipe(
    [
      [1, 2],
      [3, 4],
    ],
    A.flatten
  )
); // [1, 2, 3, 4]

// chain (flatMap) - 映射并展平
console.log(
  pipe(
    numbers,
    A.chain(n => [n, n * 2])
  )
); // [1, 2, 2, 4, 3, 6, 4, 8, 5, 10]

// 3. 查找元素
// findFirst - 查找第一个匹配的元素
console.log(
  pipe(
    users,
    A.findFirst(user => user.age > 30)
  )
); // Option<User>

// findLast - 查找最后一个匹配的元素
console.log(
  pipe(
    users,
    A.findLast(user => user.age > 30)
  )
); // Option<User>

// 4. 数组验证
// every - 检查是否所有元素都满足条件
console.log(
  pipe(
    numbers,
    A.every(n => n > 0)
  )
); // true

// some - 检查是否存在元素满足条件
console.log(
  pipe(
    numbers,
    A.some(n => n > 4)
  )
); // true

// 5. 排序
// sort - 按指定比较函数排序
console.log(
  '排序',
  pipe(
    users,
    A.sort<(typeof users)[0]>({
      compare: (a, b) => (a.age < b.age ? -1 : a.age > b.age ? 1 : 0),
      equals: (a, b) => a.age === b.age,
    })
  )
);

// sortBy - 按多个条件排序
console.log(
  pipe(
    users,
    A.sortBy<(typeof users)[0]>([
      {
        compare: (a, b) => (a.age < b.age ? -1 : a.age > b.age ? 1 : 0),
        equals: (a, b) => a.age === b.age,
      },
      {
        compare: (a, b) => (a.name.localeCompare(b.name) > 0 ? -1 : 1),
        equals: (a, b) => a.name === b.name,
      },
    ])
  )
);

// partition - 分割数组
console.log(
  pipe(
    users,
    A.partition(user => user.age > 30)
  )
); // 返回 [满足条件的数组, 不满足条件的数组]

// 7. 组合操作示例
// 查找年龄大于30的用户,获取他们的名字,并按字母顺序排序
console.log(
  '组合示例',
  pipe(
    users,
    A.filter(user => user.age > 30),
    A.map(user => user.name),
    A.sort<string>({
      compare: (a, b) => (a.localeCompare(b) > 0 ? -1 : 1),
      equals: (a, b) => a === b,
    })
  )
);

// 8. 实用函数
// uniq - 去重
console.log(pipe([1, 1, 2, 2, 3, 3], A.uniq({ equals: (a, b) => a === b }))); // [1, 2, 3]

// zip - 将两个数组配对
console.log(pipe([1, 2, 3], A.zip(['a', 'b', 'c']))); // [[1, 'a'], [2, 'b'], [3, 'c']]

// chunk - 将数组分成固定大小的块
console.log(pipe([1, 2, 3, 4, 5], A.chunksOf(2))); // [[1, 2], [3, 4], [5]]
  1. Task

  • 处理异步操作

  • 表示一个可能异步执行的计算

  • 常用于处理Promise

主要使用场景:

  1. API 调用

  2. 文件操作

  3. 数据库操作

  4. 任何异步操作

实践建议:

  1. 对于可能失败的异步操作,优先使用 TaskEither

  2. 使用 pipe 来组合多个操作

  3. 考虑使用并行操作来优化性能

  4. 实现错误重试机制来提高可靠性

  5. 始终处理错误情况

import { pipe } from 'fp-ts/function';
import * as T from 'fp-ts/Task';
import * as TE from 'fp-ts/TaskEither';

// 1. 基本创建和使用
// Task 类型实际上是一个返回 Promise 的函数:() => Promise<A>

// 从普通值创建 Task
const task1 = T.of(1); // Task<number>

// 从异步函数创建 Task
const fetchUser =
  (id: number): T.Task<User> =>
  () =>
    fetch(`/api/users/${id}`).then(res => res.json());

// 2. 基本操作
// map - 转换值
const doubled = pipe(
  task1,
  T.map(n => n * 2)
);
doubled().then(v => console.log('doubled', v)); // Promise<2>

// chain - 链接 Tasks
const getUserAndPosts = (userId: number) =>
  pipe(
    fetchUser(userId),
    T.chain(user =>
      pipe(
        fetchUserPosts(user.id),
        T.map(posts => ({ user, posts }))
      )
    )
  );
getUserAndPosts(1)().then(v => console.log('getUserAndPosts ', v)); // Promise<{ user: User, posts: Post[] }>

// 3. 错误处理 (使用 TaskEither)
// TaskEither 组合了 Task 和 Either,用于处理可能失败的异步操作
const fetchUserSafely = (id: number): TE.TaskEither<Error, User> =>
  TE.tryCatch(
    () => fetch(`/api/users/${id}`).then(res => res.json()),
    error => new Error(`Failed to fetch user: ${error}`)
  );

// 使用 TaskEither
const program = pipe(
  fetchUserSafely(1),
  TE.map(user => user.name),
  TE.fold(
    error => T.of(`Error: ${error.message}`),
    name => T.of(`Success: ${name}`)
  )
);
program().then(v => console.log('program', v)); // Promise<"Success: John">

// 4. 实际应用示例

// 定义接口
interface User {
  id: number;
  name: string;
}

interface Post {
  id: number;
  title: string;
  userId: number;
}

// API 调用函数
const fetchUserById = (id: number): TE.TaskEither<Error, User> =>
  TE.tryCatch(
    () => fetch(`/api/users/${id}`).then(res => res.json()),
    error => new Error(`Failed to fetch user: ${error}`)
  );

const fetchUserPosts = (userId: number): TE.TaskEither<Error, Post[]> =>
  TE.tryCatch(
    () => fetch(`/api/posts?userId=${userId}`).then(res => res.json()),
    error => new Error(`Failed to fetch posts: ${error}`)
  );

// 组合多个操作
const getUserWithPosts = (userId: number) =>
  pipe(
    fetchUserById(userId),
    TE.chain(user =>
      pipe(
        fetchUserPosts(user.id),
        TE.map(posts => ({ user, posts }))
      )
    ),
    TE.fold(
      error => T.of(`Error: ${error.message}`),
      name => T.of(`Success: ${name}`)
    )
  );
getUserWithPosts(1)(); // Promise<{ user: User, posts: Post[] }>

// 5. 并行操作
// 并行执行多个 Task
const parallel = T.sequenceArray([fetchUser(1), fetchUser(2), fetchUser(3)]);
console.log('parallel', parallel()); // Promise<[User, User, User]>

// 并行执行多个 TaskEither
const parallelTE = TE.sequenceArray([fetchUserSafely(1), fetchUserSafely(2), fetchUserSafely(3)]);
console.log('parallelTE', parallelTE()); // Promise<Either<Error, [User, User, User]>>

// 6. 实用工具函数
// delay - 延迟执行
const delay =
  (ms: number): T.Task<void> =>
  () =>
    new Promise(resolve => setTimeout(resolve, ms));

// 带延迟的操作
const fetchUserWithDelay = (id: number) =>
  pipe(
    delay(1000),
    T.chain(() => fetchUser(id))
  );
console.log('fetchUserWithDelay', fetchUserWithDelay(1)()); // Promise<User>

// 7. 错误重试
const withRetry = <E, A>(task: TE.TaskEither<E, A>, maxAttempts: number): TE.TaskEither<E, A> => {
  const attempt = (currentAttempt: number): TE.TaskEither<E, A> =>
    pipe(
      task,
      TE.orElse(error =>
        currentAttempt < maxAttempts
          ? pipe(
              delay(1000),
              T.chain(() => attempt(currentAttempt + 1))
            )
          : TE.left(error)
      )
    );
  return attempt(1);
};

// 使用示例
const fetchWithRetry = pipe(fetchUserSafely(1), task => withRetry(task, 3));
console.log('fetchWithRetry', fetchWithRetry()); // Promise<Either<Error, User>>

// 8. 完整的应用示例
// const userProgram = (userId: number) =>
//   pipe(
//     getUserWithPosts(userId),
//     TE.fold(
//       error =>
//         T.of({
//           success: false as const,
//           error: error.message,
//         }),
//       result =>
//         T.of({
//           success: true as const,
//           data: result,
//         })
//     )
//   );

// 执行
// userProgram(1)().then(result => {
//   if (result.success) {
//     console.log('User and posts:', result.data);
//   } else {
//     console.error('Error:', result.error);
//   }
// });
  1. IO

  • 处理同步的副作用

  • 用于封装有副作用的操作

主要使用场景:

  1. DOM 操作

  2. 本地存储操作

  3. 控制台输入输出

  4. 随机数生成

  5. 日期/时间操作

  6. 全局状态管理

实践建议:

  1. IO 用于封装所有带有副作用的同步操作

  2. 对于可能失败的操作,使用 IOEither

  3. 使用 pipe 组合多个操作

  4. 将复杂的操作分解成小的、可组合的单元

  5. 始终考虑错误处理

  6. 保持函数纯净,将副作用限制在 IO 中

设计模式:

  1. Program = Pure core + Impure shell

    • 将核心业务逻辑保持纯函数

    • 使用 IO 在外层处理副作用

  2. 依赖注入

    • 通过参数传入 IO 操作,提高可测试性

  3. 错误处理

    • 使用 IOEither 处理可能的错误

    • 实现重试机制

import { pipe } from 'fp-ts/function';
import * as IO from 'fp-ts/IO';
import * as IOE from 'fp-ts/IOEither';
import * as E from 'fp-ts/Either';

// 1. 基本概念
// IO<A> 本质上是一个返回类型 A 的函数:() => A

// 2. 基本创建和使用
// 创建简单的 IO
const getTimestamp: IO.IO<number> = () => Date.now();
const getRandomNumber: IO.IO<number> = () => Math.random();

// 创建控制台操作
const log =
  (message: string): IO.IO<void> =>
  () =>
    console.log(message);

const readLine: IO.IO<string> = () => prompt('Enter something:') || '';

// 3. 基本操作
// map - 转换值
const doubledTimestamp = pipe(
  getTimestamp,
  IO.map(time => time * 2)
);

// chain - 链接多个 IO
const logTimestamp = pipe(
  getTimestamp,
  IO.chain(time => log(`Current time: ${time}`))
);

// 4. IOEither - 处理可能失败的同步操作
// 解析 JSON
const parseJSON = (str: string): IOE.IOEither<Error, unknown> =>
  IOE.tryCatch(
    () => JSON.parse(str),
    error => new Error(`Parse failed: ${error}`)
  );

// 5. 实际应用示例

// DOM 操作
interface DOMOperations {
  getElementById: (id: string) => IO.IO<HTMLElement | null>;
  setValue: (element: HTMLElement, value: string) => IO.IO<void>;
  getValue: (element: HTMLElement) => IO.IO<string>;
}

const dom: DOMOperations = {
  getElementById: id => () => document.getElementById(id),
  setValue: (element, value) => () => {
    if ('value' in element) {
      (element as HTMLInputElement).value = value;
    }
  },
  getValue: element => () => ('value' in element ? (element as HTMLInputElement).value : ''),
};
console.log('dom :>> ', dom);

// 本地存储操作
const storage = {
  getItem:
    (key: string): IO.IO<string | null> =>
    () =>
      localStorage.getItem(key),

  setItem:
    (key: string, value: string): IO.IO<void> =>
    () =>
      localStorage.setItem(key, value),
};

// 6. 复杂示例:表单处理
interface FormData {
  name: string;
  email: string;
}

const validateForm = (data: FormData): IOE.IOEither<string, FormData> =>
  IOE.tryCatch(
    () => {
      if (!data.name) throw new Error('Name is required');
      if (!data.email) throw new Error('Email is required');
      if (!data.email.includes('@')) throw new Error('Invalid email');
      return data;
    },
    error => `Validation failed: ${error}`
  );

const saveForm = (data: FormData): IO.IO<void> =>
  pipe(
    storage.setItem('formData', JSON.stringify(data)),
    IO.chain(() => log('Form saved successfully'))
  );

// 类型守卫函数
const isFormData = (json: unknown): json is FormData => {
  // 实现你的类型检查逻辑
  return true; // 这里需要根据实际 FormData 结构进行验证
};

const loadForm = (): IOE.IOEither<string, FormData> =>
  pipe(
    storage.getItem('formData'),
    IO.chain(data =>
      data
        ? pipe(
            parseJSON(data),
            IOE.mapLeft(error => error.message), // 将 Error 转换为 string
            IOE.chainEitherK(json =>
              // 验证并转换为 FormData
              isFormData(json) ? E.right(json as FormData) : E.left('Invalid form data format')
            )
          )
        : IO.of(E.left('No saved form data'))
    )
  );

console.log('loadForm :>> ', loadForm()());

// 7. 组合多个操作
const formProgram = (data: FormData) =>
  pipe(
    validateForm(data),
    IOE.chain(validData =>
      IOE.tryCatch(
        () => saveForm(validData)(),
        error => `Save failed: ${error}`
      )
    )
  );

// 8. 实用工具函数
// 重试机制
const withRetry = <A>(io: IO.IO<A>, maxAttempts: number): IO.IO<A> => {
  const attempt =
    (currentAttempt: number): IO.IO<A> =>
    () => {
      try {
        return io();
      } catch (error) {
        if (currentAttempt < maxAttempts) {
          return attempt(currentAttempt + 1)();
        }
        throw error;
      }
    };
  return attempt(1);
};

// 9. 完整的应用示例
const userFormProgram = () => {
  const getUserInput = (): IO.IO<FormData> => () => ({
    name: (document.getElementById('name') as HTMLInputElement)?.value || '',
    email: (document.getElementById('email') as HTMLInputElement)?.value || '',
  });

  return pipe(
    getUserInput(),
    IO.chain(data =>
      pipe(
        formProgram(data),
        IOE.fold(
          error => log(`Error: ${error}`),
          () => log('Success!')
        )
      )
    )
  );
};

// 执行示例
const main = userFormProgram();
main(); // 执行所有操作

一些理解:

  1. 函数式编程的要求是纯函数,而非一定是最简函数,那可能是最佳实践,在实际的使用过程中,先学会用,再去重构到最佳实践

  2. 函数式编程就是关于组合

  3. 处理复杂转换时,将操作分解为多个步骤

  4. 面向对象是分门别类,永远分不完。而函数式编程是制定规则,定义世界,你是人类还是动物,对于创造世界的神来说都是一样的。就比如说吃饭,吃了就有能量,不吃就没有,动物和人都一样会饿死,这是一条规则!


评论