/**
* Combinable is a structure that can be combine two fixed values. Some examples
* of Combinable are Array.combine, addition for numbers, or merging of two
* structs by combining their internal values.
*
* @module Combinable
* @since 2.0.0
*/
import type { Sortable } from "./sortable.ts";
import * as S from "./sortable.ts";
type ReadonlyRecord = Readonly>;
type NonEmptyArray = readonly [A, ...A[]];
/**
* The Combine function in a Combinable.
*
* @since 2.0.0
*/
export type Combine = (second: A) => (first: A) => A;
/**
* Combinable is a structure that allows the combination of two concrete values
* of A into a single value of A. In other functional libraries this is called a
* Semigroup.
*
* @since 2.0.0
*/
export interface Combinable {
readonly combine: Combine;
}
/**
* A type for Combinable over any, useful as an extension target for
* functions that take any Combinable and do not need to
* unwrap the type.
*
* @since 2.0.0
*/
// deno-lint-ignore no-explicit-any
export type AnyCombinable = Combinable;
/**
* A type level unwrapper, used to pull the inner type from a Combinable.
*
* @since 2.0.0
*/
export type TypeOf = T extends Combinable ? A : never;
/**
* Create a Combinable from a Combine and an init function.
*
* @since 2.0.0
*/
export function fromCombine(
combine: Combine,
): Combinable {
return { combine };
}
/**
* Get an Combinable over A that always returns the first
* parameter supplied to combine (confusingly this is
* actually the last parameter since combine is in curried
* form).
*
* @example
* ```ts
* import { first, getCombineAll } from "./combinable.ts";
* import { pipe } from "./fn.ts";
*
* type Person = { name: string, age: number };
* const FirstPerson = first();
* const getFirstPerson = getCombineAll(FirstPerson);
*
* const octavia: Person = { name: "Octavia", age: 42 };
* const kimbra: Person = { name: "Kimbra", age: 32 };
* const brandon: Person = { name: "Brandon", age: 37 };
*
* const result = getFirstPerson(octavia, kimbra, brandon); // octavia
* ```
*
* @since 2.0.0
*/
export function first(): Combinable {
return fromCombine(() => (first) => first);
}
/**
* Get an Combinable over A that always returns the last
* parameter supplied to combine (confusingly this is
* actually the first parameter since combine is in curried
* form).
*
* @example
* ```ts
* import { last } from "./combinable.ts";
* import { pipe } from "./fn.ts";
*
* type Person = { name: string, age: number };
*
* const CombinablePerson = last();
*
* const octavia: Person = { name: "Octavia", age: 42 };
* const kimbra: Person = { name: "Kimbra", age: 32 };
* const brandon: Person = { name: "Brandon", age: 37 };
*
* const lastPerson = pipe(
* octavia,
* CombinablePerson.combine(kimbra),
* CombinablePerson.combine(brandon),
* ); // lastPerson === brandon
* ```
*
* @since 2.0.0
*/
export function last(): Combinable {
return fromCombine((second) => () => second);
}
/**
* Get the "Dual" of an existing Combinable. This effectively reverses
* the order of the input combinable's application. For example, the
* dual of the "first" combinable is the "last" combinable. The dual
* of (boolean, ||) is itself.
*
* @example
* ```ts
* import * as SG from "./combinable.ts";
* import { pipe } from "./fn.ts";
*
* type Person = { name: string, age: number };
*
* const last = SG.last();
* const dual = SG.dual(last);
*
* const octavia: Person = { name: "Octavia", age: 42 };
* const kimbra: Person = { name: "Kimbra", age: 32 };
* const brandon: Person = { name: "Brandon", age: 37 };
*
* const dualPerson = pipe(
* octavia,
* dual.combine(kimbra),
* dual.combine(brandon),
* ); // dualPerson === octavia
* ```
*
* @since 2.0.0
*/
export function dual({ combine }: Combinable): Combinable {
return fromCombine((second) => (first) => combine(first)(second));
}
/**
* Get a Combinable from a tuple of combinables. The resulting
* combinable will operate over tuples applying the input
* combinables applying each based on its position,
*
* @example
* ```ts
* import * as SG from "./combinable.ts";
* import { pipe } from "./fn.ts";
*
* type Person = { name: string, age: number };
*
* const first = SG.first();
* const last = SG.last();
* const { combine } = SG.tuple(first, last);
*
* const octavia: Person = { name: "Octavia", age: 42 };
* const kimbra: Person = { name: "Kimbra", age: 32 };
* const brandon: Person = { name: "Brandon", age: 37 };
*
* const tuplePeople = pipe(
* [octavia, octavia],
* combine([kimbra, kimbra]),
* combine([brandon, brandon]),
* ); // tuplePeople === [octavia, brandon]
* ```
*
* @since 2.0.0
*/
export function tuple(
...combinables: T
): Combinable<{ readonly [K in keyof T]: TypeOf }> {
type Return = { [K in keyof T]: TypeOf };
return fromCombine((second) => (first): Return =>
combinables.map(({ combine }, index) =>
combine(second[index])(first[index])
) as Return
);
}
/**
* Get a Combinable from a struct of combinables. The resulting
* combinable will operate over similar shaped structs applying
* the input combinables applying each based on its position,
*
* @example
* ```ts
* import type { Combinable } from "./combinable.ts";
* import * as SG from "./combinable.ts";
* import * as N from "./number.ts";
* import { pipe } from "./fn.ts";
*
* type Person = { name: string, age: number };
* const person = (name: string, age: number): Person => ({ name, age });
*
* // Chooses the longest string, defaulting to left when equal
* const longestString: Combinable = {
* combine: (right) => (left) => right.length > left.length ? right : left,
* };
*
* // This combinable will merge two people, choosing the longest
* // name and the oldest age
* const { combine } = SG.struct({
* name: longestString,
* age: N.InitializableNumberMax,
* })
*
* const brandon = pipe(
* person("Brandon Blaylock", 12),
* combine(person("Bdon", 17)),
* combine(person("Brandon", 30))
* ); // brandon === { name: "Brandon Blaylock", age: 30 }
* ```
*
* @since 2.0.0
*/
// deno-lint-ignore no-explicit-any
export function struct>(
combinables: { [K in keyof O]: Combinable },
): Combinable {
type Entries = [keyof O, typeof combinables[keyof O]][];
return fromCombine((second) => (first) => {
const r = {} as Record;
for (const [key, { combine }] of Object.entries(combinables) as Entries) {
r[key] = combine(second[key])(first[key]);
}
return r as { [K in keyof O]: O[K] };
});
}
/**
* Create a combinable fron an instance of Sortable that returns
* that maximum for the type being ordered. This Combinable
* functions identically to max from Sortable.
*
* @example
* ```ts
* import * as SG from "./combinable.ts";
* import * as N from "./number.ts";
* import { pipe } from "./fn.ts";
*
* const { combine } = SG.max(N.SortableNumber);
*
* const biggest = pipe(
* 0,
* combine(-1),
* combine(10),
* combine(1000),
* combine(5),
* combine(9001)
* ); // biggest is over 9000
* ```
*
* @since 2.0.0
*/
export function max(sortable: Sortable): Combinable {
return fromCombine(S.max(sortable));
}
/**
* Create a combinable fron an instance of Sortable that returns
* that minimum for the type being ordered. This Combinable
* functions identically to min from Sortable.
*
* @example
* ```ts
* import * as SG from "./combinable.ts";
* import * as N from "./number.ts";
* import { pipe } from "./fn.ts";
*
* const { combine } = SG.min(N.SortableNumber);
*
* const smallest = pipe(
* 0,
* combine(-1),
* combine(10),
* combine(1000),
* combine(5),
* combine(9001)
* ); // smallest is -1
* ```
*
* @since 2.0.0
*/
export function min(sortable: Sortable): Combinable {
return fromCombine(S.min(sortable));
}
/**
* Create a combinable that works like Array.join,
* inserting middle between every two values
* that are combineenated. This can have some interesting
* results.
*
* @example
* ```ts
* import * as SG from "./combinable.ts";
* import * as S from "./string.ts";
* import { pipe } from "./fn.ts";
*
* const { combine: toList } = pipe(
* S.InitializableString,
* SG.intercalcate(", "),
* );
*
* const list = pipe(
* "apples",
* toList("oranges"),
* toList("and bananas"),
* ); // list === "apples, oranges, and bananas"
* ```
*
* @since 2.0.0
*/
export function intercalcate(
middle: A,
): (C: Combinable) => Combinable {
return ({ combine }) =>
fromCombine((second) => (first) => combine(second)(combine(middle)(first)));
}
/**
* Create a combinable that always returns the
* given value, ignoring anything that it is
* combineenated with.
*
* @example
* ```ts
* import * as SG from "./combinable.ts";
* import { pipe } from "./fn.ts";
*
* const { combine } = SG.constant("cake");
*
* const whatDoWeWant = pipe(
* "apples",
* combine("oranges"),
* combine("bananas"),
* combine("pie"),
* combine("money"),
* ); // whatDoWeWant === "cake"
* ```
*
* @since 2.0.0
*/
export function constant(a: A): Combinable {
return fromCombine(() => () => a);
}
/**
* Given a Combinable, create a function that will
* iterate through an array of values and combine
* them. This is not much more than Array.fold(combine).
*
* @example
* ```ts
* import * as C from "./combinable.ts";
* import * as N from "./number.ts";
* import { pipe } from "./fn.ts";
*
* const sumAll = C.getCombineAll(N.InitializableNumberSum);
*
* const result = sumAll(1, 30, 80, 1000, 52, 42); // 1205
* ```
*
* @since 2.0.0
*/
export function getCombineAll(
{ combine }: Combinable,
): (...as: NonEmptyArray) => A {
const _combine = (first: A, second: A) => combine(second)(first);
return (...as) => as.reduce(_combine);
}