Skip to content

Instantly share code, notes, and snippets.

@JBoss925
Last active October 15, 2024 20:58
Show Gist options
  • Save JBoss925/183ba9fa079ebab1d5f908fbf7725243 to your computer and use it in GitHub Desktop.
Save JBoss925/183ba9fa079ebab1d5f908fbf7725243 to your computer and use it in GitHub Desktop.
A typescript implementation of several versions of a Monad that allows a pipe-style application syntax as well as Monad-level logic.
// -------------------------------------------------------------------------------------------------------------------------------------
// This is a basic example of how a framework can be developed to apply a series of transformations in sequence.
// -------------------------------------------------------------------------------------------------------------------------------------
type Chain<T> = {
value: T,
apply: (transform: (value: T) => T) => Chain<T>,
}
function wrap<T>(value: T): Chain<T> {
return {
value: value,
apply: (transform) => wrap(transform(value))
}
};
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1: Chain<number> =
wrap(2)
.apply(v => v + 1)
.apply(v => v * 3)
.apply(v => v * v)
.apply(v => v/3);
console.log(result1.value);
// 27
const result2: Chain<string> =
wrap("H")
.apply(v => v + "ello,")
.apply(v => v + " ")
.apply(v => v + "world")
.apply(v => v + "!");
console.log(result2.value);
// "Hello, world!"
// -------------------------------------------------------------------------------------------------------------------------------------
// With a little extra code, we can create and store metadata in the Monad without the transformation's knowledge.
// In this way, the transformations remain clean and reusable. The Monad can handle higher level tasks using the
// values returned by the transformations. Thus, code can be ran before, during, and after transformations.
// And the best part is, the transformations don't need to know anything about it.
//
// In this example, I use this metadata to keep a history of all the values of the result as it passes through each transformation.
// -------------------------------------------------------------------------------------------------------------------------------------
type Chain<T> = {
value: T,
apply: (transform: (value: T) => T) => MetadataChain<T>
}
type WithArrayHistory<T> = {
history: T[]
}
type MetadataChain<T> = Chain<T> & WithArrayHistory<T>;
function wrap<T>(value: T, metadata?: WithArrayHistory<T>): MetadataChain<T> {
const history = metadata ? metadata.history.concat(value) : [value];
return {
value: value,
apply: (transform) => wrap(transform(value), { history }),
history
}
}
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1: MetadataChain<number> =
wrap(2)
.apply(v => v + 1)
.apply(v => v * 3)
.apply(v => v * v)
.apply(v => v/3);
console.log(result1.value);
// 27
console.log(result1.history);
// [2, 3, 9, 81, 27]
const result2: MetadataChain<string> =
wrap("H")
.apply(v => v + "ello,")
.apply(v => v + " ")
.apply(v => v + "world")
.apply(v => v + "!");
console.log(result2.value);
// "Hello, world!"
console.log(result2.history);
// ["H", "Hello,", "Hello, ", "Hello, world", "Hello, world!"]
// -------------------------------------------------------------------------------------------------------------------------------------
// The downside of the previous approach is that we are combining the Chain object and Metadata object types.
// This means the Chain itself is not 100% reusable, and the implementation must make Metadata changes
// in the same function that creates the Chain itself (wrap, in this case).
//
// An easier, cleaner solution is to make the Metadata object its own property, and operate on it separately
// from the Chain object.
// This also makes the Chain object itself more reusable, since individual chains no longer need a specific
// type to represent the Chain with Metadata, like MetadataChain in the previous example.
// Instead, the Metadata type is a generic type argument to Chain.
//
// Now, suppose we want to add this arbitrary, generic metadata type to the Chain.
// In that case, we need a higher-level transform function to transform the Metadata object.
//
// KEEP IN MIND: The underlying chainable transforms have no idea this metadata object exists.
// The underlying chainable transforms are simply of type: (value: T) => T
//
// This metadata transform is for the implementer of the Chain itself to make adjustments to the metadata
// object as the chainable transforms are applied.
//
// Below, I've reimplemented the Chain with history using this new generic Metadata framework.
// -------------------------------------------------------------------------------------------------------------------------------------
type Chain<T, M> = {
value: T,
apply: (transform: (value: T) => T) => Chain<T, M>,
metadata: M
}
type ArrayHistory<T> = {
history: T[]
}
function transformMetadata<T>(value: T, metadata?: ArrayHistory<T>): ArrayHistory<T> {
return metadata
? { history: metadata.history.concat(value)}
: { history: [value] }
}
function wrap<T>(value: T, metadata?: ArrayHistory<T>): Chain<T, ArrayHistory<T>> {
const nextMetadata: ArrayHistory<T> = transformMetadata(value, metadata);
return {
value: value,
apply: (transform) => {
const transformed = transform(value);
return wrap(transformed, nextMetadata);
},
metadata: nextMetadata
}
}
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1: Chain<number, ArrayHistory<number>> =
wrap(2)
.apply(v => v + 1)
.apply(v => v * 3)
.apply(v => v * v)
.apply(v => v/3);
console.log(result1.value);
// 27
console.log(result1.metadata.history);
// [2, 3, 9, 81, 27]
const result2: Chain<string, ArrayHistory<string>> =
wrap("H")
.apply(v => v + "ello,")
.apply(v => v + " ")
.apply(v => v + "world")
.apply(v => v + "!");
console.log(result2.value);
// "Hello, world!"
console.log(result2.metadata.history);
// ["H", "Hello,", "Hello, ", "Hello, world", "Hello, world!"]
// -------------------------------------------------------------------------------------------------------------------------------------
// Now, in my history use case, the metadata type is dependent on the type of value in the transform.
// What if the metadata type is different than the value type?
//
// In this example, instead of storing an array, I generate a string containing all historical values.
// This way, the values are numbers, and the metadata type is { history: String }. There is no overlap in the two.
//
// Yet, using this framework, we can still operate on the metadata object using the transformed values as reference,
// even if the metadata value is completely different from the value type.
// -------------------------------------------------------------------------------------------------------------------------------------
type Chain<T, M> = {
value: T,
apply: (transform: (value: T) => T) => Chain<T, M>,
metadata: M
}
type StringHistory = {
history: String
}
function transformMetadata<T>(value: T, metadata?: StringHistory): StringHistory {
return metadata
? { history: metadata.history + " | " + value}
: { history: "" + value }
}
function wrap<T>(value: T, metadata?: StringHistory): Chain<T, StringHistory> {
const nextMetadata: StringHistory = transformMetadata(value, metadata);
return {
value: value,
apply: (transform) => {
const transformed = transform(value);
return wrap(transformed, nextMetadata);
},
metadata: nextMetadata
}
}
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1: Chain<number, StringHistory> =
wrap(2)
.apply(v => v + 1)
.apply(v => v * 3)
.apply(v => v * v)
.apply(v => v/3);
console.log(result1.value);
// 27
console.log(result1.metadata.history);
// "2 | 3 | 9 | 81 | 27"
const result2: Chain<string, StringHistory> =
wrap("H")
.apply(v => v + "ello,")
.apply(v => v + " ")
.apply(v => v + "world")
.apply(v => v + "!");
console.log(result2.value);
// "Hello, world!"
console.log(result2.metadata.history);
// "H | Hello, | Hello, | Hello, world | Hello, world!"
// -------------------------------------------------------------------------------------------------------------------------------------
// It may seem like we're needlessly adding types, but what this gives us is the ability to use the same Chain type for all chains,
// even if they are different in their behaviour.
//
// For example, I have implemented two different chains: one that keeps track of value history in an array, and one that keeps track
// of value history as a string with separators.
// Note how in the end, both wraps produce a Chain object, just configured with different Metadata types.
// This is how a single Chain type can be used to create an unlimited number of Chains with different behaviors.
// All that is needed to create a new Chain is a way to wrap a new value, and the transform for the Metadata object.
// -------------------------------------------------------------------------------------------------------------------------------------
type Chain<T, M> = {
value: T,
apply: (transform: (value: T) => T) => Chain<T, M>,
metadata: M
}
// -------------------------------------------------------------------------------------------------------------------------------------
// Code for Chain<T, StringHistory>
// -------------------------------------------------------------------------------------------------------------------------------------
type StringHistory = {
history: String
}
function transformStringHistory<T>(value: T, metadata?: StringHistory): StringHistory {
return metadata
? { history: metadata.history + " | " + value}
: { history: "" + value }
}
function wrapForStringHistory<T>(value: T, metadata?: StringHistory): Chain<T, StringHistory> {
const nextMetadata: StringHistory = transformStringHistory(value, metadata);
return {
value: value,
apply: (transform) => {
const transformed = transform(value);
return wrapForStringHistory(transformed, nextMetadata);
},
metadata: nextMetadata
}
}
// -------------------------------------------------------------------------------------------------------------------------------------
// Code for Chain<T, ArrayHistory<T>>
// -------------------------------------------------------------------------------------------------------------------------------------
type ArrayHistory<T> = {
history: T[]
}
function transformArrayHistory<T>(value: T, metadata?: ArrayHistory<T>): ArrayHistory<T> {
return metadata
? { history: metadata.history.concat(value)}
: { history: [value] }
}
function wrapForArrayHistory<T>(value: T, metadata?: ArrayHistory<T>): Chain<T, ArrayHistory<T>> {
const nextMetadata: ArrayHistory<T> = transformArrayHistory(value, metadata);
return {
value: value,
apply: (transform) => {
const transformed = transform(value);
return wrapForArrayHistory(transformed, nextMetadata);
},
metadata: nextMetadata
}
}
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples comparing a chain with StringHistory vs a chain with ArrayHistory:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1a: Chain<number, StringHistory> =
wrapForStringHistory(2)
.apply(v => v + 1)
.apply(v => v * 3)
.apply(v => v * v)
.apply(v => v/3);
console.log(result1a.value);
// 27
console.log(result1a.metadata.history);
// "2 | 3 | 9 | 81 | 27"
const result1b: Chain<number, ArrayHistory<number>> =
wrapForArrayHistory(2)
.apply(v => v + 1)
.apply(v => v * 3)
.apply(v => v * v)
.apply(v => v/3);
console.log(result1b.value);
// 27
console.log(result1b.metadata.history);
// [2, 3, 9, 81, 27]
const result2a: Chain<string, StringHistory> =
wrapForStringHistory("H")
.apply(v => v + "ello,")
.apply(v => v + " ")
.apply(v => v + "world")
.apply(v => v + "!");
console.log(result2a.value);
// "Hello, world!"
console.log(result2a.metadata.history);
// "H | Hello, | Hello, | Hello, world | Hello, world!"
const result2b: Chain<string, ArrayHistory<string>> =
wrapForArrayHistory("H")
.apply(v => v + "ello,")
.apply(v => v + " ")
.apply(v => v + "world")
.apply(v => v + "!");
console.log(result2b.value);
// "Hello, world!"
console.log(result2b.metadata.history);
// ["H", "Hello,", "Hello, ", "Hello, world", "Hello, world!"]
// -------------------------------------------------------------------------------------------------------------------------------------
// Now, using this generic Chain framework we've developed, I will write a new Chain that extends its functionality.
// In this case, I will write a chain that is "undoable", meaning transformations on it can be undone using an undo() function.
// I will use the ArrayHistory metadata we've already developed to enable this undo function.
// I will also add the ability to create an ExtensionT, which is a type that allows us to extend the Chain type
// with our own custom functions (like undo, in this case).
// This allows us to customize the Chain to use any API we desire, while still remaining a Chain.
//
// NOTE that UndoableChain<T> is still just a plain old Chain<T, ArrayHistory<T>, UndoExtension<T>>.
// Thus, it is still using the same Chain type, meaning it can still be used wherever chains are as
// a normal chain!
// -------------------------------------------------------------------------------------------------------------------------------------
type Chain<ValueT, MetadataT, ExtensionT> = ExtensionT & {
value: ValueT,
apply: (transform: (value: ValueT) => ValueT) => Chain<ValueT, MetadataT, ExtensionT>,
metadata: MetadataT
}
// -------------------------------------------------------------------------------------------------------------------------------------
// Code for UndoableChain
// -------------------------------------------------------------------------------------------------------------------------------------
type ArrayHistory<T> = {
history: T[]
}
type UndoExtension<T> = {
undo: () => UndoableChain<T>
}
type UndoableChain<T> = Chain<T, ArrayHistory<T>, UndoExtension<T>>
function addValueToHistory<T>(value: T, metadata?: ArrayHistory<T>): ArrayHistory<T> {
return metadata
? { history: metadata.history.concat(value)}
: { history: [value] }
}
function undoValueFromHistory<T>(metadata: ArrayHistory<T>): ArrayHistory<T> {
return {
history: metadata.history.slice(0, metadata.history.length - 2)
}
}
function wrapUndoable<T>(value: T, metadata?: ArrayHistory<T>): UndoableChain<T> {
const nextMetadata: ArrayHistory<T> = addValueToHistory(value, metadata);
return {
value: value,
apply: (transform) => {
const transformed = transform(value);
return wrapUndoable(transformed, nextMetadata);
},
metadata: nextMetadata,
undo: () => {
const previousValue = nextMetadata.history[nextMetadata.history.length - 2];
return wrapUndoable(previousValue, undoValueFromHistory(nextMetadata));
}
}
}
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1: UndoableChain<number> =
wrapUndoable(2)
.apply(v => v + 1)
.apply(v => v * 3)
.apply(v => v * v)
.apply(v => v/3);
console.log(result1.value);
// 27
console.log(result1.metadata.history);
// [2, 3, 9, 81, 27]
const result1a = result1.undo();
console.log(result1a.value);
// 81
console.log(result1a.metadata.history);
// [2, 3, 9, 81]
const result1b = result1a.undo();
console.log(result1b.value);
// 9
console.log(result1b.metadata.history);
// [2, 3, 9]
const result2: UndoableChain<string> =
wrapUndoable("H")
.apply(v => v + "ello,")
.apply(v => v + " ")
.apply(v => v + "world")
.apply(v => v + "!");
console.log(result2.value);
// "Hello, world!"
console.log(result2.metadata.history);
// ["H", "Hello,", "Hello, ", "Hello, world", "Hello, world!"]
const result2a = result2.undo();
console.log(result2a.value);
// "Hello, world"
console.log(result2a.metadata.history);
// ["H", "Hello,", "Hello, ", "Hello, world"]
const result2b = result2a.undo();
console.log(result2b.value);
// "Hello, "
console.log(result2b.metadata.history);
// ["H", "Hello,", "Hello, "]

This is great and all, but what if we want the chain to be able to accept multiple types?

That is, we wrap TypeA, then transform to TypeB, and we want to continue the chain from there?

Generally, I recommend to keep the ValueT static in the Chain, and instead use something like a PropertySheet which allows for a value to be multiple, pre-defined data types.

This will allow us to remain 100% type-checked, since the metadata transform will know its input type is a PropertySheet. Otherwise the type of ValueT is unknown at the time of metadata transformation.

If you use a PropertySheet, that means that some transformations which assume a PropertySheet to have a certain value as a certain type can fail.

In this case, though, it's better to just use Options to chain those failable transformations. So the transformation type would be:

transform: (v: PropertySheet) => Option<PropertySheet>

Then in our apply function, when we compute the next value in the chain, if the Option is empty we just pass along the empty Option as the value.

Thus, by the end of the Chain, we just end up with an empty Option, so we know one of the operations failed.

We could also use a Result monad instead if we would like to include an error message, meaning the transform would be of type:

transform: (v: PropertySheet) => Result<PropertySheet>

And while this is a good pattern, if we know that no transform can produce an error, there is no use for metadata, or you just really want to have transforms between types, then we can adapt the Chain type to allow for a separate transformation return type (essentially allowing us to change the Chain's ValueT after a transformation).

// -------------------------------------------------------------------------------------------------------------------------------------
// Now we can create a more flexibly-typed Chain<ValueT, MetadataT>.
//
// We can accept a type argument to our apply() function, which allows us to specify the OutputT of the transform at the time of
// application. This means, we can choose to change the OutputT to be different then ValueT, and pass a fn from (v: ValueT) => OutputT
// as the transform, and all should be good.
//
// This does, however, mean transformMetadata can no longer make assumptions about ValueT being constant, and so it must be careful
// to ensure it's functions will still work under arbitrary ValueT options.
//
// As an example, I've implemented the Chain<T, StringHistory> with generic transform application.
// -------------------------------------------------------------------------------------------------------------------------------------
type StringHistory = {
history: String
}
type Chain<ValueT, MetadataT> = {
value: ValueT,
apply: <OutputT>(transform: (value: ValueT) => OutputT) => Chain<OutputT, MetadataT>,
metadata: MetadataT
}
function wrap<ValueT, MetadataT>(value: ValueT, transformMetadata: (value: unknown, metadata?: MetadataT) => MetadataT, metadata?: MetadataT): Chain<ValueT, MetadataT> {
const nextMetadata: MetadataT = transformMetadata(value, metadata);
return {
value: value,
apply: function apply<OutputT>(transform: (value: ValueT) => OutputT) {
return wrap(transform(value), transformMetadata, nextMetadata);
},
metadata: nextMetadata
};
}
function transformMetadata<T>(value: T, metadata?: StringHistory): StringHistory {
return metadata
? { history: metadata.history + " | " + value}
: { history: "" + value }
}
function strHistoryWrap<ValueT>(value: ValueT): Chain<ValueT, StringHistory> {
return wrap(value, transformMetadata);
}
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1: Chain<number, StringHistory> =
strHistoryWrap(2)
.apply<number>(v => v + 1);
console.log(result1.value);
// 3
console.log(result1.metadata.history);
// "2 | 3"
const result2: Chain<string, StringHistory> =
strHistoryWrap(2)
.apply<number>((v) => v + 1)
.apply<number>(v => v + 2)
.apply<string>(v => "Test: " + v);
console.log(result2.value);
// "Test: 5"
console.log(result2.metadata.history);
// "2 | 3 | 5 | Test: 5"
// -------------------------------------------------------------------------------------------------------------------------------------
// Now I will use the class feature to create a ChainNode<ValueT> in an objective style. This will be a specific point along the Chain
// which has a value held inside it, along with the metadata at that point in the Chain.
//
// Just as before, we can accept a type argument to our apply() function, which allows us to specify the OutputT of the transform at the
// time of application. This means, we can choose to change the OutputT to be different then ValueT, and pass a fn from
// (v: ValueT) => OutputT as the transformation, and all should be good.
//
// Additionally, the class-based approach allows us to name individual implementations of the Chain, instead of always using a generic
// Chain type with generic arguments. In this example, that's StringHistoryChain<ValueT>.
// -------------------------------------------------------------------------------------------------------------------------------------
type StringHistory = {
history: String
}
class ChainNode<ValueT, MetadataT> {
public value: ValueT;
public metadata: MetadataT;
public transformMetadata: (value: unknown, metadata?: MetadataT) => MetadataT;
constructor(value: ValueT, transformMetadata: (value: unknown, metadata?: MetadataT) => MetadataT, metadata: MetadataT) {
this.value = value;
this.metadata = metadata;
this.transformMetadata = transformMetadata;
}
protected static chainWrap<ValueT, MetadataT>(value: ValueT, transformMetadata: (value: unknown, metadata?: MetadataT) => MetadataT, metadata?: MetadataT): ChainNode<ValueT, MetadataT> {
const nextMetadata: MetadataT = transformMetadata(value, metadata);
return new ChainNode<ValueT, MetadataT>(value, transformMetadata, nextMetadata);
}
public apply<OutputT>(transform: (value: ValueT) => OutputT): ChainNode<OutputT, MetadataT> {
return ChainNode.chainWrap<OutputT, MetadataT>(transform(this.value), this.transformMetadata, this.metadata);
}
}
class StringHistoryChain<ValueT> extends ChainNode<ValueT, StringHistory> {
private static transformMetadata<T>(value: T, metadata?: StringHistory): StringHistory {
return metadata
? { history: metadata.history + " | " + value}
: { history: "" + value }
}
public static wrap<ValueT>(value: ValueT): ChainNode<ValueT,StringHistory> {
return ChainNode.chainWrap<ValueT, StringHistory>(value, StringHistoryChain.transformMetadata);
}
}
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1: StringHistoryChain<number> =
StringHistoryChain.wrap(2)
.apply<number>(v => v + 1);
console.log(result1.value);
// 3
console.log(result1.metadata.history);
// "2 | 3"
const result2: StringHistoryChain<string> =
StringHistoryChain.wrap(2)
.apply<number>((v) => v + 1)
.apply<number>(v => v + 2)
.apply<string>(v => "Test: " + v);
console.log(result2.value);
// "Test: 5"
console.log(result2.metadata.history);
// "2 | 3 | 5 | Test: 5"

Now we have reached our Chain monad's final form.

It can be used to Chain together transformations of type (v: ValueT) => OutputT with hidden logic written by the API developers.

It can use and track Metadata about the transformations, all out of sight from the API consumers.

And finally, it can be extended using ExtensionT to extend the Chain's API, allowing for more interesting behavior.

// -------------------------------------------------------------------------------------------------------------------------------------
// As a bonus, here's the UndoableChain adapted to the multiple OutputT style.
//
// Unfortunately, each Chain cannot know the ValueT of the Chain whose apply() produced it.
// So undo() must produce a Chain<ValueT, ArrayHistory, UndoExtension<unknown>>, since we cannot know the type of the previous ValueT.
//
// Or rather, we can know, but we would have to add another generic type argument to UndoExtension and Chain called PrevValueT.
// That would let undo() produce a Chain<ValueT, PrevValueT, ArrayHistory, UndoExtension<PrevValueT>>.
//
// But now consider the Chain<ValueT, PrevValueT, ArrayHistory, UndoExtension<PrevValueT>> that undo() produces.
// If I try to undo() again, then the undo() must produce a Chain<PrevValueT, unknown, ArrayHistory, UndoExtension<unknown>>
//
// Thus, we must know the PrevPrevValueT in order to type-check undo() and allow you to undo() 2 times in a row.
// Then, we'd have a Chain<PrevValueT, PrevPrevValueT, ArrayHistory, UndoExtension<PrevPrevValueT>>
//
// But now when we undo() 3 times in a row, we must know PrevPrevPrevValueT to typecheck the next undo(), and so on and so forth.
// Therefore, we would need to know the amount of transformations and all the ValueTs at each step before this could be typecheckable.
//
// This is not possible, at least for my knowledge of Typescript, and defeats the purpose of typechecking in the first place.
// It should enforce the rules and logic for you, not require you to manually provide all types ahead of time and worry about
// compatibility then instead of as you write transformations. It makes the pattern much less modular.
// -------------------------------------------------------------------------------------------------------------------------------------
type ArrayHistory = {
history: any[]
}
type Chain<ValueT, MetadataT, ExtensionT> = ExtensionT & {
value: ValueT,
apply: <OutputT>(transform: (value: ValueT) => OutputT) => Chain<OutputT, MetadataT, ExtensionT>,
metadata: MetadataT
}
type UndoExtension<ValueT> = {
undo: () => Chain<ValueT, ArrayHistory, UndoExtension<unknown>>
}
type UndoChain<T> = Chain<T, ArrayHistory, UndoExtension<unknown>>;
function addValueToHistory<T>(value: T, metadata?: ArrayHistory): ArrayHistory {
return metadata
? { history: metadata.history.concat(value)}
: { history: [value] }
}
function undoValueFromHistory<T>(metadata: ArrayHistory): ArrayHistory {
return {
history: metadata.history.slice(0, metadata.history.length - 2)
}
}
function chainWrap<ValueT>(value: ValueT, transformMetadata: (value: unknown, metadata?: ArrayHistory) => ArrayHistory, metadata?: ArrayHistory): UndoChain<ValueT> {
const nextMetadata: ArrayHistory = transformMetadata(value, metadata);
return {
value: value,
apply: function apply<OutputT>(transform: (value: ValueT) => OutputT) {
return chainWrap<OutputT>(transform(value), transformMetadata, nextMetadata);
},
metadata: nextMetadata,
undo: () => {
const previousValue = nextMetadata.history[nextMetadata.history.length - 2];
return chainWrap(previousValue, addValueToHistory, undoValueFromHistory(nextMetadata));
}
};
}
function wrap<ValueT>(value: ValueT): UndoChain<ValueT> {
return chainWrap(value, addValueToHistory);
}
// -------------------------------------------------------------------------------------------------------------------------------------
// And here are some usage examples:
// -------------------------------------------------------------------------------------------------------------------------------------
const result1: UndoChain<number> =
wrap(2)
.apply<number>(v => v + 1);
console.log(result1.value);
// 3
console.log(result1.metadata.history);
// [2, 3]
const result1a = result1.undo();
console.log(result1a.value);
// 2
console.log(result1a.metadata.history);
// [2]
const result2: UndoChain<String> =
wrap(2)
.apply<number>((v) => v + 1)
.apply<number>(v => v + 2)
.apply<string>(v => "Test: " + v);
console.log(result2.value);
// "Test: 5"
console.log(result2.metadata.history);
// [2, 3, 5, "Test: 5"]
const result2a = result2.undo();
console.log(result2a.value);
// 5
console.log(result2a.metadata.history);
// [2, 3, 5]
const result2b = result2a.undo();
console.log(result2b.value);
// 3
console.log(result2b.metadata.history);
// [2, 3]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment