typescript-monads
Version:
Write cleaner TypeScript
643 lines • 25 kB
JavaScript
var __read = (this && this.__read) || function (o, n) {
var m = typeof Symbol === "function" && o[Symbol.iterator];
if (!m) return o;
var i = m.call(o), r, ar = [], e;
try {
while ((n === void 0 || n-- > 0) && !(r = i.next()).done) ar.push(r.value);
}
catch (error) { e = { error: error }; }
finally {
try {
if (r && !r.done && (m = i["return"])) m.call(i);
}
finally { if (e) throw e.error; }
}
return ar;
};
var __spreadArray = (this && this.__spreadArray) || function (to, from, pack) {
if (pack || arguments.length === 2) for (var i = 0, l = from.length, ar; i < l; i++) {
if (ar || !(i in from)) {
if (!ar) ar = Array.prototype.slice.call(from, 0, i);
ar[i] = from[i];
}
}
return to.concat(ar || Array.prototype.slice.call(from));
};
/**
* Implementation of the Reader monad for handling environment or configuration-based computations.
*
* @typeParam TConfig - The environment/configuration type
* @typeParam TOut - The result type
*/
var Reader = /** @class */ (function () {
function Reader(fn) {
this.fn = fn;
}
/**
* 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
*/
Reader.of = function (value) {
return new Reader(function () { return value; });
};
/**
* 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);
*/
Reader.ask = function () {
return new Reader(function (config) { return config; });
};
/**
* 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'
*/
Reader.asks = function (accessor) {
return new Reader(accessor);
};
/**
* 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']
*/
Reader.sequence = function (readers) {
return new Reader(function (config) { return readers.map(function (reader) { return reader.run(config); }); });
};
/**
* 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 }
* );
*/
Reader.traverse = function (readers, reducer, initialValue) {
return new Reader(function (config) {
return readers.reduce(function (acc, reader, index) { return reducer(acc, reader.run(config), index); }, initialValue);
});
};
/**
* 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
* })
* );
*/
Reader.combine = function (readers, fn) {
return new Reader(function (config) {
var values = readers.map(function (reader) { return reader.run(config); });
return fn.apply(void 0, __spreadArray([], __read(values), false));
});
};
/**
* 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
*/
Reader.prototype.of = function (fn) {
return new Reader(fn);
};
/**
* 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
*/
Reader.prototype.map = function (fn) {
var _this = this;
return new Reader(function (c) { return fn(_this.run(c)); });
};
/**
* 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!");
*/
Reader.prototype.mapTo = function (val) {
return this.map(function () { return val; });
};
/**
* 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
*/
Reader.prototype.flatMap = function (fn) {
var _this = this;
return new Reader(function (c) { return fn(_this.run(c)).run(c); });
};
/**
* 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!"
*/
Reader.prototype.run = function (config) {
return this.fn(config);
};
/**
* 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...
* });
*/
Reader.prototype.local = function (fn) {
var _this = this;
return new Reader(function (c) { return _this.run(fn(c)); });
};
/**
* 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
*/
Reader.prototype.zipWith = function (other, fn) {
var _this = this;
return new Reader(function (c) { return fn(_this.run(c), other.run(c)); });
};
/**
* 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);
*/
Reader.prototype.tap = function (fn) {
var _this = this;
return new Reader(function (c) {
var result = _this.run(c);
fn(result);
return result;
});
};
/**
* 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
*/
Reader.prototype.andThen = function (other) {
var _this = this;
return new Reader(function (c) {
_this.run(c); // Run for side effects but ignore result
return other.run(c);
});
};
/**
* 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)
* );
*/
Reader.prototype.andFinally = function (other) {
var _this = this;
return new Reader(function (c) {
var result = _this.run(c);
other.run(c); // Run for side effects but ignore result
return result;
});
};
/**
* 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
* );
* }
* );
*/
Reader.prototype.withEnv = function (fn) {
var _this = this;
return new Reader(function (c) { return fn(c, _this.run(c)); });
};
/**
* 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
* );
*/
Reader.prototype.filter = function (predicate, defaultValue) {
var _this = this;
return new Reader(function (c) {
var result = _this.run(c);
return predicate(result) ? result : defaultValue;
});
};
/**
* 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]
*/
Reader.prototype.fanout = function () {
var _this = this;
var fns = [];
for (var _i = 0; _i < arguments.length; _i++) {
fns[_i] = arguments[_i];
}
return new Reader(function (c) {
var value = _this.run(c);
return fns.map(function (fn) { return fn(value); });
});
};
/**
* 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);
* }
* }
*/
Reader.prototype.toPromise = function () {
var _this = this;
return function (env) { return Promise.resolve(_this.run(env)); };
};
/**
* 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
*/
Reader.prototype.memoize = function (cacheKeyFn) {
var _this = this;
var cache = new Map();
return new Reader(function (c) {
var key = cacheKeyFn ? cacheKeyFn(c) : c;
if (cache.has(key)) {
return cache.get(key);
}
var result = _this.run(c);
cache.set(key, result);
return result;
});
};
return Reader;
}());
export { Reader };
//# sourceMappingURL=reader.js.map