480 lines
15 KiB
TypeScript
480 lines
15 KiB
TypeScript
type FunType<input, output> = {
|
|
(inputValue: input): output;
|
|
next: <nextOuput>(nextFunction: FunType<output, nextOuput>) => FunType<input, nextOuput>;
|
|
repeat: (this: FunType<input, input>, count: number) => FunType<input, input>;
|
|
repeatUntil: (this: FunType<input, input>, condition: FunType<input, boolean>) => FunType<input, input>;
|
|
}
|
|
|
|
const Fun = <input, output>(inputLamba: (_: input) => output): FunType<input, output> => {
|
|
const func = inputLamba as FunType<input, output>;
|
|
|
|
func.next = function<nextOutput>(nextFunction: FunType<output, nextOutput>): FunType<input, nextOutput> {
|
|
return Fun(inputValue => nextFunction(inputLamba(inputValue)));
|
|
}
|
|
|
|
func.repeat = function(this: FunType<input, input>, count: number): FunType<input, input> {
|
|
if (count == 0) {
|
|
return Fun((x: input) => x);
|
|
}
|
|
|
|
return this.next(this.repeat(count - 1));
|
|
}
|
|
|
|
func.repeatUntil = function(this: FunType<input, input>, condition: FunType<input, boolean>): FunType<input, input> {
|
|
return Fun((x: input) => {
|
|
if (condition(x)) {
|
|
return x;
|
|
}
|
|
|
|
console.log(x);
|
|
|
|
return this.next(this.repeatUntil(condition))(x);
|
|
})
|
|
|
|
// Shorthand
|
|
// return Fun((x: input) => condition(x) ? x : this.next(this.repeatUntil(condition))(x));
|
|
}
|
|
|
|
return func;
|
|
}
|
|
|
|
//
|
|
// Some FunType lamdas
|
|
//
|
|
const isPositive: FunType<number, boolean> = Fun((x: number) => x > 0);
|
|
const isEven: FunType<number, boolean> = Fun((x: number) => x % 2 == 0);
|
|
const incr: FunType<number, number> = Fun((x: number) => x + 1);
|
|
const double: FunType<number, number> = Fun((x: number) => x * 2);
|
|
const decr: FunType<number, number> = Fun((x: number) => x - 1);
|
|
const convert: FunType<number, string> = Fun((x: number) => String(x));
|
|
const exclaim: FunType<string, string> = Fun((x: string) => {
|
|
let y = x + "!";
|
|
return y;
|
|
});
|
|
const question: FunType<string, string> = Fun((x: string) => {
|
|
let y = x + "?";
|
|
return y;
|
|
});
|
|
const saveSqrt = Fun((a: number) => a > 0 ? Math.sqrt(a) : Math.abs(Math.sqrt(a)));
|
|
|
|
let xxx = incr.next(convert) (4)
|
|
|
|
//
|
|
// Generic id functions
|
|
//
|
|
|
|
// Id functions do basically nothing but initialize a base where we can work from (initializing a pipeline for example)
|
|
// Eg. Id functions are the base of all other functions
|
|
const id: <a>() => FunType<a, a> = () => Fun(x => x);
|
|
// function id<a>(): FunType<a, a> {
|
|
// return Fun(_ => _);
|
|
// }
|
|
|
|
// Example usage 1
|
|
let exampleId = id<number>().next(incr).next(incr)(0) // -> result would be 2
|
|
// |
|
|
// Define type here
|
|
|
|
// Id functions but for a specific type (Non generic)
|
|
type Unit = {};
|
|
const idNum: FunType<Unit, number> = Fun(_ => 0);
|
|
const idString: FunType<Unit, string> = Fun(_ => "");
|
|
// |
|
|
// Define type here
|
|
|
|
// Example usage 2
|
|
let exampleId2 = idNum.next(incr).next(incr)(0)
|
|
|
|
//
|
|
// Countainers / Functors
|
|
//
|
|
|
|
// A functor is a container that is mappable
|
|
// Aka: We can run map functions on the containers itself to perform some kind of data update.
|
|
|
|
type Countainer<content> = {
|
|
content: content; // Content
|
|
counter: number; // For Example: amount of times object has been changed or mapped.
|
|
}
|
|
|
|
// Increments the value of counter with 1
|
|
function incrCountainer<content>(countainer: Countainer<content>): Countainer<content> {
|
|
return {
|
|
content: countainer.content,
|
|
counter: countainer.counter + 1
|
|
};
|
|
}
|
|
|
|
// Applies transformer lamda to content of countainer to potentionally map value to other type: content -> newContent
|
|
function mapCountainerNoFun<content, newContent>(countainer: Countainer<content>, transformer: FunType<content, newContent>): Countainer<newContent> {
|
|
return {
|
|
counter: countainer.counter + 1,
|
|
content: transformer(countainer.content)
|
|
}
|
|
}
|
|
|
|
|
|
// Applies transformer lamda to each element content of array to potentionally map value to other type: content -> newContent
|
|
function mapArray<content, newContent>(transformer: FunType<content, newContent>): FunType<Array<content>, Array<newContent>> {
|
|
return Fun((array: Array<content>) => array.map(transformer));
|
|
}
|
|
|
|
// Applies transformer lamda to content of countainer to potentionally map value to other type: content -> newContent
|
|
// !! But is wrapped in the FunType so we can use this in a pipeline !!
|
|
function mapCountainer<content, newContent>(transformer: FunType<content, newContent>): FunType<Countainer<content>, Countainer<newContent>> {
|
|
return Fun(c => {
|
|
return {
|
|
counter: c.counter + 1,
|
|
content: transformer(c.content)
|
|
}
|
|
})
|
|
}
|
|
|
|
type Message = {
|
|
username: string;
|
|
likes: number;
|
|
message: string;
|
|
}
|
|
|
|
// Some pipeline functions for Message type
|
|
const setUsername = (newUsername: string) => Fun<Message, Message>(c => ({ username: newUsername, likes: c.likes, message: c.message }));
|
|
const incrLikes = () => Fun<Message, Message>(c => ({ username: c.username, likes: c.likes + 1, message: c.message }));
|
|
|
|
// We could also write these functions like this
|
|
function setMessage (msg: string): FunType<Message, Message> {
|
|
return Fun(c => {
|
|
return {
|
|
username: c.username,
|
|
likes: c.likes,
|
|
message: msg
|
|
}
|
|
})
|
|
}
|
|
|
|
// Countainer where content is of Message Type
|
|
let messageCountainer: Countainer<Message> = {
|
|
counter: 0,
|
|
content: {
|
|
username: "",
|
|
likes: 0,
|
|
message: ""
|
|
}
|
|
}
|
|
|
|
// Pipeline usage example with countainer and message
|
|
// Using mapCountainer
|
|
const messagePipeline1 = mapCountainer(
|
|
setUsername("Test")
|
|
.next(setMessage("This is a message"))
|
|
.next(incrLikes())
|
|
.next(incrLikes())
|
|
)
|
|
|
|
const messagePipeline2 = mapCountainer(
|
|
setUsername("Test2")
|
|
.next(setMessage("This is a new and more improved message message"))
|
|
.next(incrLikes())
|
|
.next(incrLikes())
|
|
.next(incrLikes())
|
|
)
|
|
|
|
const message1 = messagePipeline1(messageCountainer);
|
|
const message2 = messagePipeline2(messageCountainer);
|
|
|
|
//console.log(message1);
|
|
// Result: {
|
|
// counter: 1,
|
|
// content: { username: 'Test', likes: 2, message: 'This is a message' }
|
|
// }
|
|
|
|
// This function will increase all the likes by 1 in a array of Countainers where its type is Message
|
|
// We use the MapArray function for this operation
|
|
const increaseAllLikes: FunType<Array<Countainer<Message>>, Array<Countainer<Message>>> = mapArray(mapCountainer(incrLikes()));
|
|
|
|
// Create array
|
|
const messages: Array<Countainer<Message>> = [message1, message2];
|
|
|
|
// console.log(increaseAllLikes(messages));
|
|
// Result: [
|
|
// {
|
|
// counter: 2,
|
|
// content: { username: 'Test', likes: 3, message: 'This is a message' }
|
|
// },
|
|
// {
|
|
// counter: 2,
|
|
// content: {
|
|
// username: 'Test2',
|
|
// likes: 4,
|
|
// message: 'This is a new and more improved message message'
|
|
// }
|
|
// }
|
|
// ]
|
|
|
|
// Some more examples of array mapping
|
|
|
|
// Basic increment
|
|
// console.log(
|
|
// mapArray(incr)([1, 2, 3, 4])
|
|
//)
|
|
// Result: [ 2, 3, 4, 5 ]
|
|
|
|
// Increment, double and decrement in pipeline
|
|
// console.log(
|
|
// mapArray(incr.next(double).next(decr))([1, 2, 3, 4])
|
|
// )
|
|
// Result: [ 3, 5, 7, 9 ]
|
|
|
|
// Increment, check if even.
|
|
// Here we also perform a mapping function that will change the type of content
|
|
// console.log(
|
|
// mapArray(incr.next(isEven))([1, 2, 3, 4])
|
|
// )
|
|
// Result: [ true, false, true, false ]
|
|
|
|
//
|
|
// Monoid / Monoidal structure / Monoids
|
|
//
|
|
|
|
// Monoids are datatypes (structures) that supports a given mathematical operation plus some given values
|
|
// Examples:
|
|
|
|
// + (Operator in numerical context)
|
|
// 1 + 2 == 2 + 1
|
|
// (a + b) * c == c * (b + a)
|
|
// a + 0 == 0 + a == a
|
|
|
|
// * (Operator in numerical context)
|
|
// (a * b) * c == a * (b * c)
|
|
// a * 1 == 1 * a == a
|
|
|
|
// + (Operator in string context)
|
|
// (a + b) + c == a + (b + c)
|
|
// a + "" == "" + a == a
|
|
|
|
// Practical examples with arrays
|
|
const numArray1: Array<number> = [1, 2, 3];
|
|
const numArray2: Array<number> = [2, 3, 4];
|
|
const numArray3: Array<number> = [5, 6, 7];
|
|
const numArray4: Array<number> = [];
|
|
|
|
const resultNumArr1 = numArray1.concat(numArray2.concat(numArray3)); // -> [1, 2, 3, 2, 3, 4, 5, 6, 7]
|
|
const resultNumArr2 = numArray1.concat(numArray2).concat(numArray3); // -> [1, 2, 3, 2, 3, 4, 5, 6, 7]
|
|
const resultNumArr3 = (numArray1.concat(numArray2)).concat(numArray3); // -> [1, 2, 3, 2, 3, 4, 5, 6, 7]
|
|
// Therefore: resultNumArr1 == resultNumArr2 == resultNumArr31
|
|
|
|
const resultNumArr4 = numArray4.concat(numArray1); // -> [1, 2, 3]
|
|
const resultNumArr5 = numArray1.concat(numArray4); // -> [1, 2, 3]
|
|
// Therefore: resultNumArr4 == resultNumArr5
|
|
|
|
// This type of reasoning also applies to (monoidal) functions!!
|
|
// Here monoids come into play
|
|
|
|
let monoidTest1 = id<number>().next(incr).next(incr).next(incr) (0);
|
|
let monoidTest2 = id<number>().next(incr.next(incr).next(incr)) (0);
|
|
let monoidTest3 = id<number>().next(incr.next(incr)).next(incr) (0);
|
|
// Therefore: monoidTest1 == monoidTest2 == monoidTest3
|
|
|
|
let monoidTest4 = incr.next(id<number>()) (0);
|
|
let monoidTest5 = id<number>().next(incr) (0);
|
|
// Therefore: monoidTest4 == monoidTest5
|
|
|
|
// A monoido or triple is
|
|
// A type T
|
|
// An operator + : [T, T] => T
|
|
// An element z : T
|
|
//
|
|
// Such that:
|
|
// (a + b) + c == a + (b + c) <--- associative law
|
|
// a + z == z + a == a <--- identity law
|
|
|
|
//
|
|
// Monads are different than monoids!
|
|
// Monads (NOT Monoid / Monodial, etc) are Functors and Monoids combined!
|
|
//
|
|
|
|
//
|
|
// Pairs
|
|
//
|
|
|
|
type Pair<a, b> = {
|
|
a: a,
|
|
b: b
|
|
};
|
|
|
|
const pairA = <a, b>(): FunType<Pair<a, b>, a> => Fun(c => c.a);
|
|
const pairB = <a, b>(): FunType<Pair<a, b>, b> => Fun(c => c.b);
|
|
|
|
const mapPair = <a, b, c, d>(f1: FunType<a, c>, f2: FunType<b, d>): FunType<Pair<a, b>, Pair<c, d>> => Fun(c => {
|
|
return {
|
|
a: f1(c.a),
|
|
b: f2(c.b),
|
|
}
|
|
})
|
|
|
|
//
|
|
// Options
|
|
//
|
|
|
|
type Option<a> = ({ kind: "empty" } | { kind: "full", content: a }) & {
|
|
then <b>(cont: (_: a) => Option<b>): Option<b>
|
|
};
|
|
|
|
const emptyOption = <a>(): Option<a> => ({
|
|
kind: "empty",
|
|
then: function<b>(this: Option<a>, cont: (_: a) => Option<b>) { return bindOption(this, Fun(cont)) }
|
|
});
|
|
|
|
const fullOption = <a>(input: a): Option<a> => ({
|
|
kind: "full",
|
|
content: input,
|
|
then: function<b>(this: Option<a>, cont: (_: a) => Option<b>) { return bindOption(this, Fun(cont)) }
|
|
});
|
|
|
|
class OptionFunctors {
|
|
static join = <a>(nestedOption: Option<Option<a>>): Option<a> => {
|
|
if (nestedOption.kind == "empty") return emptyOption<a>();
|
|
if (nestedOption.content.kind == "empty") return emptyOption<a>();
|
|
return fullOption<a>(nestedOption.content.content);
|
|
}
|
|
|
|
static funJoin = <a>(): FunType<Option<Option<a>>, Option<a>> => {
|
|
return Fun((nestedOption: Option<Option<a>>) => {
|
|
if (nestedOption.kind == "empty") return emptyOption<a>();
|
|
if (nestedOption.content.kind == "empty") return emptyOption<a>();
|
|
return fullOption<a>(nestedOption.content.content);
|
|
})
|
|
}
|
|
|
|
static unit = <a>(unstructuredValue: a) => {
|
|
return fullOption(unstructuredValue);
|
|
}
|
|
|
|
static zero = <a>(): Option<a> => {
|
|
return emptyOption<a>();
|
|
}
|
|
}
|
|
|
|
const mapOption = <a, b>(f: FunType<a, b>): FunType<Option<a>, Option<b>> => {
|
|
return Fun((option: Option<a>) => {
|
|
if (option.kind == "empty") {
|
|
return emptyOption<b>();
|
|
}
|
|
return fullOption(f(option.content));
|
|
})
|
|
}
|
|
|
|
const bindOption = <a, b>(source: Option<a>, cont: FunType<a, Option<b>>): Option<b> => {
|
|
return mapOption(cont).next(OptionFunctors.funJoin()) (source);
|
|
}
|
|
|
|
const saveDivide = (x: Option<number>, y: Option<number>): Option<number> => {
|
|
return x.then((xValue) => {
|
|
return y.then((yValue) => {
|
|
if (yValue == 0) {
|
|
return emptyOption<number>();
|
|
}
|
|
|
|
return fullOption(xValue / yValue);
|
|
})
|
|
})
|
|
}
|
|
|
|
|
|
// let exmp = saveDivide(fullOption(3), saveDivide(fullOption(3), fullOption(0)));
|
|
|
|
//
|
|
// Either
|
|
//
|
|
|
|
type Either<a, b> = {
|
|
kind: "left",
|
|
value: a
|
|
} | {
|
|
kind: "right",
|
|
value: b
|
|
}
|
|
|
|
const eitherL = <a, b>(): FunType<a, Either<a, b>> => Fun((input: a) => ({ kind: "left", value: input }));
|
|
const eitherR = <a, b>(): FunType<b, Either<a, b>> => Fun((input: b) => ({ kind: "right", value: input }));
|
|
|
|
const mapEither = <a1, b1, a2, b2>(fL: FunType<a1, a2>, fR: FunType<b1, b2>): FunType<Either<a1, b1>, Either<a2, b2>> => {
|
|
return Fun((input: Either<a1, b1>) => {
|
|
if (input.kind === "left") {
|
|
return fL.next(eitherL<a2, b2>())(input.value);
|
|
}
|
|
return fR.next(eitherR<a2, b2>())(input.value);
|
|
})
|
|
}
|
|
|
|
type MyException = {
|
|
msg: string;
|
|
}
|
|
|
|
interface ServerConnection {
|
|
ip: string; //ip address
|
|
hello: string; //hello message
|
|
}
|
|
|
|
const servers: Array<ServerConnection> = [
|
|
{ ip: "11.11.11.11", hello: "Connected to EU server!" },
|
|
{ ip: "22.22.22.22", hello: "Connected to Asia Server" },
|
|
{ ip: "33.33.33.33", hello: "Connected to US Server" },
|
|
]
|
|
|
|
const connect = (): FunType<string, Either<ServerConnection, MyException>> => {
|
|
return Fun((ip: string) => {
|
|
const prob: number = Math.random();
|
|
|
|
if (prob < 0.8) {
|
|
return eitherL<ServerConnection, MyException>()({
|
|
ip: "11",
|
|
hello: "2323"
|
|
});
|
|
} else {
|
|
return eitherR<ServerConnection, MyException>()({
|
|
msg: "Could not connect to server"
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
//
|
|
// Process
|
|
//
|
|
|
|
type Process<s, a> = FunType<s, [s, a]>;
|
|
|
|
const mapProcess = <s, a, b>(f: FunType<a, b>): FunType<Process<s, a>, Process<s, b>> => {
|
|
return Fun((initialprocess) => {
|
|
return Fun((initialState) => {
|
|
const [newState, result] = initialprocess(initialState);
|
|
const mappedResult = f(result);
|
|
return [newState, mappedResult];
|
|
})
|
|
})
|
|
}
|
|
|
|
class ProcessFunctors {
|
|
static join = <S, T>(outerProcess: Process<S, Process<S, T>>): Process<S, T> => {
|
|
return Fun((outerState) => {
|
|
const [innerState, innerProcess] = outerProcess(outerState);
|
|
return innerProcess(innerState);
|
|
})
|
|
}
|
|
|
|
// static funJoin = <a>(): FunType<Option<Option<a>>, Option<a>> => {
|
|
// return Fun((nestedOption: Option<Option<a>>) => {
|
|
// if (nestedOption.kind == "empty") return emptyOption<a>();
|
|
// if (nestedOption.content.kind == "empty") return emptyOption<a>();
|
|
// return fullOption<a>(nestedOption.content.content);
|
|
// })
|
|
// }
|
|
|
|
static unit = <S, T>(unstructuredValue: T): Process<S, T> => {
|
|
return Fun((state) => {
|
|
return [state, unstructuredValue];
|
|
})
|
|
}
|
|
}
|
|
|
|
type ProcessWithError<s, a> = FunType<s, Option<[s, a]>>; |