UNPKG

typescript-monads

Version:
529 lines 22.1 kB
import { IReader } from './reader.interface'; /** * Implementation of the Reader monad for handling environment or configuration-based computations. * * @typeParam TConfig - The environment/configuration type * @typeParam TOut - The result type */ export declare class Reader<TConfig, TOut> implements IReader<TConfig, TOut> { private readonly fn; constructor(fn: (config: TConfig) => TOut); /** * Creates a Reader that always returns a constant value, ignoring the environment. * * This is the "pure" or "return" operation for the Reader monad. * * @param value - The constant value to return * @returns A Reader that always produces the given value * * @example * // Create a Reader that always returns 42 * const constReader = Reader.of(42); * constReader.run(anyEnvironment) // Returns 42 */ static of<TConfig, TOut>(value: TOut): IReader<TConfig, TOut>; /** * Creates a Reader that returns the environment itself. * * This is the "ask" operation in ReaderT parlance, which gives access to the * entire environment. * * @returns A Reader that returns its environment * * @example * // Create a Reader that provides access to its environment * const askReader = Reader.ask<Config>(); * * // Use it to extract a specific part of the environment * const getApiUrl = askReader.map(config => config.apiUrl); */ static ask<TConfig>(): IReader<TConfig, TConfig>; /** * Creates a new Reader that accesses a specific part of the environment. * * @typeParam TConfig - The environment type * @typeParam TOut - The type of the property to access * @param accessor - Function that extracts a value from the environment * @returns A Reader that returns the specified part of the environment * * @example * // Create a Reader that accesses the apiUrl from a config object * const getApiUrl = Reader.asks<AppConfig, string>(config => config.apiUrl); * * // Run it with a configuration * const url = getApiUrl.run({ apiUrl: 'https://api.example.com', timeout: 5000 }); * // url is 'https://api.example.com' */ static asks<TConfig, TOut>(accessor: (config: TConfig) => TOut): IReader<TConfig, TOut>; /** * Combines multiple Readers into a single Reader that returns an array of results. * * This is useful for running multiple independent operations that share the same * environment and collecting their results. * * @param readers - Array of Readers to combine * @returns A Reader that produces an array of all results * * @example * // Define individual Readers * const getName = Reader.asks<UserConfig, string>(c => c.name); * const getAge = Reader.asks<UserConfig, number>(c => c.age); * const getEmail = Reader.asks<UserConfig, string>(c => c.email); * * // Combine them to get all user info at once * const getUserInfo = Reader.sequence([getName, getAge, getEmail]); * * // Run with a configuration * const userInfo = getUserInfo.run({ * name: 'Alice', * age: 30, * email: 'alice@example.com' * }); * // userInfo is ['Alice', 30, 'alice@example.com'] */ static sequence<TConfig, TOut>(readers: Array<IReader<TConfig, TOut>>): IReader<TConfig, TOut[]>; /** * Combines multiple Readers into a single Reader, aggregating their results with a reducer function. * * This is useful for combining the results of multiple operations that share * the same environment into a single value. * * @typeParam TConfig - The shared environment type * @typeParam TOut - The type of each Reader's output * @typeParam TAcc - The type of the accumulated result * @param readers - Array of Readers to combine * @param reducer - Function that combines the results * @param initialValue - Initial value for the accumulator * @returns A Reader that produces the aggregated result * * @example * interface Dependencies { * userService: UserService; * orderService: OrderService; * } * * // Define Readers for different stats * const getActiveUsers = Reader.asks<Dependencies, number>( * deps => deps.userService.countActiveUsers() * ); * const getPendingOrders = Reader.asks<Dependencies, number>( * deps => deps.orderService.countPendingOrders() * ); * const getCompletedOrders = Reader.asks<Dependencies, number>( * deps => deps.orderService.countCompletedOrders() * ); * * // Combine into a dashboard stats object * const getDashboardStats = Reader.traverse( * [getActiveUsers, getPendingOrders, getCompletedOrders], * (acc, count, index) => { * if (index === 0) acc.activeUsers = count; * else if (index === 1) acc.pendingOrders = count; * else if (index === 2) acc.completedOrders = count; * return acc; * }, * { activeUsers: 0, pendingOrders: 0, completedOrders: 0 } * ); */ static traverse<TConfig, TOut, TAcc>(readers: Array<IReader<TConfig, TOut>>, reducer: (acc: TAcc, value: TOut, index: number) => TAcc, initialValue: TAcc): IReader<TConfig, TAcc>; /** * Combines the results of multiple Readers with a mapping function. * * This provides a more ergonomic way to combine several Readers' outputs * into a single value using a dedicated mapping function. * * @typeParam Args - Tuple type of Reader results * @typeParam R - The type of the combined result * @param readers - Tuple of Readers * @param fn - Function to combine the results * @returns A Reader that produces the combined result * * @example * // Define individual Readers * const getUser = Reader.asks<AppDeps, User>(deps => deps.userService.getCurrentUser()); * const getPermissions = Reader.asks<AppDeps, string[]>(deps => deps.authService.getPermissions()); * const getSettings = Reader.asks<AppDeps, UserSettings>(deps => deps.settingsService.getUserSettings()); * * // Combine them into a user profile object * const getUserProfile = Reader.combine( * [getUser, getPermissions, getSettings], * (user, permissions, settings) => ({ * id: user.id, * name: user.name, * permissions, * theme: settings.theme, * notifications: settings.notifications * }) * ); */ static combine<TConfig, Args extends unknown[], R>(readers: { [K in keyof Args]: IReader<TConfig, Args[K]>; }, fn: (...args: Args) => R): IReader<TConfig, R>; /** * Creates a new Reader with the given function. * * @param fn - Function that takes a configuration and returns a value * @returns A new Reader containing the provided function */ of(fn: (config: TConfig) => TOut): IReader<TConfig, TOut>; /** * Maps the output of this Reader using the provided function. * * Creates a new Reader that first applies this Reader's function to the configuration, * then applies the provided mapping function to the result. * * @typeParam TNewOut - The type of the mapped result * @param fn - Function to transform the output value * @returns A new Reader that produces the transformed output * * @example * // Create a Reader that gets a user object * const getUser = Reader.asks<AppDeps, User>(deps => deps.userService.getCurrentUser()); * * // Map it to extract just the user's name * const getUserName = getUser.map(user => user.name); * * // When run, getUserName will return just the name string */ map<TNewOut>(fn: (val: TOut) => TNewOut): IReader<TConfig, TNewOut>; /** * Maps the output to a constant value. * * Creates a new Reader that produces the specified value, ignoring the actual output * of the original Reader's computation. * * @typeParam TNewOut - The type of the new constant value * @param val - The constant value to return * @returns A new Reader that always produces the specified value * * @example * // Create a Reader that performs a validation * const validateUser = Reader.asks<UserData, boolean>(data => { * return data.username.length >= 3 && data.password.length >= 8; * }); * * // Map to specific success/error messages * const successMessage = validateUser * .filter(isValid => isValid, false) * .mapTo("Validation successful!"); * * const errorMessage = validateUser * .filter(isValid => !isValid, true) * .mapTo("Validation failed!"); */ mapTo<TNewOut>(val: TNewOut): IReader<TConfig, TNewOut>; /** * Chains this Reader with another Reader-producing function. * * Creates a new Reader that first applies this Reader's function to the configuration, * then passes the result to the provided function to get a new Reader, which is then * run with the same configuration. * * @typeParam TNewOut - The type of the final result * @param fn - Function that takes the output of this Reader and returns a new Reader * @returns A new Reader representing the composed operation * * @example * // Get a user from the configuration * const getUser = Reader.asks<AppConfig, User>(config => config.currentUser); * * // Define a function that creates a Reader to get user permissions * const getPermissions = (user: User) => Reader.asks<AppConfig, string[]>( * config => config.authService.getPermissionsForUser(user.id) * ); * * // Chain the Readers to get the user's permissions * const getUserPermissions = getUser.flatMap(getPermissions); * * // When run, getUserPermissions will return the permissions array */ flatMap<TNewOut>(fn: (val: TOut) => IReader<TConfig, TNewOut>): IReader<TConfig, TNewOut>; /** * Executes the Reader's function with the provided configuration. * * @param config - The configuration to use * @returns The result of applying the Reader's function to the configuration * * @example * // Create a Reader that greets a user * const greetUser = Reader.asks<{name: string}, string>(config => `Hello, ${config.name}!`); * * // Run the Reader with a configuration * const greeting = greetUser.run({ name: 'Alice' }); * // greeting is "Hello, Alice!" */ run(config: TConfig): TOut; /** * Creates a new Reader that applies the given function to the environment before * passing it to this Reader. * * This is used for modifying the environment/configuration before it reaches the * Reader's main function. * * @typeParam TNewConfig - The type of the new environment * @param fn - Function to transform the environment * @returns A new Reader that operates on the new environment type * * @example * // Create a Reader that requires specific config * const getDatabaseUrl = Reader.asks<DatabaseConfig, string>( * config => `${config.protocol}://${config.host}:${config.port}/${config.database}` * ); * * // Make it work with a different config format * const getDbUrlFromAppConfig = getDatabaseUrl.local<AppConfig>( * appConfig => appConfig.database * ); * * // Now it can be run with the app config * const url = getDbUrlFromAppConfig.run({ * database: { * protocol: 'postgres', * host: 'localhost', * port: 5432, * database: 'myapp' * }, * // other app config... * }); */ local<TNewConfig>(fn: (config: TNewConfig) => TConfig): IReader<TNewConfig, TOut>; /** * Applies a binary function to the results of two Readers. * * This method allows you to combine the results of this Reader with another Reader, * using both results as inputs to the provided combining function. * * @typeParam TNewOut - The type of the other Reader's result * @typeParam TCombined - The type of the combined result * @param other - Another Reader to combine with this one * @param fn - Function that combines both Reader results * @returns A new Reader that produces the combined result * * @example * // Get username from config * const getUsername = Reader.asks<UserConfig, string>(config => config.username); * * // Get greeting template from config * const getGreeting = Reader.asks<UserConfig, string>(config => config.greetingTemplate); * * // Combine them to create a personalized greeting * const personalizedGreeting = getGreeting.zipWith( * getUsername, * (template, username) => template.replace('{name}', username) * ); * * // When run with config, personalizedGreeting will return the formatted greeting */ zipWith<TNewOut, TCombined>(other: IReader<TConfig, TNewOut>, fn: (a: TOut, b: TNewOut) => TCombined): IReader<TConfig, TCombined>; /** * Executes side-effect functions and returns the original Reader for chaining. * * This method allows you to perform an action using the Reader's value without * affecting the Reader itself. * * @param fn - A function to execute with the Reader's result value * @returns This Reader unchanged, for chaining * * @example * // Create a Reader to fetch data * const fetchUserData = Reader.asks<AppDeps, UserData>( * deps => deps.userService.getCurrentUser() * ); * * // Use tap for logging without affecting the chain * const loggedFetchUserData = fetchUserData.tap( * userData => console.log('User data fetched:', userData) * ); * * // Continue with the chain using the original data * const userName = loggedFetchUserData.map(userData => userData.name); */ tap(fn: (val: TOut) => void): IReader<TConfig, TOut>; /** * Combines this Reader with another, ignoring the result of this Reader. * * This is useful when you want to perform a computation for its effects, * but use the result of another Reader. * * @typeParam TNewOut - The type of the other Reader's result * @param other - The Reader whose result will be used * @returns A new Reader that produces the second Reader's result * * @example * // Create a Reader that logs an action * const logAction = Reader.asks<AppDeps, void>( * deps => deps.logger.log('Action performed') * ); * * // Create a Reader that gets a result * const getResult = Reader.asks<AppDeps, string>( * deps => deps.service.getResult() * ); * * // Combine them to log and then return the result * const logAndGetResult = logAction.andThen(getResult); * * // Result will be the string from getResult */ andThen<TNewOut>(other: IReader<TConfig, TNewOut>): IReader<TConfig, TNewOut>; /** * Combines this Reader with another, ignoring the result of the other Reader. * * This is useful when you want to perform another computation for its effects, * but keep the result of this Reader. * * @typeParam TNewOut - The type of the other Reader's result * @param other - The Reader to execute after this one * @returns A new Reader that produces this Reader's result * * @example * // Create a Reader that gets a validation result * const validateInput = Reader.asks<FormData, ValidationResult>( * data => validateForm(data) * ); * * // Create a Reader that logs the result * const logValidation = (result: ValidationResult) => Reader.asks<FormData, void>( * data => console.log(`Validation of form ${data.id}:`, result) * ); * * // Combine them to validate, log, but return the original validation result * const validateAndLog = validateInput.flatMap( * result => logValidation(result).mapTo(result) * ); * * // A more elegant solution using andFinally * const validateAndLog2 = validateInput.andFinally( * validateInput.flatMap(logValidation) * ); */ andFinally<TNewOut>(other: IReader<TConfig, TNewOut>): IReader<TConfig, TOut>; /** * Applies a function from the environment to transform the Reader's output. * * This method combines aspects of both `map` and `local` - it allows a transformation * based on both the environment and the current output value. * * @typeParam TNewOut - The type of the transformed result * @param fn - Function that takes the environment and the current output to produce a new output * @returns A new Reader that applies the environment-aware transformation * * @example * // Create a Reader that gets the base translation * const getBaseTranslation = Reader.asks<MessageConfig, string>( * config => config.translations[config.messageId] * ); * * // Use withEnv to apply variables from the environment * const getFormattedMessage = getBaseTranslation.withEnv( * (config, message) => { * // Replace variables in the message with values from config * return Object.entries(config.variables).reduce( * (msg, [key, value]) => msg.replace(`{${key}}`, value), * message * ); * } * ); */ withEnv<TNewOut>(fn: (env: TConfig, val: TOut) => TNewOut): IReader<TConfig, TNewOut>; /** * Filters the output value using a predicate function. * * If the predicate returns true for the output value, the original value is kept. * If the predicate returns false, the provided default value is used instead. * * @param predicate - Function to test the output value * @param defaultValue - Value to use if the predicate returns false * @returns A new Reader with the filtered value * * @example * // Create a Reader that fetches a user's age * const getUserAge = Reader.asks<UserProfile, number>(profile => profile.age); * * // Filter to ensure the age is valid * const getValidUserAge = getUserAge.filter( * age => age >= 0 && age <= 120, // Valid age range * 0 // Default value for invalid ages * ); */ filter(predicate: (val: TOut) => boolean, defaultValue: TOut): IReader<TConfig, TOut>; /** * Composes multiple transformations of the same environment. * * This creates a new Reader that runs all provided transformation functions * and returns an array of their results. * * @param fns - Functions that transform the environment to various results * @returns A Reader that produces an array of all transformation results * * @example * // Create a Reader that gets user data * const getUserData = Reader.asks<UserProfile, UserData>(profile => profile.userData); * * // Use fanout to apply multiple transformations to the user data * const getUserStats = getUserData.fanout( * data => data.loginCount, * data => data.lastLoginDate, * data => data.activeSubscriptions.length, * data => data.preferences.theme * ); * * // Result will be an array [loginCount, lastLoginDate, subscriptionCount, theme] */ fanout<B extends unknown[]>(...fns: Array<(val: TOut) => B[number]>): IReader<TConfig, B>; /** * Creates a Reader that runs asynchronously, returning a Promise. * * This method transforms a Reader<E, A> into a function that takes an environment E * and returns a Promise<A>, allowing for integration with async/await code. * * @returns A function that takes an environment and returns a Promise with the result * * @example * // Create a Reader that performs a synchronous operation * const processData = Reader.asks<DataConfig, ProcessedData>( * config => processDataSynchronously(config.data, config.options) * ); * * // Convert to a Promise-based function for use in async code * const processDataAsync = processData.toPromise(); * * // Use in an async function * async function handleDataProcessing(config: DataConfig) { * try { * const result = await processDataAsync(config); * saveResults(result); * } catch (error) { * handleError(error); * } * } */ toPromise(): (env: TConfig) => Promise<TOut>; /** * Creates a Reader that caches its result for repeated calls with the same environment. * * This optimization is useful when the Reader's computation is expensive and * the same environment is used multiple times. * * @param cacheKeyFn - Optional function to derive a cache key from the environment * @returns A new Reader that caches results based on the environment * * @example * // Create a Reader for an expensive computation * const computeAnalytics = Reader.asks<AnalyticsConfig, AnalyticsResult>( * config => performExpensiveAnalytics(config.data, config.options) * ); * * // Create a memoized version that will cache results * const memoizedAnalytics = computeAnalytics.memoize( * // Custom cache key based on relevant parts of the config * config => `${config.dataVersion}-${config.options.precision}` * ); * * // Running multiple times with the same config will reuse the cached result * const result1 = memoizedAnalytics.run(config); // Computed * const result2 = memoizedAnalytics.run(config); // Retrieved from cache */ memoize(cacheKeyFn?: (env: TConfig) => string | number): IReader<TConfig, TOut>; } //# sourceMappingURL=reader.d.ts.map