You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Functional core - Imperative shell模式下,依赖昂贵IO操作结果的逻辑处理方案问询

Functional core - Imperative shell模式下,依赖昂贵IO操作结果的逻辑处理方案问询

Great question—this is one of the most common pain points when scaling the Functional Core/Imperative Shell (FC/IS) pattern beyond demo-friendly examples, especially in multi-paradigm languages like TypeScript or Kotlin where you don’t have Clojure’s built-in data-first control flow tools. The pattern doesn’t break down here; you just need to extend your Effect model to describe dependent, branching logic instead of only returning flat lists of final side effects.

Let’s walk through practical approaches tailored to non-pure languages, using TypeScript examples (easily adaptable to Kotlin) that keep your core logic pure and testable.


1. 分层Effect模型:描述控制流与延迟依赖

Instead of returning only primitive side effects (like db-transact or ms-graph/request), define a hierarchy of effects that includes control flow structures. These structures let your core logic describe:

  • When to run expensive IO operations
  • How to branch based on their results
  • How to chain subsequent effects using those results

Step 1: 定义带类型的Effect层级(TypeScript)

首先,将原始副作用和控制流副作用都建模为纯数据(或可测试的极简函数):

// 用于追踪昂贵IO操作结果的唯一ID
type EffectId = string;

// 所有Effect的基础类型
type BaseEffect = { kind: string; id?: EffectId };

// 原始副作用(纯数据描述)
type DbTransactEffect = BaseEffect & {
  kind: "db-transact";
  data: Record<string, any>[];
};

type FindParticipantsEffect = BaseEffect & {
  kind: "db/find-participants";
  criteria: "A" | "B";
  id: EffectId; // 标记这个IO,方便后续引用其结果
};

type SendEmailEffect = BaseEffect & {
  kind: "email/send";
  participantIds: string[];
  emailType: "notification" | "refusal";
};

// 控制流Effect(描述分支/序列逻辑)
type IfEffect = BaseEffect & {
  kind: "control/if";
  // 通过ID引用之前IO的结果
  condition: {
    dependsOn: EffectId;
    // 纯函数,用于判断结果
    predicate: (result: { allowed: boolean }) => boolean;
  };
  then: Effect[];
  else: Effect[];
};

type SequenceEffect = BaseEffect & {
  kind: "control/sequence";
  first: Effect;
  // 纯函数,根据第一个Effect的结果生成后续Effect
  then: (result: Participant[]) => Effect[];
};

// 所有Effect类型的联合
type Effect = DbTransactEffect | FindParticipantsEffect | SendEmailEffect | IfEffect | SequenceEffect;

Step 2: 函数核心返回依赖型Effect链

现在你的核心逻辑可以描述依赖昂贵IO结果的分支,而不需要实际执行这些IO:

interface Participant { id: string; }

function planParticipantEmails(allowedCriteria: { A: any; B: any }): Effect {
  // 1. 描述昂贵的DB检查(标记ID)
  const heavyCheckId = "heavy-db-check-001";
  const heavyDbCheck: DbTransactEffect & { id: EffectId } = {
    kind: "db-transact",
    id: heavyCheckId,
    data: [{ operation: "heavy-permission-check" }]
  };

  // 2. 根据检查结果描述分支逻辑
  return {
    kind: "control/if",
    condition: {
      dependsOn: heavyCheckId,
      predicate: (result) => result.allowed
    },
    then: [
      // 3. 链式逻辑:先执行参与者查询,再发送邮件
      {
        kind: "control/sequence",
        first: {
          kind: "db/find-participants",
          id: "participants-A-001",
          criteria: "A"
        } as FindParticipantsEffect,
        then: (participants) => 
          participants.map(p => ({
            kind: "email/send",
            participantIds: [p.id],
            emailType: "notification"
          } as SendEmailEffect))
      }
    ],
    else: [
      {
        kind: "control/sequence",
        first: {
          kind: "db/find-participants",
          id: "participants-B-001",
          criteria: "B"
        } as FindParticipantsEffect,
        then: (participants) => 
          participants.map(p => ({
            kind: "email/send",
            participantIds: [p.id],
            emailType: "refusal"
          } as SendEmailEffect))
      }
    ]
  };
}

Step 3: 无需IO即可测试核心逻辑

你不需要mock任何服务——直接测试纯函数部分:

test("planParticipantEmails在允许时发送通知邮件", () => {
  // 模拟DB检查结果
  const mockCheckResult = { allowed: true };
  
  // 提取分支逻辑中的序列Effect
  const effect = planParticipantEmails({ A: {}, B: {} }) as IfEffect;
  const sequenceEffect = effect.then[0] as SequenceEffect;
  
  // 测试判断函数的正确性
  expect(effect.condition.predicate(mockCheckResult)).toBe(true);
  
  // 测试邮件生成逻辑
  const mockParticipants = [{ id: "user-1" }];
  const emailEffects = sequenceEffect.then(mockParticipants);
  
  expect(emailEffects).toHaveLength(1);
  expect(emailEffects[0]).toMatchObject({
    kind: "email/send",
    emailType: "notification"
  });
});

2. 惰性加载:避免预取不必要的数据

如果预取所有可能的数据集成本过高,可以使用惰性求值将IO延迟到真正需要的时候执行。你的核心逻辑将接收惰性操作的描述,而不是操作的结果。

TypeScript示例

// 昂贵操作的惰性包装
type Lazy<T> = () => Promise<T>;

// 函数核心:使用惰性操作描述逻辑的纯函数
function planParticipantEmails(
  lazyHeavyCheck: Lazy<{ allowed: boolean }>,
  lazyParticipantsA: Lazy<Participant[]>,
  lazyParticipantsB: Lazy<Participant[]>
): Effect[] {
  return [{
    kind: "control/lazy-if",
    // 描述何时执行惰性检查
    lazyCondition: lazyHeavyCheck,
    predicate: (result) => result.allowed,
    then: [
      {
        kind: "control/lazy-execute",
        lazy: lazyParticipantsA,
        then: (participants) => 
          participants.map(p => ({
            kind: "email/send",
            participantIds: [p.id],
            emailType: "notification"
          } as SendEmailEffect))
      }
    ],
    else: [
      {
        kind: "control/lazy-execute",
        lazy: lazyParticipantsB,
        then: (participants) => 
          participants.map(p => ({
            kind: "email/send",
            participantIds: [p.id],
            emailType: "refusal"
          } as SendEmailEffect))
      }
    ]
  }];
}

// 命令式外壳:负责执行惰性操作
async function executeLazyEffect(effect: any): Promise<void> {
  if (effect.kind === "control/lazy-if") {
    const conditionResult = await effect.lazyCondition();
    if (effect.predicate(conditionResult)) {
      await Promise.all(effect.then.map(executeLazyEffect));
    } else {
      await Promise.all(effect.else.map(executeLazyEffect));
    }
  } else if (effect.kind === "control/lazy-execute") {
    const result = await effect.lazy();
    const nextEffects = effect.then(result);
    await Promise.all(nextEffects.map(executeLazyEffect));
  } else if (effect.kind === "email/send") {
    await emailService.send(effect.participantIds, effect.emailType);
  }
}

测试时只需将惰性操作替换为返回固定值的纯函数,然后断言生成的Effect即可。


3. 非FP语言中的关键总结

FC/IS模式并没有失效,你只需要:

  • 扩展Effect模型:加入控制流结构(if/sequence),支持引用前序IO的结果。
  • 保持核心逻辑纯:使用纯函数处理判断和Effect生成,这样无需IO即可测试。
  • 将执行权交给外壳:外壳负责解析依赖、运行IO,并根据结果链式执行Effect。

即使在Kotlin这样的语言中,你也可以用密封类定义Effect,用挂起函数处理惰性IO。核心思想始终不变:领域逻辑只描述应该做什么以及如何根据结果调整,而外壳负责处理执行这些指令的繁琐工作。

火山引擎 最新活动