/** * ReadonlyRecord is a readonly product structure that operates * like a Map. Keys are always strings and Key/Value pairs * can be added and removed arbitrarily. The ReadonlyRecord * type in fun favors immutability. * * @module ReadonlyRecord * @since 2.0.0 */ // deno-lint-ignore-file no-explicit-any import type { $, AnySub, Intersect, Kind, Out } from "./kind.ts"; import type { Applicable } from "./applicable.ts"; import type { Combinable } from "./combinable.ts"; import type { Comparable } from "./comparable.ts"; import type { Either } from "./either.ts"; import type { Filterable } from "./filterable.ts"; import type { Foldable } from "./foldable.ts"; import type { Initializable } from "./initializable.ts"; import type { Mappable } from "./mappable.ts"; import type { Option } from "./option.ts"; import type { Pair } from "./pair.ts"; import type { Showable } from "./showable.ts"; import type { Traversable } from "./traversable.ts"; import { isRight } from "./either.ts"; import { isSome, none, some } from "./option.ts"; import { pair } from "./pair.ts"; import { identity, pipe, uncurry2 } from "./fn.ts"; /** * ReadonlyRecord is an alias of Readonly>. * It's meant to be used wherever a Record can be used. * * @since 2.0.0 */ export type ReadonlyRecord = Readonly>; /** * A type used to constrain an input to a ReadonlyRecord with any values. * * @since 2.0.0 */ export type AnyReadonlyRecord = ReadonlyRecord; /** * Extract the inner type of a ReadonlyRecord * * @since 2.0.0 */ export type TypeOf = T extends ReadonlyRecord ? A : never; /** * NonEmptyRecord is a bounding type that will * return a type level never if the type value of R is * either not a record is a record without any * index or key values. * * @example * ``` * import type { NonEmptyRecord } from "./record.ts"; * * function doSomething(_: NonEmptyRecord): void { * return undefined; * } * * const result = doSomething({ one: 1 }); // This is ok * // const result2 = doSomethign({}); // This is a type error * ``` * * @since 2.0.0 */ export type NonEmptyRecord = keyof R extends never ? never : R; /** * Specifies ReadonlyRecord as a Higher Kinded Type, with covariant * parameter A corresponding to the 0th index of any substitutions. * * @since 2.0.0 */ export interface KindReadonlyRecord extends Kind { readonly kind: ReadonlyRecord>; } /** * An alias of Object.entries * * @example * ```ts * import * as R from "./record.ts"; * * const data: R.ReadonlyRecord = { * hello: "world", * foo: "bar", * }; * * // [["hello", "world"], ["foo", "bar"]] * const result1 = R.entries(data); * const result2 = R.entries({}); // [] * ``` * * @since 2.0.0 */ export function entries(ua: ReadonlyRecord): ReadonlyArray<[string, A]> { return Object.entries(ua); } /** * An alias of Object.keys specific to ReadonlyRecord. * * @example * ```ts * import * as R from "./record.ts"; * * const data: R.ReadonlyRecord = { * hello: "world", * foo: "bar", * }; * * const result1 = R.keys(data); // ["hello", "foo"] * const result2 = R.keys({}); // [] * ``` * * @since 2.0.0 */ export function keys(ua: ReadonlyRecord): ReadonlyArray { return Object.keys(ua); } /** * Omit specified `keys` from a `record`. Value-space implementation of the * [`Omit`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys) * utility type. * * @example * ```ts * import { omit } from "./record.ts"; * omit("a", "c")({ a: 1, b: 2 }) // { b: 2 } * ``` * * @since 2.0.0 */ export function omit( ...keys: K ): (record: T) => Omit { return (record: T) => { const output = { ...record }; for (const key of keys as unknown as (keyof typeof output)[]) { delete output[key]; } return output; }; } /** * Picks specified `keys` from a `record`. Value-space implemenuation of the * [`Pick`](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) * utility type. * * @example * ```ts * import { pipe } from "./fn.ts"; * import { pick } from "./record.ts"; * * pipe({ a: 1, b: 2, c: 3 }, pick("a", "b")) * // { a: 1, b: 2 } * ``` * * @since 2.0.0 */ export function pick, K extends keyof T>( ...keys: readonly K[] ): (record: T) => Pick { return (record) => { const output = {} as Pick; for (const key of keys) { if (key in record) { output[key] = record[key]; } } return output; }; } /** * Creates a new object with the same keys of `ua`. Values are transformed * using `fai`. * * @example * ```ts * import { map } from "./record.ts" * map((n: number) => n + 1)({ a: 1 }); // { a: 2 } * ``` * * @since 2.0.0 */ export function map( fai: (a: A, i: string) => I, ): (ua: ReadonlyRecord) => ReadonlyRecord { return (ua) => { const out = {} as Record; for (const [key, entry] of Object.entries(ua) as [string, A][]) { out[key] = fai(entry, key); } return out; }; } /** * Collect all of the A values in a ReadonlyRecord into a single * O value by the process of reduction. The order of key/value pairs * in this reduction are stable and determined by ecmascript standard * [here](https://262.ecma-international.org/8.0/#sec-enumerate-object-properties). * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * { one: 1, two: 2 }, * R.fold((sum, value) => sum + value, 0), * ); // 3 * ``` * * @since 2.0.0 */ export function fold( foao: (o: O, a: A, i: string) => O, o: O, ): (ua: ReadonlyRecord) => O { return (ua) => { let result = o; for (const key in ua) { result = foao(result, ua[key], key); } return result; }; } /** * Collect all values in a ReadonlyRecord into a single * value I by using a Combinable and a mapping function * from A to I. This is effectively fold using a Combinable * for the initial value. * * @example * ```ts * import * as R from "./record.ts"; * import { InitializableNumberSum } from "./number.ts"; * import { pipe } from "./fn.ts"; * * const collectSum = R.collect(InitializableNumberSum); * const collectLengths = collectSum((s: string) => s.length); * * const result = collectLengths({ one: "one", two: "two" }); // 6 * ``` * * @since 2.0.0 */ export function collect( M: Initializable, ): (fai: (a: A, index: string) => I) => (ua: ReadonlyRecord) => I { return (fai: (a: A, index: string) => I) => { const foldr = fold( (i, a: A, index) => M.combine(fai(a, index))(i), M.init(), ); return (ua: ReadonlyRecord) => foldr(ua); }; } /** * Collect all values in a ReadonlyRecord into a single * value I by using a Combinable. This is effectively fold * using a Combinable for the initial value and combination. * * @example * ```ts * import * as R from "./record.ts"; * import { InitializableNumberSum } from "./number.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * { one: 1, two: 2, three: 3 }, * R.collapse(InitializableNumberSum), * ); // 6 * ``` * * @since 2.0.0 */ export function collapse( M: Initializable, ): (ua: ReadonlyRecord) => A { return fold(uncurry2(M.combine), M.init()); } /** * Traverse a ReadonlyRecord, mapping each A into an * algebraic data type V (so V), then collecting each * I in V back into a ReadonlyRecord, ultimately * returning V>. In more concrete terms * this can take ReadonlyRecord> and return * Option> (or any other inner type. * * Traverse, in general, is much like reducing and collecting * over the outer and inner types of an ADT at the same time. * * @example * ```ts * import * as R from "./record.ts"; * import * as O from "./option.ts"; * import { pipe } from "./fn.ts"; * * const traverseOption = R.traverse(O.ApplicableOption); * const swapOption = traverseOption((o: O.Option) => o); * * const result1 = swapOption({ one: O.some(1), two: O.some(2) }); * // Some({ one: 1, two: 2 }); * const result2 = swapOption({ one: O.some(1), two: O.none }); // None * ``` * * TODO: Revisit because mutability needs proof * @since 2.0.0 */ export function traverse( A: Applicable, ): ( favi: (value: A, key: string) => $, ) => (ua: ReadonlyRecord) => $, J, K], [L], [M]> { // We include a copy of the type parameters here to make the implementation // type safe. return ( favi: (a: A, i: string) => $, ): (ua: ReadonlyRecord) => $, J, K], [L], [M]> => { // Mutably pushes an i into is at key const pusher = (key: string) => (is: Record) => ( i: I, ): Record => ({ ...is, [key]: i }); // Interior mutability is used to increase perf const foldr = ( vis: $, J, K], [L], [M]>, a: A, key: string, ): $, J, K], [L], [M]> => pipe( vis, A.map(pusher(key)), A.apply(favi(a, key)), ); return (ua) => pipe(ua, fold(foldr, A.wrap({}))); }; } /** * The Sequence inverts a tuple of substitutions over V into V containing a * tuple of inferred values of the substitution. * * ie. * [Option, Option] * becomes * Option<[number, string]> * * or * * [Either Either] * becomes * Either */ // deno-fmt-ignore type Sequence>> = $ ? A : never; }, { [K in keyof R]: R[K] extends $ ? B : never; }[keyof R], { [K in keyof R]: R[K] extends $ ? C : never; }[keyof R], ], [ Intersect< { [K in keyof R]: R[K] extends $ ? D : never; }[keyof R] >, ], [ Intersect< { [K in keyof R]: R[K] extends $ ? E : never; }[keyof R] >, ] >; /** * The return type of sequence for use with type inference. * * @since 2.0.0 */ export type SequenceRecord = < US extends ReadonlyRecord>, >( values: NonEmptyRecord, ) => Sequence; /** * Sequence over an ReadonlyRecord of type V, inverting the relationship between V and * ReadonlyRecord. This function also keeps the indexed types of in each V at * covariant position 0. In other words sequence over [Option, * Option] becomes Option<[number, string]>. * * @example * ```ts * import * as R from "./record.ts"; * import * as O from "./option.ts"; * * const sequence = R.sequence(O.ApplicableOption); * * const result1 = sequence({ one: O.some(1), two: O.some("Hello")}); // Some({ one: 1, two: "Hello"}) * const result2 = sequence({ one: O.none, two: O.some("Uh Oh")}); // None * ``` * * @since 2.0.0 */ export function sequence( A: Applicable, ): >>( values: NonEmptyRecord, ) => Sequence { const sequence = traverse(A)(identity as any); return >>( vs: NonEmptyRecord, ): Sequence => sequence(vs) as Sequence; } /** * Insert a value A into a ReadonlyRecord at the key location. If the value * inserted has object equality with the current value in the record then * no change is made and the original record is returned. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const insert = R.insert(1); * * const result1 = pipe( * { one: 1, two: 2 }, * insert('one'), * ); // No Mutation, returns original object * const result2 = pipe( * { two: 2 }, * insert('one'), * ); // { one: 1, two: 2 } * ``` * * @since 2.0.0 */ export function insert( value: A, ): (key: string) => (ua: ReadonlyRecord) => ReadonlyRecord { return (key) => (ua) => ua[key] === value ? ua : { ...ua, [key]: value }; } /** * Insert a value A into a ReadonlyRecord at the key location. If the value * inserted has object equality with the current value in the record then * no change is made and the original record is returned. This is the same * function as insert but with the order of parameters swapped * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const atOne = R.insertAt('one'); * * const result1 = pipe( * { one: 1, two: 2 }, * atOne(1), * ); // No Mutation, returns original object * const result2 = pipe( * { two: 2 }, * atOne(1), * ); // { one: 1, two: 2 } * ``` * * @since 2.0.0 */ export function insertAt( key: string, ): (value: A) => (ua: ReadonlyRecord) => ReadonlyRecord { return (value) => (ua) => ua[key] === value ? ua : { ...ua, [key]: value }; } /** * Modify a value A into a ReadonlyRecord at the key location. If the * object does not hold the specified key then no change is made. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const addOne = R.modify((n: number) => n + 1); * * const result1 = pipe( * { one: 1, two: 2 }, * addOne('one'), * ); // { one: 2, two: 2 } * const result2 = pipe( * { two: 2 }, * addOne('one') * ); // { two: 2 } * ``` * * @since 2.0.0 */ export function modify( modifyFn: (a: A) => A, ): (key: string) => (ua: ReadonlyRecord) => ReadonlyRecord { return (key) => (ua) => { if (Object.hasOwn(ua, key)) { const out = modifyFn(ua[key]); return out === ua[key] ? ua : { ...ua, [key]: out }; } return ua; }; } /** * Modify a value A into a ReadonlyRecord at the key location. If the * object does not hold the specified key then no change is made. This * is the same function as modify with the order of parameters flipped. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const inc = (n: number) => n + 1; * const atOne = R.modifyAt('one'); * * const result1 = pipe( * { one: 1, two: 2 }, * atOne(inc), * ); // { one: 2, two: 2 } * const result2 = pipe( * { two: 2 }, * atOne(inc), * ); // { two: 2 } * ``` * * @since 2.0.0 */ export function modifyAt( key: string, ): (modifyFn: (a: A) => A) => (ua: ReadonlyRecord) => ReadonlyRecord { return (modifyFn) => (ua) => { if (Object.hasOwn(ua, key)) { const out = modifyFn(ua[key]); return out === ua[key] ? ua : { ...ua, [key]: out }; } return ua; }; } /** * Update a ReadonlyRecord at key with a value A. The record will only be * updated if it already holds the specified key, otherwise no changes are * made. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const to2 = R.update(2); * * const result1 = pipe( * { one: 1, two: 2 }, * to2('one'), * ); // { one: 2, two: 2 } * const result2 = pipe( * { two: 2 }, * to2('one'), * ); // No change { two: 2 } * ``` * * @since 2.0.0 */ export function update( value: A, ): (key: string) => (ua: ReadonlyRecord) => ReadonlyRecord { return (key) => (ua) => Object.hasOwn(ua, key) ? { ...ua, [key]: value } : ua; } /** * Update a ReadonlyRecord at key with a value A. The record will only be * updated if it already holds the specified key, otherwise no changes are * made. This function does the same as update but has the parameters * switched in order * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const atOne = R.updateAt('one'); * * const result1 = pipe( * { one: 1, two: 2 }, * atOne(2), * ); // { one: 2, two: 2 } * const result2 = pipe( * { two: 2 }, * atOne(2), * ); // No change { two: 2 } * ``` * * @since 2.0.0 */ export function updateAt( key: string, ): (value: A) => (ua: ReadonlyRecord) => ReadonlyRecord { return (value) => (ua) => Object.hasOwn(ua, key) ? { ...ua, [key]: value } : ua; } /** * Lookup the value at key. Returns an Option, where None indicates * that the record does not hold the key. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const result1 = pipe( * { one: 1, two: 2 }, * R.lookupAt('one'), * ); // Some(1) * const result2 = pipe( * { one: 1, two: 2 }, * R.lookupAt('three'), * ); // None * ``` * * @since 2.0.0 */ export function lookupAt(key: string): (ua: ReadonlyRecord) => Option { return (ua) => Object.hasOwn(ua, key) ? some(ua[key]) : none; } /** * Lookup the value in a record at key and return an optional * pair with the key and the value if the record holds the key. * Returns None if the record does not hold the key. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const result1 = pipe( * { one: 1, two: 2 }, * R.lookupWithKey('one'), * ); // Some(['one', 1]) * const result2 = pipe( * { one: 1, two: 2 }, * R.lookupWithKey('three'), * ); // None * ``` * * @since 2.0.0 */ export function lookupWithKey( key: string, ): (ua: ReadonlyRecord) => Option> { return (ua) => { if (Object.hasOwn(ua, key)) { return some([key, ua[key]]); } return none; }; } /** * Remove the value and key at key from a ReadonlyRecord. If the * record does not hold the key then no change is made and the * original record is returned. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const result1 = pipe( * { one: 1, two: 2 }, * R.deleteAt('one'), * ); // { two: 2 } * const result2 = pipe( * { two: 2 }, * R.deleteAt('one'), * ); // No Change { two: 2 } * * ``` * * @since 2.0.0 */ export function deleteAt( key: string, ): (ua: ReadonlyRecord) => ReadonlyRecord { return (ua) => { if (Object.hasOwn(ua, key)) { const out = { ...ua }; delete out[key]; return out; } return ua; }; } /** * Remove the key from the ReadonlyRecord, returning a pair containing * the new record and an Option containing the removed key value. If * the record did not hold the specified key then this is a non-op and * the return will be the original record and none. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const result1 = pipe( * { one: 1, two: 2 }, * R.deleteAtWithValue('one'), * ); // [{ two: 2 }, Some(1)] * const result2 = pipe( * { one: 1, two: 2 }, * R.deleteAtWithValue('three'), * ); // [{ one: 1, two: 2}, None] * ``` * * @since 2.0.0 */ export function deleteAtWithValue( key: string, ): (ua: ReadonlyRecord) => Pair, Option> { return (ua) => { if (Object.hasOwn(ua, key)) { const out = { ...ua }; const value = ua[key]; delete out[key]; return [out, some(value)]; } return [ua, none]; }; } /** * Given an instance of Comparable for the values in a ReadonlyRecord * return a curried function `second => first => boolean` that returns * true when first is a subrecord of second. * * @example * ```ts * import * as R from "./record.ts"; * import { ComparableNumber } from "./number.ts"; * import { pipe } from "./fn.ts"; * * const first = { one: 1, two: 2 }; * const second = { one: 1, two: 2, three: 3 }; * const isSub = R.isSubrecord(ComparableNumber); * * const result1 = pipe( * first, * isSub(second), * ); // true * const result2 = pipe( * second, * isSub(first), * ); // false * ``` * * @since 2.0.0 */ export function isSubrecord( S: Comparable, ): (second: ReadonlyRecord) => (first: ReadonlyRecord) => boolean { return (second) => (first) => { for (const key in first) { if (!Object.hasOwn(second, key) || !S.compare(second[key])(first[key])) { return false; } } return true; }; } /** * Given a refinement or a predicate, filter a ReadonlyRecord * by removing any values that do not match the predicate or * refinement. ie. When the predicate/refinement return true * a value is kept and when it returns false a value is removed. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * { one: 1, two: 2, three: 3 }, * R.filter(n => n > 1), * ); // { one: 1 } * ``` * * @since 2.0.0 */ export function filter( refinement: (a: A, key: string) => a is I, ): (ua: ReadonlyRecord) => ReadonlyRecord; export function filter( predicate: (a: A, key: string) => boolean, ): (ua: ReadonlyRecord) => ReadonlyRecord; export function filter( predicate: (a: A, key: string) => boolean, ): (ua: ReadonlyRecord) => ReadonlyRecord { return (ua) => { const output = {} as Record; for (const key in ua) { if (predicate(ua[key], key)) { output[key] = ua[key]; } } return output; }; } /** * Given a function over the values in a ReadonlyArray returning an * Option, return a function thatsimultaneously filters and maps over * the values in a ReadonlyRecord. * * @example * ```ts * import * as R from "./record.ts"; * import * as O from "./option.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * { one: 1, two: 2, three: 3 }, * R.filterMap(n => n > 1 ? O.some(`${n} is big enough`) : O.none), * ); // { two: "2 is big enough", three: "3 is big enough" } * ``` * * @since 2.0.0 */ export function filterMap( fai: (a: A, key: string) => Option, ): (ua: ReadonlyRecord) => ReadonlyRecord { return (ua) => { const output = {} as Record; for (const key in ua) { const result = fai(ua[key], key); if (isSome(result)) { output[key] = result.value; } } return output; }; } /** * Given a refinement or predicate, return a function that splits a * ReadonlyRecord into a Pair of ReadonlyRecords, with the first * record containing the values for which the predicate/refinement * returned true and the second record containing the values for which * the predicate/refinement returned false. * * @example * ```ts * import * as R from "./record.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * { one: 1, two: 2, three: 3 }, * R.partition(n => n > 1), * ); // [{ two: 2, three: 3 }, { one: 1 }] * ``` * * @since 2.0.0 */ export function partition( refinement: (a: A, key: string) => a is I, ): (ua: ReadonlyRecord) => Pair, ReadonlyRecord>; export function partition( predicate: (a: A, key: string) => boolean, ): (ua: ReadonlyRecord) => Pair, ReadonlyRecord>; export function partition( predicate: (a: A, key: string) => boolean, ): (ua: ReadonlyRecord) => Pair, ReadonlyRecord> { return (ua) => { const first = {} as Record; const second = {} as Record; for (const key in ua) { if (predicate(ua[key], key)) { first[key] = ua[key]; } else { second[key] = ua[key]; } } return pair(first as ReadonlyRecord, second as ReadonlyRecord); }; } /** * Given a function that takes an A and a key and returns an Either * return a function that simultaneously partitions and maps over the * values in a ReadonlyRecord. This is the equivalent of first * partitioning a ReadonlyRecord, and then using Pair's Bimap over * both values in a Pair. * * @example * ```ts * import * as R from "./record.ts"; * import * as E from "./either.ts"; * import { pipe } from "./fn.ts"; * * const result = pipe( * { one: 1, two: 2, three: 3 }, * R.partitionMap( * n => n > 1 * ? E.right(`${n} is big enough`) * : E.left(`${n} is small enough`) * ), * ); * // [ * // { two: "2 is big enough", three: "3 is big enough" }, * // { one: "1 is small enough" } * // ] * ``` * * @since 2.0.0 */ export function partitionMap( fai: (a: A, key: string) => Either, ): (ua: ReadonlyRecord) => Pair, ReadonlyRecord> { return (ua) => { const first = {} as Record; const second = {} as Record; for (const key in ua) { const result = fai(ua[key], key); if (isRight(result)) { first[key] = result.right; } else { second[key] = result.left; } } return pair(first, second); }; } /** * @since 2.0.0 */ export function getCombinableRecord( { combine }: Combinable, ): Combinable> { return { combine: (second) => (first) => { const result: Record = { ...first }; for (const [key, value] of entries(second)) { if (key in result) { result[key] = combine(value)(result[key]); } else { result[key] = value; } } return result; }, }; } /** * @since 2.0.0 */ export function getInitializableRecord( I: Initializable, ): Initializable> { return { init: () => ({}), ...getCombinableRecord(I), }; } /** * @since 2.0.0 */ export function getComparableRecord( C: Comparable, ): Comparable> { const _isSubrecord = isSubrecord(C); return { compare: (second) => (first) => _isSubrecord(second)(first) && _isSubrecord(first)(second), }; } /** * Given a Showable for the inner values of a ReadonlyRecord, return an instance * of Showable for ReadonlyRecord. * * @example * ```ts * import * as R from "./record.ts"; * import { ShowableNumber } from "./number.ts"; * * const { show } = R.getShowableRecord(ShowableNumber); * * const result = show({ one: 1, two: 2, three: 3 }); * // "{one: 1, two: 2, three: 3}" * ``` * * @since 2.0.0 */ export function getShowableRecord( SA: Showable, ): Showable> { return ({ show: (ua) => `{${ Object.entries(ua).map(([key, value]) => `${key}: ${SA.show(value)}`) .join(", ") }}`, }); } /** * The canonical implementation of Filterable for ReadonlyRecord. It contains * the methods filter, filterMap, partition, and partitionMap. * * @since 2.0.0 */ export const FilterableRecord: Filterable = { filter, filterMap, partition, partitionMap, }; /** * The canonical implementation of Mappable for ReadonlyRecord. It contains * the method map. * * @since 2.0.0 */ export const MappableRecord: Mappable = { map }; /** * The canonical implementation of Foldable for ReadonlyRecord. It contains * the method fold. * * @since 2.0.0 */ export const FoldableRecord: Foldable = { fold }; /** * The canonical implementation of Traversable for ReadonlyRecord. It contains * the methods map, fold, and traverse. * * @since 2.0.0 */ export const TraversableRecord: Traversable = { map, fold, traverse, };