Skip to content

Commit

Permalink
feat: Implement thenDo and elseDo for delayed execution
Browse files Browse the repository at this point in the history
  • Loading branch information
delanni committed Jun 23, 2020
1 parent 34cc2ea commit a72d5bb
Show file tree
Hide file tree
Showing 3 changed files with 197 additions and 7 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,17 @@ const RegularHeader = ({ dayOfTheWeek }) => {
)
}
```
You can also use it with unit functions, to short circuit execution of branches where the chain executes to false:
```javascript
const result = $if(false)
.thenDo(() => computeEngine.computeValue())
.elseIf(true).thenDo(() => computeEngine.computeSomethingElse())
.elseIf(Math.random > 0.5).then('bingo!')
.elseDo(computeEngine.getDefaultValue);
```
In this above example, only one of the branches gets executed, so you don't need to worry about all the wasted calculation or unwanted side-effects.

---

And $if also supports types through [Typescript](http://typescriptlang.org/)!
57 changes: 50 additions & 7 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ const ifExpression = (condition: boolean): IfStatement => {

const makeIfStatement = (cond: boolean): IfStatement => {
return {
then: <T>(thenValue: T) => makeThenStatement(cond, thenValue)
}
then: <T>(thenValue: T) => makeThenStatement(cond, thenValue),
thenDo: <T>(thenArrow: Unit<T>) => makeThenDoStatement(cond, thenArrow),
};
}

const makeThenStatement = <T>(cond: boolean, thenValue: T): ThenStatement<T> => {
Expand All @@ -19,41 +20,83 @@ const makeThenStatement = <T>(cond: boolean, thenValue: T): ThenStatement<T> =>
},
elseIf: (cond2: boolean) => {
if (cond) {
return makeLoadedIfStatement(thenValue) as any;
return makeLoadedIfStatement(thenValue);
} else {
return makeIfStatement(cond2);
}
},
elseDo: <V>(elseArrow: Unit<V>) => {
if (cond) {
return thenValue;
} else {
return elseArrow();
}
}
}
};
}

const makeLoadedIfStatement = <T>(fixedValue: T) => {
const makeThenDoStatement = <T>(cond: boolean, thenArrow: Unit<T>): ThenStatement<T> => {
return {
then: <TX>(ignoredThenValue: TX) => makeLoadedThenStatement(fixedValue)
else: <V>(elseValue: V) => {
if (cond) {
return thenArrow();
} else {
return elseValue;
}
},
elseIf: (cond2: boolean) => {
if (cond) {
return makeLoadedIfStatement(thenArrow()) as any;
} else {
return makeIfStatement(cond2);
}
},
elseDo: <V>(elseArrow: Unit<V>) => {
if (cond) {
return thenArrow();
} else {
return elseArrow();
}
}
};
}

const makeLoadedIfStatement = <T>(fixedValue: T): IfStatement => {
return {
then: <TX>(ignoredThenValue: TX) => makeLoadedThenStatement(fixedValue),
thenDo: <TX>(ignoredThenValue: Unit<TX>) => makeLoadedThenStatement(fixedValue)
}
}

const makeLoadedThenStatement = <T>(fixedValue: T) => {
const makeLoadedThenStatement = <T>(fixedValue: T): ThenStatement<T> => {
return {
else: <VX>(ignoredElseValue: VX) => {
return fixedValue;
},
elseIf: (ignoredCond: boolean) => {
return makeLoadedIfStatement(fixedValue);
},
elseDo: <VX>(ignoredElseArrow: Unit<VX>) => {
return fixedValue;
}
}
}

type Unit<T> = () => T;

type IfStatement = {
then: <T>(t: T) => ThenStatement<T>;
thenDo: <T>(p: Unit<T>) => ThenStatement<T>;
}

type ElseIfStatement<T1> = {
then: <T2>(t2: T2) => ThenStatement<T1 | T2>;
thenDo: <T2>(p: Unit<T2>) => ThenStatement<T1 | T2>;
}

type ThenStatement<T> = {
else: ElseStatement<T>;
elseDo: <V>(p: Unit<V>) => T | V;
elseIf: (condition: boolean) => ElseIfStatement<T>;
}

Expand Down
133 changes: 133 additions & 0 deletions test/index.spec.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {$if} from "../src";

type Unit<T> = () => T;

describe("$if expression", () => {
it("returns the .then clause's value when condition is true", () => {
Expand Down Expand Up @@ -82,3 +83,135 @@ describe("$if with elseif clause", () => {
expect(expressionValue).toBe(firstTrueIndex);
});
});

describe("$if with delayed values", () => {
type A = 'valA';
type B = 'valB';
type C = 'valC';

let fnA: () => 'valA';
let fnB: () => 'valB';
let fnC: () => 'valC';

beforeEach(() => {
fnA = jest.fn(() => 'valA');
fnB = jest.fn(() => 'valB');
fnC = jest.fn(() => 'valC');
});

it('works on .thenDo()', () => {
const value: A | B = $if(true).thenDo(fnA).elseDo(fnB);

expect(value).toBe('valA');
expect(fnA).toBeCalledTimes(1);
expect(fnB).not.toBeCalled();
});


it('works on .elseDo()', () => {
const value: A | B = $if(false).thenDo(fnA).elseDo(fnB);

expect(value).toBe('valB');
expect(fnA).not.toBeCalled();
expect(fnB).toBeCalledTimes(1);
});

it('works on .elseIf.thenDo()', () => {
const value: A | B | C = $if(false).thenDo(fnA).elseIf(true).thenDo(fnB).elseDo(fnC);

expect(value).toBe('valB');
expect(fnA).not.toBeCalled();
expect(fnB).toBeCalledTimes(1);
expect(fnC).not.toBeCalled();
});

it('works on .elseIf.elseDo()', () => {
const value: A | B | C = $if(false).thenDo(fnA).elseIf(false).thenDo(fnB).elseDo(fnC);

expect(value).toBe('valC');
expect(fnA).not.toBeCalled();
expect(fnB).not.toBeCalled();
expect(fnC).toBeCalledTimes(1);
});

it(`doesn't bleed over to any further arrows`, () => {
const fnD: Unit<'valD'> = jest.fn(() => 'valD');
const fnE: Unit<'valE'> = jest.fn(() => 'valE');

const value: A|B|C|'valD'|'valE' = $if(false).thenDo(fnA)
.elseIf(true).thenDo(fnB)
.elseIf(true).thenDo(fnC)
.elseIf(false).thenDo(fnD)
.elseDo(fnE);

expect(value).toBe('valB');
expect(fnA).not.toBeCalled();
expect(fnB).toBeCalledTimes(1);
expect(fnC).not.toBeCalled();
expect(fnD).not.toBeCalled();
expect(fnE).not.toBeCalled();
});
});

describe("$if with mixed clauses", () => {
type A = 'valA';
type B = 'valB';
type C = 'valC';

let fnA: () => 'valA';
let fnB: () => 'valB';
let fnC: () => 'valC';

beforeEach(() => {
fnA = jest.fn(() => 'valA');
fnB = jest.fn(() => 'valB');
fnC = jest.fn(() => 'valC');
});

it('works on .thenDo()', () => {
const value: A | B = $if(true).then(fnA()).elseDo(fnB);

expect(value).toBe('valA');
expect(fnA).toBeCalledTimes(1);
expect(fnB).not.toBeCalled();
});


it('works on .elseDo()', () => {
const value: A | B = $if(false).then(fnA()).elseDo(fnB);

expect(value).toBe('valB');
expect(fnB).toBeCalledTimes(1);
});

it('works on .elseIf.thenDo()', () => {
const value: A | B | C = $if(false).thenDo(fnA).elseIf(true).thenDo(fnB).else(fnC());

expect(value).toBe('valB');
expect(fnA).not.toBeCalled();
expect(fnB).toBeCalledTimes(1);
});

it('works on .elseIf.elseDo()', () => {
const value: A | B | C = $if(false).thenDo(fnA).elseIf(false).then(fnB()).elseDo(fnC);

expect(value).toBe('valC');
expect(fnA).not.toBeCalled();
expect(fnC).toBeCalledTimes(1);
});

it(`doesn't bleed over`, () => {
const fnD: Unit<'valD'> = jest.fn(() => 'valD');
const fnE: Unit<'valE'> = jest.fn(() => 'valE');

const value: A|B|C|'valD'|'valE' = $if(false).then(fnA())
.elseIf(true).thenDo(fnB)
.elseIf(true).then(fnC())
.elseIf(true).thenDo(fnD)
.else(fnE());

expect(value).toBe('valB');
expect(fnB).toBeCalledTimes(1);
expect(fnD).not.toBeCalled();
});
});

0 comments on commit a72d5bb

Please sign in to comment.