Last active
June 27, 2021 13:50
-
-
Save moatorres/021500d57c3929918c8f0db61d71fe75 to your computer and use it in GitHub Desktop.
Tuple type in JS/Node (without classes)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
interface ITuple<F, S> { | |
fst(): F | |
snd(): S | |
toArray(): [F, S] | |
unwrap(): [F, S] | |
toJSON(): [F, S] | |
inspect(): string | |
toString(): string | |
equals(other: ITuple<F, S>): boolean | |
bimap<F2, S2>(f: (fst: F) => F2, g: (snd: S) => S2): ITuple<F2, S2> | |
mapFirst<F2>(f: (fst: F) => F2): ITuple<F2, S> | |
map<S2>(f: (snd: S) => S2): ITuple<F, S2> | |
reduce<T>(reducer: (accumulator: T, value: S) => T, initialValue: T): T | |
swap(): ITuple<S, F> | |
ap<T, S2>(f: ITuple<T, (value: S) => S2>): ITuple<F, S2> | |
every(pred: (value: F | S) => boolean): boolean | |
some(pred: (value: F | S) => boolean): boolean | |
} | |
interface ITupleType<F, S> { | |
<F, S>(first: F, second: S): ITuple<F, S> | |
of<F, S>(fst: F, snd: S): ITuple<F, S> | |
fromArray<F, S>([fst, snd]: [F, S]): ITuple<F, S> | |
fromArray<F, S>([fst, snd]: (F | S)[]): ITuple<F, S> | |
fanout<F, S, T>( | |
f: (value: T) => F, | |
g: (value: T) => S, | |
value: T | |
): ITuple<F, S> | |
fanout<F, S, T>( | |
f: (value: T) => F, | |
g: (value: T) => S | |
): (value: T) => ITuple<F, S> | |
fanout<F, T>( | |
f: (value: T) => F | |
): <S>(g: (value: T) => S) => (value: T) => ITuple<F, S> | |
} | |
export const Tuple: ITupleType<F, S> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const makeTuple = (first, second) => ({ | |
fst: () => first, | |
snd: () => second, | |
swap: () => makeTuple(second, first), | |
equals: (other) => first === other.fst() && second === other.snd(), | |
map: (f) => makeTuple(first, f(second)), | |
mapFirst: (f) => makeTuple(f(first), second), | |
bimap: (f, g) => makeTuple(f(first), g(second)), | |
ap: (f) => makeTuple(first, f.snd()(second)), | |
reduce: (reducer, initialValue) => reducer(initialValue, second), | |
some: (pred) => pred(first) || pred(second), | |
every: (pred) => pred(first) && pred(second), | |
inspect: () => `Tuple(${JSON.stringify(first)}, ${JSON.stringify(second)})`, | |
toArray: () => [first, second], | |
unwrap: function () { | |
return this.toArray() | |
}, | |
toJSON: function () { | |
return this.toArray() | |
}, | |
toString: function () { | |
return this.inspect() | |
}, | |
[Symbol.iterator]: function* () { | |
yield first | |
yield second | |
}, | |
}) | |
export const Tuple = Object.assign( | |
function Tuple(fst, snd) { | |
return makeTuple(fst, snd) | |
}, | |
{ | |
of: (fst, snd) => makeTuple(fst, snd), | |
fromArray: ([fst, snd]) => makeTuple(fst, snd), | |
fanout: function (...args) { | |
const [f, g, value] = args | |
switch (args.length) { | |
case 3: | |
return makeTuple(f(value), g(value)) | |
case 2: | |
return (value) => this.fanout(f, g, value) | |
default: | |
return (g) => (value) => this.fanout(f, g, value) | |
} | |
}, | |
} | |
) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Tuple } from './Tuple' | |
let tup | |
beforeEach(() => { | |
tup = Tuple(1, true) | |
}) | |
describe('Static', () => { | |
it('Should be defined', () => { | |
expect(Tuple).toBeDefined() | |
}) | |
describe('Tuple.fromArray()', () => { | |
it('Should be defined', () => { | |
expect(Tuple.fromArray).toBeDefined() | |
}) | |
it('Should construct a tuple from an array with two elements', () => { | |
let arr = [1, 'Pa'] | |
tup = Tuple.fromArray(arr) | |
expect(tup.toArray()).toEqual(arr) | |
expect(tup.equals(tup)).toBeTrue() | |
}) | |
}) | |
describe('Tuple.of()', () => { | |
it('Should be defined', () => { | |
expect(Tuple.of).toBeDefined() | |
}) | |
it('Should construct a tuple from an array with two elements', () => { | |
tup = Tuple.of(2, 'Blue') | |
expect(tup.toArray()).toEqual([2, 'Blue']) | |
expect(tup.equals(tup)).toBeTrue() | |
}) | |
}) | |
describe('Tuple.fanout()', () => { | |
it('Should be defined', () => { | |
expect(Tuple.fanout).toBeDefined() | |
}) | |
it('Should apply two functions over a single value and construct a tuple from the results', () => { | |
tup = Tuple.fanout( | |
(x) => x[0], | |
(x) => x + 's', | |
'moka' | |
) | |
expect(tup.toArray()).toEqual(['m', 'mokas']) | |
expect(tup.equals(tup)).toBeTrue() | |
}) | |
}) | |
}) | |
describe('Tuple', () => { | |
describe('.fst()', () => { | |
it('Should be defined', () => { | |
expect(tup.fst).toBeDefined() | |
}) | |
it('Should return the first value of "this"', () => { | |
expect(tup.fst()).toEqual(1) | |
}) | |
}) | |
describe('.snd()', () => { | |
it('Should be defined', () => { | |
expect(tup.snd).toBeDefined() | |
}) | |
it('Should return the second value of "this"', () => { | |
expect(tup.snd()).toEqual(true) | |
}) | |
}) | |
describe('.toArray()', () => { | |
it('Should be defined', () => { | |
expect(tup.toArray).toBeDefined() | |
}) | |
it('Should return an array with the 2 values inside "this"', () => { | |
expect(tup.toArray()).toEqual([1, true]) | |
}) | |
}) | |
describe('.swap()', () => { | |
it('Should be defined', () => { | |
expect(tup.swap).toBeDefined() | |
}) | |
it('Should swap the values inside "this"', () => { | |
expect(tup.swap().toArray()).toEqual([true, 1]) | |
}) | |
}) | |
describe('.equals()', () => { | |
it('Should be defined', () => { | |
expect(tup.equals).toBeDefined() | |
}) | |
it('Should compare the values inside "this" and another tuple', () => { | |
let tap = Tuple(2, '') | |
expect(tup.equals(tup)).toBeTrue() | |
expect(tap.equals(tup)).toBeFalse() | |
}) | |
}) | |
describe('.map()', () => { | |
it('Should be defined', () => { | |
expect(tup.map).toBeDefined() | |
}) | |
it('Should apply a function to the second value of "this"', () => { | |
expect(tup.map((x) => !x).toArray()).toEqual([1, false]) | |
}) | |
}) | |
describe('.mapFirst()', () => { | |
it('Should be defined', () => { | |
expect(tup.mapFirst).toBeDefined() | |
}) | |
it('Should apply a function to the first value of "this"', () => { | |
expect(tup.mapFirst((v) => v * 100).toArray()).toEqual([100, true]) | |
}) | |
}) | |
describe('.bimap()', () => { | |
it('Should be defined', () => { | |
expect(tup.bimap).toBeDefined() | |
}) | |
it('Should transform the two values inside "this" with two mapper functions', () => { | |
expect( | |
tup | |
.bimap( | |
(x) => x * 2, | |
(x) => !x | |
) | |
.toArray() | |
).toEqual([2, false]) | |
}) | |
}) | |
describe('.reduce()', () => { | |
it('Should be defined', () => { | |
expect(tup.reduce).toBeDefined() | |
}) | |
it('Should pass the initialValue and the second value inside of `this` as arguments to the reducer', () => { | |
tup = Tuple(1, 2) // → Tuple(x, y) | |
// acc → initialValue | |
// y → second value of the calling tuple | |
expect(tup.reduce((acc, y) => acc + y, 10)).toEqual(12) | |
}) | |
}) | |
describe('.ap()', () => { | |
it('Should be defined', () => { | |
expect(tup.ap).toBeDefined() | |
}) | |
it('Should apply the second value of a tuple to the second value of "this"', () => { | |
expect(tup.ap(Tuple('toggle', (x) => !x)).toArray()).toEqual([1, false]) | |
}) | |
}) | |
describe('.some()', () => { | |
it('Should be defined', () => { | |
expect(tup.some).toBeDefined() | |
}) | |
it('Should test whether at least one element in the tuple passes the test implemented by the predicate function', () => { | |
expect(tup.some((v) => typeof v === 'boolean')).toBeTrue() | |
}) | |
}) | |
describe('.every()', () => { | |
it('Should be defined', () => { | |
expect(tup.every).toBeDefined() | |
}) | |
it('Should test whether both elements in the tuple passes the test implemented by the predicate function', () => { | |
expect(tup.every((v) => v > 0)).toBeTrue() | |
expect(tup.every((v) => typeof v === 'number')).toBeFalse() | |
}) | |
}) | |
describe('.inspect()', () => { | |
it('Should be defined', () => { | |
expect(tup.inspect).toBeDefined() | |
}) | |
it('Should return a stringified representation of the tuple', () => { | |
expect(tup.inspect()).toEqual('Tuple(1, true)') | |
}) | |
}) | |
describe('.toJSON()', () => { | |
it('Should be defined', () => { | |
expect(tup.toJSON).toBeDefined() | |
}) | |
it('Should return an array with the 2 values inside "this" (alias of .toArray())', () => { | |
expect(tup.toJSON()).toEqual([1, true]) | |
}) | |
}) | |
describe('.unwrap()', () => { | |
it('Should be defined', () => { | |
expect(tup.unwrap).toBeDefined() | |
}) | |
it('Should return an array with the 2 values inside "this" (alias of .toArray())', () => { | |
expect(tup.unwrap()).toEqual([1, true]) | |
}) | |
}) | |
describe('.toString()', () => { | |
it('Should be defined', () => { | |
expect(tup.toString).toBeDefined() | |
}) | |
it('Should return a stringified representation of the tuple', () => { | |
expect(tup.toString()).toEqual('Tuple(1, true)') | |
}) | |
}) | |
describe('[Symbol.iterator]', () => { | |
it('Should be defined', () => { | |
expect(tup[Symbol.iterator]).toBeDefined() | |
}) | |
it('Should allow us to destructure tuples like we destructure arrays', () => { | |
let [f, s] = tup | |
expect(f).toEqual(1) | |
expect(s).toEqual(true) | |
}) | |
}) | |
}) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment