receta
Function

Integration Guide

Combining Function combinators with Result, Option, and Remeda for powerful pipelines.

Overview

The Function module is designed to work seamlessly with Receta's Result and Option types, as well as Remeda's data transformation utilities. This guide shows you how to combine them effectively.

Integration with Result

Conditional Logic with Results

import { pipe } from 'remeda'
import { ifElse, when, cond } from '@ontologyio/receta/function'
import { ok, err, isOk, map, flatMap } from '@ontologyio/receta/result'
import { unwrapOr as unwrapOrOption } from '@ontologyio/receta/option'

// Transform Results based on conditions
const processResult = <T>(result: Result<T, string>) =>
  pipe(
    result,
    when(
      isOk,
      (r) => map(r, (value) => ({ ...value, processed: true }))
    )
  )

// Multi-way error handling with cond
const handleError = cond<string, string>([
  [(err) => err.includes('network'), () => 'Network error - please retry'],
  [(err) => err.includes('auth'), () => 'Authentication failed'],
  [(err) => err.includes('not found'), () => 'Resource not found'],
  [() => true, (err) => `Unknown error: ${err}`]
])

const getErrorMessage = (result: Result<any, string>) =>
  isOk(result) ? 'Success' : unwrapOrOption(handleError(result.error), 'Error occurred')

Safe Transformation Pipelines

import { tryCatch } from '@ontologyio/receta/function'
import { flatMap, map } from '@ontologyio/receta/result'

interface User {
  id: string
  config: string // JSON string
}

const processUser = (user: User) =>
  pipe(
    user.config,
    // Parse JSON safely
    tryCatch((str: string) => JSON.parse(str)),
    // Transform if successful
    map((config) => ({
      ...user,
      parsedConfig: config
    })),
    // Validate
    flatMap(validateUser),
    // Add metadata
    map((validated: User) => ({
      ...validated,
      processedAt: new Date()
    }))
  )

Combining tap with Result

const processWithLogging = (data: string) =>
  pipe(
    data,
    tap((input) => logger.info('Input received', { length: input.length })),
    tryCatch(JSON.parse),
    tap((result) => {
      if (isOk(result)) {
        logger.info('Parse successful')
      } else {
        logger.error('Parse failed', result.error)
      }
    }),
    flatMap(validate),
    tap((result) => logger.info('Validation complete', { isOk: isOk(result) }))
  )

Integration with Option

Conditional Transformations

import { cond, when } from '@ontologyio/receta/function'
import { some, none, isSome, unwrapOr, map } from '@ontologyio/receta/option'

// cond returns Option naturally
const classify = cond<number, string>([
  [(n) => n < 0, () => 'negative'],
  [(n) => n === 0, () => 'zero'],
  [(n) => n > 0, () => 'positive']
])

const result = pipe(
  5,
  classify,
  unwrapOr('unknown')
)
// => 'positive'

Safe Lookups

import { fromNullable, isSome, map, unwrapOr } from '@ontologyio/receta/option'

interface Config {
  [key: string]: any
}

const getSetting = (config: Config, key: string): Option<any> =>
  fromNullable(config[key])

const processConfig = (config: Config) =>
  pipe(
    getSetting(config, 'apiUrl'),
    when(
      isSome,
      (opt) => map(opt, (url: string) => url.trim())
    ),
    unwrapOr('https://api.default.com')
  )

Integration with Remeda

Combining map/filter with Function Utilities

import * as R from 'remeda'
import { when, tap, converge } from '@ontologyio/receta/function'

// Use Remeda's pipe for left-to-right composition
const processNumbers = R.pipe(
  [1, 2, 3, 4, 5, -1, -2],
  tap((nums) => console.log('Input:', nums)),
  // Remeda's map with when for conditional transformation
  R.map(when((n: number) => n < 0, Math.abs)),
  tap((nums) => console.log('Absolute values:', nums)),
  // Remeda's filter
  R.filter((n) => n > 2),
  tap((nums) => console.log('Filtered (>2):', nums)),
  // Remeda's reduce
  R.reduce((sum, n) => sum + n, 0),
  tap((sum) => console.log('Sum:', sum))
)
// => 12

Advanced Pipelines

import * as R from 'remeda'
import { when, tap, converge, juxt } from '@ontologyio/receta/function'

interface Product {
  id: string
  name: string
  price: number
  category: string
}

// Analyze products using Remeda + Function combinators
const analyzeProducts = (products: Product[]) =>
  R.pipe(
    products,
    // Use Remeda's map with when for conditional pricing
    R.map(when(
      (p: Product) => p.category === 'sale',
      (p) => ({ ...p, price: p.price * 0.9 })
    )),
    tap((prods) => logger.info('Prices adjusted', { count: prods.length })),

    // Use converge to extract multiple metrics at once
    converge(
      (total, count, categoryCount, avgPrice) => ({
        totalValue: total,
        productCount: count,
        avgPrice,
        uniqueCategories: categoryCount
      }),
      [
        // Use Remeda's sumBy for cleaner sum
        (prods: Product[]) => R.sumBy(prods, (p) => p.price),
        (prods: Product[]) => prods.length,
        (prods: Product[]) => R.pipe(prods, R.map(p => p.category), R.unique).length,
        (prods: Product[]) => {
          const total = R.sumBy(prods, (p) => p.price)
          return total / prods.length
        }
      ]
    ),
    tap((analytics) => logger.info('Analysis complete', analytics))
  )

Integration with Predicate Module

Combining cond with Predicate Builders

import * as R from 'remeda'
import { cond, when, unless } from '@ontologyio/receta/function'
import { where, gt, lt, between, and, oneOf } from '@ontologyio/receta/predicate'
import { unwrapOr } from '@ontologyio/receta/option'

type Product = {
  id: string
  name: string
  price: number
  stock: number
  category: string
  featured: boolean
}

// Build reusable predicates
const isExpensive = where<Product>({ price: gt(1000) })
const isAffordable = where<Product>({ price: lt(100) })
const isInStock = where<Product>({ stock: gt(0) })
const isFeatured = where<Product>({ featured: (x) => x === true })
const isElectronics = where<Product>({ category: oneOf(['laptop', 'phone', 'tablet']) })

// Use cond with predicates for classification
const classifyProduct = cond<Product, string>([
  [and(isExpensive, isFeatured, isInStock), (p) => `💎 Premium: ${p.name}`],
  [and(isAffordable, isInStock), (p) => `✓ Budget-Friendly: ${p.name}`],
  [and(isElectronics, isInStock), (p) => `⚡ Tech Deal: ${p.name}`],
  [isExpensive, (p) => `💰 Luxury: ${p.name}`],
  [(p) => p.stock === 0, (p) => `✗ Out of Stock: ${p.name}`],
])

// Process products with predicates
const processProducts = (products: Product[]) =>
  R.pipe(
    products,
    // Filter using predicate combinators
    R.filter(and(isInStock, or(isAffordable, isFeatured))),
    // Classify each product
    R.map((p) => unwrapOr(classifyProduct(p), `Unknown: ${p.name}`)),
    tap((results) => logger.info('Classified products', { count: results.length }))
  )

When/Unless with Predicates

import { where, gt, and } from '@ontologyio/receta/predicate'

type User = {
  age: number
  verified: boolean
  premium: boolean
}

// Define predicates
const isAdult = where<User>({ age: gt(18) })
const isVerified = where<User>({ verified: (x) => x === true })
const isPremium = where<User>({ premium: (x) => x === true })
const isFullAccess = and(isAdult, isVerified)

// Use predicates in conditionals
const processUser = (user: User) =>
  R.pipe(
    user,
    // Add verification badge if verified
    when(isVerified, (u) => ({ ...u, badge: 'verified' })),
    // Upgrade to premium features if eligible
    when(
      and(isFullAccess, isPremium),
      (u) => ({ ...u, features: ['advanced-analytics', 'priority-support'] })
    ),
    // Add age restriction notice unless adult
    unless(isAdult, (u) => ({ ...u, notice: 'Parental consent required' })),
    tap((u) => logger.debug('User processed', { hasFeatures: 'features' in u }))
  )

Integration with Collection Module

Using nest and groupBy with Function Combinators

import * as R from 'remeda'
import { converge, juxt, tap } from '@ontologyio/receta/function'
import { nest, indexByUnique, diff } from '@ontologyio/receta/collection'

interface Order {
  id: string
  userId: string
  region: string
  status: 'pending' | 'shipped' | 'delivered'
  total: number
}

// Analyze orders by multiple dimensions
const analyzeOrders = (orders: Order[]) =>
  R.pipe(
    orders,
    tap((ords) => logger.info('Analyzing orders', { count: ords.length })),

    // Use converge with collection operations
    converge(
      (byRegion, byStatus, metrics) => ({
        byRegion,
        byStatus,
        metrics
      }),
      [
        // Group by region using Remeda
        (ords: Order[]) => R.groupBy(ords, (o) => o.region),

        // Group by status
        (ords: Order[]) => R.groupBy(ords, (o) => o.status),

        // Extract metrics using juxt
        juxt([
          (ords: Order[]) => ords.length,
          (ords: Order[]) => R.sumBy(ords, (o) => o.total),
          (ords: Order[]) => R.uniqueBy(ords, (o) => o.userId).length,
          (ords: Order[]) => R.groupBy(ords, (o) => o.region)
        ])
      ]
    ),

    tap((analysis) => {
      const [total, revenue, uniqueUsers, regions] = analysis.metrics
      logger.info('Analysis complete', {
        total,
        revenue,
        uniqueUsers,
        regionCount: Object.keys(regions).length
      })
    })
  )

Combining diff with Conditional Logic

import { diff } from '@ontologyio/receta/collection'
import { when, tap, ifElse } from '@ontologyio/receta/function'

interface Item {
  id: string
  name: string
  quantity: number
}

// Track inventory changes
const syncInventory = (oldItems: Item[], newItems: Item[]) =>
  R.pipe(
    diff(oldItems, newItems, (item) => item.id),

    tap((changes) => {
      logger.info('Inventory changes detected', {
        added: changes.added.length,
        removed: changes.removed.length,
        updated: changes.updated.length
      })
    }),

    // Process based on change type
    (changes) => ({
      added: R.pipe(
        changes.added,
        R.map(when(
          (item) => item.quantity > 100,
          (item) => ({ ...item, bulkOrder: true })
        )),
        tap((items) => items.forEach(item => notifyWarehouse('restock', item)))
      ),

      removed: R.pipe(
        changes.removed,
        tap((items) => items.forEach(item => notifyWarehouse('removed', item)))
      ),

      updated: R.pipe(
        changes.updated,
        R.map(({ old, new: newItem }) =>
          ifElse(
            () => newItem.quantity < old.quantity,
            () => ({ ...newItem, status: 'decreased' }),
            () => ({ ...newItem, status: 'increased' }),
            newItem.quantity
          )
        )
      )
    })
  )

Real-World Pattern: API Client

import { pipe } from 'remeda'
import { tap, partial } from '@ontologyio/receta/function'
import { flatMap, isOk, map, tryCatchAsync, unwrapOr } from '@ontologyio/receta/result'

interface ApiConfig {
  baseUrl: string
  headers: Record<string, string>
}

interface ApiError {
  status: number
  message: string
  timestamp: string
}

const createApiClient = (config: ApiConfig) => {
  const fetchApi = async (endpoint: string, options?: RequestInit) => {
    const url = `${config.baseUrl}${endpoint}`
    logger.debug('Fetching', { url })

    // tryCatch from this module is for sync code. Fetching needs tryCatchAsync.
    const result = await tryCatchAsync(
      async () => {
        const response = await fetch(url, {
          ...options,
          headers: { ...config.headers, ...(options?.headers || {}) }
        })

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}`)
        }

        return response.json()
      },
      (error): ApiError => ({
        status: 500,
        message: error instanceof Error ? error.message : String(error),
        timestamp: new Date().toISOString()
      })
    )

    return pipe(
      result,
      tap((result) => {
        if (isOk(result)) {
          logger.debug('Fetch successful')
        } else {
          logger.error('Fetch failed', result.error)
        }
      })
    )
  }
  
  return {
    get: partial(fetchApi),
    post: (endpoint: string, body: any) =>
      fetchApi(endpoint, {
        method: 'POST',
        body: JSON.stringify(body)
      })
  }
}

// Usage
const api = createApiClient({
  baseUrl: 'https://api.example.com',
  headers: { 'Authorization': `Bearer ${token}` }
})

const user = await pipe(
  await api.get('/users/123'),
  map((data: User) => ({
    ...data,
    fetchedAt: new Date()
  })),
  flatMap(validateUser),
  unwrapOr(null)
)

Real-World Pattern: Form Validation

import { pipe } from 'remeda'
import { when, unless, tap } from '@ontologyio/receta/function'
import { ok, err, flatMap, isOk, map } from '@ontologyio/receta/result'

interface FormData {
  email: string
  password: string
  age?: number
}

interface ValidationError {
  field: string
  message: string
}

// Individual validators
const validateEmail = (email: string): Result<string, ValidationError> =>
  email.includes('@')
    ? ok(email.toLowerCase())
    : err({ field: 'email', message: 'Invalid email' })

const validatePassword = (password: string): Result<string, ValidationError> =>
  password.length >= 8
    ? ok(password)
    : err({ field: 'password', message: 'Password too short' })

const validateAge = (age?: number): Result<number, ValidationError> => {
  if (age === undefined) return ok(18) // default
  return age >= 18
    ? ok(age)
    : err({ field: 'age', message: 'Must be 18 or older' })
}

// Normalize form
const normalizeForm = (data: FormData): FormData =>
  pipe(
    data,
    when(
      (d) => d.email.includes(' '),
      (d) => ({ ...d, email: d.email.trim() })
    ),
    unless(
      (d) => d.age !== undefined,
      (d) => ({ ...d, age: 18 })
    )
  )

// Validate entire form
const validateForm = (data: FormData) =>
  pipe(
    data,
    normalizeForm,
    tap((form) => logger.debug('Normalized form', form)),
    // Each field has its own type, so chain them instead of using collect
    (form) =>
      flatMap(validateEmail(form.email), (email) =>
        flatMap(validatePassword(form.password), (password) =>
          map(validateAge(form.age), (age) => ({ email, password, age }))
        )
      )
  )

// Usage
const result = validateForm({
  email: ' USER@EXAMPLE.COM ',
  password: 'password123'
})

if (isOk(result)) {
  console.log('Valid:', result.value)
} else {
  console.error('Errors:', result.error)
}

Best Practices

1. Prefer Result for Error Details

// ✅ Result when you need error context
const parse = tryCatch(
  JSON.parse,
  (error) => ({ code: 'PARSE_ERROR', original: error })
)

// ✅ Option when error details don't matter
import { fromNullable } from '@ontologyio/receta/option'

const findUser = (id: string): Option<User> =>
  fromNullable(users.find(u => u.id === id))

2. Use tap for Side Effects in Pipelines

// ✅ Good: non-intrusive logging
pipe(
  data,
  tap(logInput),
  transform,
  tap(logTransformed),
  validate
)

// ❌ Bad: breaks the pipeline
pipe(
  data,
  (d) => { console.log(d); return d },
  transform
)

3. Combine Conditionals with Validation

import { piped } from 'remeda'

const processUser = piped(
  normalizeEmail,
  when(needsVerification, sendVerificationEmail),
  flatMap(validateUser),
  tap(auditLog),
  unless(isActive, activateUser)
)

4. Handle All Result/Option Cases

// ✅ Always handle both cases
const result = pipe(
  fetchData(),
  match({
    onOk: (data) => `Success: ${data}`,
    onErr: (error) => `Error: ${error}`
  })
)

// ❌ Don't unwrap without default
const result = unwrap(fetchData()) // Can throw!

Next Steps

On this page