mcpField
F
fp-ts Do Notation

fp-ts Do Notation

github.com/whatiskadudoing開発1450 回インストール39 週間前に更新
リポジトリ

fp-ts Do Notation について

fp-ts Do Notationは、fp-tsを使うTypeScript開発者向けに、モナド操作を読みやすいパイプラインとして構成する方法を解説するガイドです。依存する処理の順次実行、独立タスクの並列実行、以前の値を保った派生値の計算を扱います。

主な機能

  1. 01Doでコンテキストパイプラインを開始
  2. 02bindで依存する処理を順次実行
  3. 03apSで独立した処理を並列化
  4. 04letで値を導出
  5. 05EitherとTaskEitherのエラー処理

活用シーン

  1. 01ネストした非同期ワークフローの整理
  2. 02独立したダッシュボード要求の統合
  3. 03複数の処理段階で以前の値を保持
  4. 04逐次処理と並列処理の選択

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/whatiskadudoing/fp-ts-skills --skill fp-ts-do-notation

インストール後、Claude Code で/fp-ts-do-notationを入力して読み込まれたか確認します。

詳細

fp-ts Do 記法ガイド

Do 記法は、コールバック地獄に対する fp-ts の答えです。関数型の純粋性と型安全性を維持しながら、逐次的で命令形のように見えるコードを書く方法を提供します。

問題:関数型コードにおけるコールバック地獄

Do 記法を使わずに依存する操作を連鎖させると、深くネストしたコードになります。

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

// BAD: Nested chain hell
const processOrder = (orderId: string) =>
  pipe(
    fetchOrder(orderId),
    TE.chain((order) =>
      pipe(
        fetchUser(order.userId),
        TE.chain((user) =>
          pipe(
            fetchInventory(order.productId),
            TE.chain((inventory) =>
              pipe(
                validateStock(inventory, order.quantity),
                TE.chain((validated) =>
                  pipe(
                    calculatePrice(order, user.discount),
                    TE.chain((price) =>
                      createInvoice(order, user, price) // Lost context of inventory!
                    )
                  )
                )
              )
            )
          )
        )
      )
    )
  )

ネストしたチェーンの問題点:

  1. 可読性が低い - ロジックがネストの中に埋もれます
  2. コンテキストが失われる - 前の値に内側のスコープからアクセスできない場合があります
  3. リファクタリングが困難 - ステップを追加・削除するには構造を変更する必要があります
  4. 並列化が困難 - すべてが逐次的に見えます

解決策:Do 記法

Do 記法は構造を平坦化し、すべての値をスコープ内に保ちます。

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

// GOOD: Flat, readable Do notation
const processOrder = (orderId: string) =>
  pipe(
    TE.Do,
    TE.bind('order', () => fetchOrder(orderId)),
    TE.bind('user', ({ order }) => fetchUser(order.userId)),
    TE.bind('inventory', ({ order }) => fetchInventory(order.productId)),
    TE.bind('validated', ({ inventory, order }) => validateStock(inventory, order.quantity)),
    TE.bind('price', ({ order, user }) => calculatePrice(order, user.discount)),
    TE.bind('invoice', ({ order, user, price }) => createInvoice(order, user, price))
  )

Do 記法の主要な関数

Do - 開始地点

Do はモナドでラップされた空のコンテキストオブジェクト {} を作成します。

import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'
import * as O from 'fp-ts/Option'

TE.Do  // TaskEither<never, {}>
E.Do   // Either<never, {}>
O.Do   // Option<{}>
bindTo - 最初の値で初期化

すでに値があり、それを使って Do 記法を開始する場合は bindTo を使います。

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

// Instead of:
pipe(
  TE.Do,
  TE.bind('user', () => fetchUser(userId))
)

// Use bindTo for cleaner initialization:
pipe(
  fetchUser(userId),
  TE.bindTo('user'),
  TE.bind('orders', ({ user }) => fetchOrders(user.id))
)

bindTo は意味的には TE.map(user => ({ user })) と同等ですが、より読みやすくなります。

bind - 逐次的な依存操作

次の操作が前の値に依存する場合は bind を使います。

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

const getUserWithPosts = (userId: string) =>
  pipe(
    TE.Do,
    TE.bind('user', () => fetchUser(userId)),           // First: get user
    TE.bind('posts', ({ user }) => fetchPosts(user.id)), // Then: use user.id
    TE.bind('comments', ({ posts }) =>                   // Then: use posts
      TE.traverseArray(fetchComments)(posts.map(p => p.id))
    )
  )

bind の主な特徴:

  • 操作は逐次的に実行されます
  • 各ステップからそれまでのすべての値にアクセスできます
  • 最初のエラーで処理をショートサーキットします(Either/TaskEither の場合)
  • コールバックは、蓄積されたコンテキストオブジェクトを受け取ります
apS - 並列の独立操作

操作が独立していて、並列実行できる場合は apS を使います。

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

const getDashboardData = (userId: string) =>
  pipe(
    TE.Do,
    TE.bind('user', () => fetchUser(userId)),
    // These three are INDEPENDENT - use apS for parallel execution
    TE.apS('notifications', fetchNotifications(userId)),
    TE.apS('settings', fetchSettings(userId)),
    TE.apS('recentActivity', fetchRecentActivity(userId))
  )

apS の主な特徴:

  • 操作を並列に実行できます(TaskEither の場合)
  • 値はすぐに計算されます(遅延評価ではありません)
  • 前のコンテキスト値にはアクセスできません
  • エラーは Applicative に応じて収集されるか、ショートサーキットされます
let - 計算値/派生値

既存の値から派生する同期的な計算には let を使います。

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

const processPayment = (orderId: string) =>
  pipe(
    TE.Do,
    TE.bind('order', () => fetchOrder(orderId)),
    TE.bind('user', ({ order }) => fetchUser(order.userId)),
    // Computed values - no async operation needed
    TE.let('subtotal', ({ order }) => order.items.reduce((sum, i) => sum + i.price, 0)),
    TE.let('discount', ({ user, subtotal }) => subtotal * (user.discountPercent / 100)),
    TE.let('total', ({ subtotal, discount }) => subtotal - discount),
    TE.bind('payment', ({ total, user }) => chargeCard(user.paymentMethod, total))
  )

let の主な特徴:

  • 同期的な純粋計算です
  • それまでのすべての値にアクセスできます
  • 失敗することはありません(エラー型の場合)
  • 変換、計算、書式設定に使います

bind と apS:どちらを使うべきか

判断ガイド
状況使用するもの理由
次の操作で前の結果が必要bind逐次的な依存関係
操作が独立しているapS並列化できる
変換や計算が必要let同期的で、常に成功する
既存の値から開始するbindToDo + bind より簡潔
パフォーマンス:逐次処理と並列処理
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'

// SLOW: Sequential execution with bind (3 seconds total)
const slowDashboard = pipe(
  TE.Do,
  TE.bind('users', () => fetchUsers()),      // 1 second
  TE.bind('products', () => fetchProducts()), // 1 second (waits for users)
  TE.bind('orders', () => fetchOrders())      // 1 second (waits for products)
)

// FAST: Parallel execution with apS (1 second total)
const fastDashboard = pipe(
  TE.Do,
  TE.apS('users', fetchUsers()),      // 1 second
  TE.apS('products', fetchProducts()), // 1 second (runs in parallel)
  TE.apS('orders', fetchOrders())      // 1 second (runs in parallel)
)
複合パターン:逐次処理の後に並列処理
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'

const getOrderDetails = (orderId: string) =>
  pipe(
    TE.Do,
    // Sequential: need order first
    TE.bind('order', () => fetchOrder(orderId)),
    // Parallel: these only need order.userId and order.productId
    TE.apS('user', pipe(
      TE.Do,
      TE.bind('order', () => fetchOrder(orderId)), // Need to refetch or...
    )),
    // Better pattern: bind first, then parallel for truly independent operations
  )

// BETTER: Restructure for clarity
const getOrderDetailsBetter = (orderId: string) =>
  pipe(
    fetchOrder(orderId),
    TE.bindTo('order'),
    TE.bind('user', ({ order }) => fetchUser(order.userId)),
    // Now these are independent of each other (but dependent on user/order)
    TE.apS('shippingOptions', fetchShippingOptions(orderId)),
    TE.apS('paymentMethods', fetchPaymentMethods(orderId)),
    TE.let('canCheckout', ({ user, shippingOptions }) =>
      user.verified && shippingOptions.length > 0
    )
  )

実践例

例 1:ユーザー登録フロー
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'
import * as E from 'fp-ts/Either'

interface RegistrationInput {
  email: string
  password: string
  name: string
}

interface User {
  id: string
  email: string
  name: string
}

const registerUser = (input: RegistrationInput): TE.TaskEither<Error, User> =>
  pipe(
    TE.Do,
    // Validate input (synchronous)
    TE.bind('validated', () => pipe(
      validateEmail(input.email),
      E.chain(() => validatePassword(input.password)),
      E.map(() => input),
      TE.fromEither
    )),
    // Check if email exists (async)
    TE.bind('emailAvailable', ({ validated }) =>
      checkEmailAvailable(validated.email)
    ),
    // Hash password (async, CPU-intensive)
    TE.bind('hashedPassword', ({ validated }) =>
      hashPassword(validated.password)
    ),
    // Create user in database
    TE.bind('user', ({ validated, hashedPassword }) =>
      createUser({
        email: validated.email,
        name: validated.name,
        passwordHash: hashedPassword
      })
    ),
    // Send welcome email (fire and forget, but still in chain)
    TE.chainFirst(({ user }) => sendWelcomeEmail(user.email)),
    // Return just the user
    TE.map(({ user }) => user)
  )
例 2:E コマースのチェックアウト
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'

interface CheckoutResult {
  orderId: string
  paymentId: string
  estimatedDelivery: Date
}

const checkout = (
  userId: string,
  cartId: string,
  shippingAddressId: string
): TE.TaskEither<CheckoutError, CheckoutResult> =>
  pipe(
    TE.Do,
    // Fetch required data in parallel where possible
    TE.bind('cart', () => fetchCart(cartId)),
    TE.bind('user', () => fetchUser(userId)),
    TE.apS('shippingAddress', fetchAddress(shippingAddressId)),

    // Validate cart has items
    TE.bind('validatedCart', ({ cart }) =>
      cart.items.length === 0
        ? TE.left(new CheckoutError('Cart is empty'))
        : TE.right(cart)
    ),

    // Check inventory for all items (parallel)
    TE.bind('inventoryCheck', ({ validatedCart }) =>
      pipe(
        validatedCart.items,
        TE.traverseArray((item) => checkInventory(item.productId, item.quantity))
      )
    ),

    // Calculate totals (synchronous)
    TE.let('subtotal', ({ validatedCart }) =>
      validatedCart.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
    ),
    TE.let('tax', ({ subtotal, shippingAddress }) =>
      calculateTax(subtotal, shippingAddress.state)
    ),
    TE.let('shippingCost', ({ shippingAddress, validatedCart }) =>
      calculateShipping(shippingAddress, validatedCart.totalWeight)
    ),
    TE.let('total', ({ subtotal, tax, shippingCost }) =>
      subtotal + tax + shippingCost
    ),

    // Process payment
    TE.bind('payment', ({ user, total }) =>
      processPayment(user.defaultPaymentMethod, total)
    ),

    // Create order
    TE.bind('order', ({ user, validatedCart, shippingAddress, payment, total }) =>
      createOrder({
        userId: user.id,
        items: validatedCart.items,
        shippingAddressId: shippingAddress.id,
        paymentId: payment.id,
        total
      })
    ),

    // Reserve inventory
    TE.chainFirst(({ order, validatedCart }) =>
      pipe(
        validatedCart.items,
        TE.traverseArray((item) => reserveInventory(item.productId, item.quantity, order.id))
      )
    ),

    // Clear cart
    TE.chainFirst(({ cart }) => clearCart(cart.id)),

    // Calculate delivery estimate
    TE.let('estimatedDelivery', ({ shippingAddress }) =>
      calculateDeliveryDate(shippingAddress)
    ),

    // Return result
    TE.map(({ order, payment, estimatedDelivery }) => ({
      orderId: order.id,
      paymentId: payment.id,
      estimatedDelivery
    }))
  )
例 3:依存性注入を使う ReaderTaskEither
import { pipe } from 'fp-ts/function'
import * as RTE from 'fp-ts/ReaderTaskEither'
import * as TE from 'fp-ts/TaskEither'

// Define dependencies
interface Deps {
  userRepo: UserRepository
  orderRepo: OrderRepository
  paymentService: PaymentService
  emailService: EmailService
  logger: Logger
}

// Use RTE.Do for dependency-injected workflows
const processRefund = (
  orderId: string,
  reason: string
): RTE.ReaderTaskEither<Deps, RefundError, RefundResult> =>
  pipe(
    RTE.Do,
    // Access dependencies via RTE.asks
    RTE.bind('deps', () => RTE.ask<Deps>()),

    // Fetch order
    RTE.bind('order', ({ deps }) =>
      RTE.fromTaskEither(deps.orderRepo.findById(orderId))
    ),

    // Validate refund is possible
    RTE.bind('validatedOrder', ({ order }) =>
      order.status !== 'completed'
        ? RTE.left(new RefundError('Order not eligible for refund'))
        : RTE.right(order)
    ),

    // Process refund with payment service
    RTE.bind('refund', ({ deps, validatedOrder }) =>
      RTE.fromTaskEither(
        deps.paymentService.refund(validatedOrder.paymentId, validatedOrder.total)
      )
    ),

    // Update order status
    RTE.bind('updatedOrder', ({ deps, validatedOrder, refund }) =>
      RTE.fromTaskEither(
        deps.orderRepo.update(validatedOrder.id, {
          status: 'refunded',
          refundId: refund.id,
          refundReason: reason
        })
      )
    ),

    // Fetch user for email
    RTE.bind('user', ({ deps, validatedOrder }) =>
      RTE.fromTaskEither(deps.userRepo.findById(validatedOrder.userId))
    ),

    // Send notification (fire and forget)
    RTE.chainFirst(({ deps, user, refund }) =>
      RTE.fromTaskEither(
        deps.emailService.sendRefundConfirmation(user.email, refund)
      )
    ),

    // Log the refund
    RTE.chainFirst(({ deps, order, refund }) =>
      RTE.fromTaskEither(
        deps.logger.info('Refund processed', { orderId: order.id, refundId: refund.id })
      )
    ),

    // Return result
    RTE.map(({ refund, updatedOrder }) => ({
      refundId: refund.id,
      orderId: updatedOrder.id,
      amount: refund.amount,
      status: 'completed'
    }))
  )

// Execute with dependencies
const runRefund = (deps: Deps, orderId: string, reason: string) =>
  processRefund(orderId, reason)(deps)()
例 4:エラーを蓄積するバリデーション
import { pipe } from 'fp-ts/function'
import * as E from 'fp-ts/Either'
import * as TE from 'fp-ts/TaskEither'
import * as A from 'fp-ts/Apply'
import { sequenceS } from 'fp-ts/Apply'

// For parallel validation that accumulates ALL errors (not short-circuit)
// Use Apply.sequenceS instead of Do notation

interface ValidationError {
  field: string
  message: string
}

type ValidationResult<A> = E.Either<ValidationError[], A>

const validateUserInput = (input: unknown): ValidationResult<ValidUser> => {
  const validateField = <A>(
    field: string,
    value: unknown,
    validator: (v: unknown) => E.Either<string, A>
  ): ValidationResult<A> =>
    pipe(
      validator(value),
      E.mapLeft((message) => [{ field, message }])
    )

  // Use sequenceS with validation applicative to collect ALL errors
  return pipe(
    sequenceS(E.getApplicativeValidation(A.getSemigroup<ValidationError>()))({
      email: validateField('email', input.email, validateEmail),
      password: validateField('password', input.password, validatePassword),
      age: validateField('age', input.age, validateAge),
      name: validateField('name', input.name, validateName)
    })
  )
}

// For TaskEither with parallel execution AND error accumulation:
const validateUserAsync = (input: UserInput): TE.TaskEither<ValidationError[], ValidUser> =>
  pipe(
    sequenceS(TE.ApplicativePar)({
      emailUnique: checkEmailUnique(input.email),
      usernameAvailable: checkUsernameAvailable(input.username),
      phoneValid: validatePhoneNumber(input.phone)
    }),
    TE.map(({ emailUnique, usernameAvailable, phoneValid }) => ({
      ...input,
      emailVerified: emailUnique,
      usernameVerified: usernameAvailable,
      phoneVerified: phoneValid
    }))
  )
例 5:複雑なデータ集約
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'
import * as A from 'fp-ts/Array'

interface DashboardData {
  user: User
  stats: UserStats
  recentOrders: Order[]
  recommendations: Product[]
  notifications: Notification[]
}

const loadDashboard = (userId: string): TE.TaskEither<Error, DashboardData> =>
  pipe(
    TE.Do,
    // First, get user (required for everything)
    TE.bind('user', () => fetchUser(userId)),

    // These are all independent - parallel execution
    TE.apS('stats', fetchUserStats(userId)),
    TE.apS('recentOrders', fetchRecentOrders(userId)),
    TE.apS('notifications', fetchNotifications(userId)),

    // Recommendations depend on user preferences
    TE.bind('recommendations', ({ user }) =>
      fetchRecommendations(user.preferences)
    ),

    // Enhance orders with product details (depends on recentOrders)
    TE.bind('ordersWithProducts', ({ recentOrders }) =>
      pipe(
        recentOrders,
        A.map((order) =>
          pipe(
            fetchProductDetails(order.productId),
            TE.map((product) => ({ ...order, product }))
          )
        ),
        TE.sequenceArray
      )
    ),

    // Compute derived data
    TE.let('unreadCount', ({ notifications }) =>
      notifications.filter((n) => !n.read).length
    ),
    TE.let('totalSpent', ({ recentOrders }) =>
      recentOrders.reduce((sum, o) => sum + o.total, 0)
    ),

    // Return final shape
    TE.map(({ user, stats, ordersWithProducts, recommendations, notifications }) => ({
      user,
      stats,
      recentOrders: ordersWithProducts,
      recommendations,
      notifications
    }))
  )

よく使うパターンとヒント

パターン 1:早期リターン/ガード節
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'

const deleteAccount = (userId: string, confirmationCode: string) =>
  pipe(
    TE.Do,
    TE.bind('user', () => fetchUser(userId)),
    // Guard: Check confirmation code
    TE.bind('confirmed', ({ user }) =>
      confirmationCode === user.deleteConfirmationCode
        ? TE.right(true)
        : TE.left(new Error('Invalid confirmation code'))
    ),
    // Guard: Check no pending orders
    TE.bind('pendingOrders', ({ user }) => fetchPendingOrders(user.id)),
    TE.bind('canDelete', ({ pendingOrders }) =>
      pendingOrders.length === 0
        ? TE.right(true)
        : TE.left(new Error('Cannot delete account with pending orders'))
    ),
    // Proceed with deletion
    TE.bind('deleted', ({ user }) => deleteUserAccount(user.id))
  )
パターン 2:chainFirst を使った任意の操作

副作用を実行しつつ元の値を保持したい場合は chainFirst を使います。

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

const createPost = (input: PostInput) =>
  pipe(
    TE.Do,
    TE.bind('post', () => savePost(input)),
    // Log creation (side effect, ignore result)
    TE.chainFirst(({ post }) => logPostCreation(post.id)),
    // Notify followers (side effect, ignore result)
    TE.chainFirst(({ post }) => notifyFollowers(post.authorId, post.id)),
    // Index for search (side effect, ignore result)
    TE.chainFirst(({ post }) => indexForSearch(post)),
    // Return just the post
    TE.map(({ post }) => post)
  )
パターン 3:条件付きバインド
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'
import * as O from 'fp-ts/Option'

const processOrder = (orderId: string, promoCode?: string) =>
  pipe(
    TE.Do,
    TE.bind('order', () => fetchOrder(orderId)),
    // Conditionally apply promo code
    TE.bind('discount', ({ order }) =>
      promoCode
        ? validatePromoCode(promoCode, order.total)
        : TE.right(0)
    ),
    TE.let('finalTotal', ({ order, discount }) => order.total - discount)
  )
パターン 4:Do 記法で配列を扱う
import { pipe } from 'fp-ts/function'
import * as TE from 'fp-ts/TaskEither'
import * as A from 'fp-ts/Array'

const processOrders = (orderIds: string[]) =>
  pipe(
    TE.Do,
    // Fetch all orders in parallel
    TE.bind('orders', () =>
      pipe(
        orderIds,
        A.map(fetchOrder),
        TE.sequenceArray
      )
    ),
    // Process each order
    TE.bind('processed', ({ orders }) =>
      pipe(
        orders,
        A.map(processOrder),
        TE.sequenceArray
      )
    ),
    // Aggregate results
    TE.let('summary', ({ processed }) => ({
      total: processed.length,
      successful: processed.filter((p) => p.status === 'success').length,
      failed: processed.filter((p) => p.status === 'failed').length
    }))
  )

パフォーマンスに関する考慮事項

1. 独立した操作には apS を使う
// SLOW: 3 sequential API calls
pipe(
  TE.Do,
  TE.bind('a', () => fetchA()), // 100ms
  TE.bind('b', () => fetchB()), // 100ms
  TE.bind('c', () => fetchC())  // 100ms
) // Total: 300ms

// FAST: 3 parallel API calls
pipe(
  TE.Do,
  TE.apS('a', fetchA()), // 100ms
  TE.apS('b', fetchB()), // 100ms (parallel)
  TE.apS('c', fetchC())  // 100ms (parallel)
) // Total: ~100ms
2. 純粋計算には let を使う
// WRONG: Using bind for pure computation
TE.bind('total', ({ items }) => TE.right(items.reduce((s, i) => s + i.price, 0)))

// RIGHT: Using let for pure computation
TE.let('total', ({ items }) => items.reduce((s, i) => s + i.price, 0))
3. データベース操作をまとめる
// SLOW: N+1 queries
pipe(
  TE.Do,
  TE.bind('orders', () => fetchOrders(userId)),
  TE.bind('products', ({ orders }) =>
    pipe(
      orders,
      A.map((o) => fetchProduct(o.productId)), // N queries!
      TE.sequenceArray
    )
  )
)

// FAST: Batch query
pipe(
  TE.Do,
  TE.bind('orders', () => fetchOrders(userId)),
  TE.bind('products', ({ orders }) =>
    fetchProductsByIds(orders.map((o) => o.productId)) // 1 query
  )
)
4. コンテキストの再構築を避ける
// INEFFICIENT: Rebuilding large context
pipe(
  TE.Do,
  TE.bind('hugeData', () => fetchHugeData()),
  TE.map(({ hugeData }) => ({ hugeData, processed: true })), // Copies hugeData
  TE.bind('more', () => fetchMore()) // hugeData still in context
)

// BETTER: Extract what you need early
pipe(
  TE.Do,
  TE.bind('hugeData', () => fetchHugeData()),
  TE.let('summary', ({ hugeData }) => summarize(hugeData)), // Extract summary
  TE.map(({ summary }) => summary) // Drop hugeData from context
)

概要

Do 記法を使うと、深くネストしたコールバックチェーンを、簡潔で読みやすいパイプラインに変換できます。

関数目的使用するタイミング
Do空のコンテキストを開始するチェーンの開始時
bindTo値を使って開始する初期値がある場合
bind順次処理を行う前の値に依存する場合
apS並列処理を行う他の値に依存しない場合
let純粋な計算を行う値を同期的に導出する場合
chainFirst副作用を実行する結果を待たずに実行する操作

主な原則:

  1. 依存関係がある場合は bind、独立している場合は apS を使います
  2. 純粋な計算には let を使い、TE.right を使った bind は避けます
  3. コンテキストは簡潔に保ち、不要なデータを蓄積しないようにします
  4. コレクションでは sequenceArray/traverseArray と組み合わせます
  5. 結果に影響を与えない副作用には chainFirst を使います

Do 記法は、保守性の高い fp-ts コードを書くための鍵です。使いこなせば、関数型プログラミングの利点を保ちながら、命令型コードと同じくらい読みやすくなります。

コメント