Skip to content

Commit

Permalink
Result: Add either to help with creation of codemods (microsoft#14154)
Browse files Browse the repository at this point in the history
#### Pull request checklist

- [ ] Addresses an existing issue: Fixes #0000
- [ ] Include a change request file using `$ yarn change`

#### Description of changes

Either makes it easy to handle things like failure states since you can return the error and handle it later after preforming several computations.

#### Focus areas to test

(optional)
  • Loading branch information
joschect committed Jul 29, 2020
1 parent 7fb345c commit 65b946b
Show file tree
Hide file tree
Showing 6 changed files with 261 additions and 11 deletions.
8 changes: 8 additions & 0 deletions change/@fluentui-codemods-2020-07-21-17-13-11-either.json
@@ -0,0 +1,8 @@
{
"type": "patch",
"comment": "Result: add in an result helper to assit in the creation of codemods",
"packageName": "@fluentui/codemods",
"email": "joschect@microsoft.com",
"dependentChangeType": "patch",
"date": "2020-07-22T00:13:11.108Z"
}
24 changes: 21 additions & 3 deletions packages/codemods/src/helpers/chainable.ts
@@ -1,8 +1,26 @@
import { Mappable } from './mappable';

export type Flattened<T, V> = T extends Chainable<unknown> ? T : V;
export interface Chainable<T> extends Mappable<T> {
export interface Chainable<T> {
/**
* If this Chainables's T is an instance of Chainable,
* then it will flatten it so now Chainable now contains
* a value of whatever type T held
* Chainable<Chainable<T>> => Chainable<T>
*/
flatten: () => Flattened<T, Chainable<T>>;

/**
* This takes in a lambda which maps from the current chainable
* type to the same type of chainable with a new type R. The entire function then returns
* a new Chainable with type R
* @param fn: A lambda that maps from a type T and returns a new Chainable that has type R.
*/
chain: <R>(fn: (v: T) => Chainable<R>) => Chainable<R>;

/**
* A combination of chain, map, and flatten. Then can return either type R or
* Chainable<R> and this value will get flattened appropriately so the
* returned Chainable is not nested.
* @param fn: A lambda that maps from a type T and returns a new type R.
*/
then: <R>(fn: (v: T) => R) => Chainable<R>;
}
3 changes: 0 additions & 3 deletions packages/codemods/src/helpers/mappable.ts

This file was deleted.

5 changes: 0 additions & 5 deletions packages/codemods/src/helpers/maybe.ts
Expand Up @@ -3,7 +3,6 @@ import { Chainable, Flattened } from './chainable';
interface MaybeChain<T> extends Chainable<T> {
something?: boolean;
__isMaybe: true;
fmap: <ReturnType>(this: Maybe<T>, fn: (v: NonNullable<T>) => NonNullable<ReturnType>) => Maybe<ReturnType>;
chain: <ReturnType>(this: Maybe<T>, fn: (v: NonNullable<T>) => Maybe<ReturnType>) => Maybe<ReturnType>;
flatten: () => Flattened<T, Maybe<T>>;
then: <ReturnType>(this: Maybe<T>, fn: (v: NonNullable<T>) => ReturnType | Maybe<ReturnType>) => Maybe<ReturnType>;
Expand All @@ -22,10 +21,6 @@ class MB<T> implements MaybeChain<T> {
public value: T | undefined;
public __isMaybe: true = true;

public fmap<ReturnType>(this: Maybe<T>, fn: (v: NonNullable<T>) => NonNullable<ReturnType>): Maybe<ReturnType> {
return this.something ? Something(fn(this.value)) : Nothing();
}

public chain<R>(this: Maybe<T>, fn: (v: NonNullable<T>) => Maybe<R>): Maybe<R> {
return this.something ? fn(this.value) : Nothing();
}
Expand Down
119 changes: 119 additions & 0 deletions packages/codemods/src/helpers/result.ts
@@ -0,0 +1,119 @@
import { Chainable, Flattened } from './chainable';

export interface Ok<R, E> extends ResultInternal<R, E> {
ok: true;
value: R;
}

export interface Err<R, E> extends ResultInternal<R, E> {
ok: false;
value: E;
}

class ResultInternal<R, E> implements Chainable<R> {
public ok: boolean;
public value: R | E;

public constructor(options: { ok: boolean; value: R | E }) {
this.ok = options.ok;
this.value = options.value;
}

public chain<T>(this: Result<R, E>, fn: (v: R) => Result<T, E>): Result<T, E> {
if (this.ok) {
return fn(this.value);
}
return Err(this.value);
}

/**
* Works just like chain, but is only called if this Result is an error.
* This returns a new Result with type Result<R, NewType>
*/
public errChain<T>(this: Result<R, E>, fn: (v: E) => Result<R, T>): Result<R, T> {
if (!this.ok) {
return fn(this.value);
}
return Ok(this.value);
}

public flatten(): Flattened<R, Result<R, E>> {
if (this.value && this.value instanceof ResultInternal) {
return this.value as Flattened<R, Result<R, E>>;
}
return (this as unknown) as Flattened<R, Result<R, E>>;
}

/**
* This allows users to opperate on a presumed result without needing to know whether or not
* the result was successful or not.
* At each call if it is a result type of ok, it will call the supplied function, otherwise it
* will return the current value with a new Err.
*
* @param fnOk Function that takes in an ok value of type R and returns either F or Result<F,E>
*/
public then<F>(this: Result<R, E>, fnOk: (v: R) => F | Result<F, E>): Result<F, E> {
if (this.ok) {
return Ok(fnOk(this.value)).flatten() as Result<F, E>;
}

return Err(this.value);
}

/**
* Works just like then, but is only called if this Result is an error.
* This returns a new Result with type Result<R, NewType>
*/
public errThen<F>(this: Result<R, E>, fnErr: (v: E) => F | Result<R, F>): Result<R, F> {
if (!this.ok) {
return Err(fnErr(this.value)).flatten() as Result<R, F>;
}

return Ok(this.value);
}

public okOrElse(this: Result<R, E>, okElse: R): R {
if (this.ok) {
return this.value;
}
return okElse;
}

public errOrElse(this: Result<R, E>, errElse: E): E {
if (!this.ok) {
return this.value;
}
return errElse;
}

/**
* Takes in two functions, one which takes type R and the other which takes type E and both return
* type T. This lets you handle either case and potentially return a unifying type. You might use this
* to create a message for users.
*
* @param fnOk Function that maps from Ok value(R) to new type T
* @param fnErr Function that maps from Err value(E) to new type T
*/
public resolve<T>(this: Result<R, E>, fnOk: (v: R) => T, fnErr: (v: E) => T): T {
if (this.ok) {
return fnOk(this.value);
}
return fnErr(this.value);
}
}

export const Ok = <R, E>(value: R): Ok<R, E> => {
return new ResultInternal<R, E>({ value: value, ok: true }) as Ok<R, E>;
};

export const Err = <R, E>(value: E): Err<R, E> => {
return new ResultInternal<R, E>({ value: value, ok: false }) as Err<R, E>;
};

/**
* Result is a useful type for when you might want to handle errors down the line without swallowing them
* while still preforming potentially several operations that could result in an error. A simple example could
* be a dividing function that returns a Result. Instead of throwing an error that you cannot divide by zero,
* it would be returned in an Err. This allows the rest of the program to execute cleanly.
*/
export type Result<R, E> = Err<R, E> | Ok<R, E>;
113 changes: 113 additions & 0 deletions packages/codemods/src/helpers/tests/result.test.ts
@@ -0,0 +1,113 @@
import { Err, Ok, Result } from '../result';

const getOk = <T, Z>(ok: T, err: Z): Result<T, Z> => {
return Ok<T, Z>(ok!);
};

const getErr = <T, Z>(okay: T, err: Z): Result<T, Z> => {
return Err<T, Z>(err!);
};

describe('Result', () => {
it('chained Okay value is evaluated correctly', () => {
expect(
getOk(3, '4')
.chain(v => Ok(v + 3))
.okOrElse(100),
).toBe(6);
});

it('errChained Err value is evaluated correctly', () => {
expect(
getErr(3, '4')
.errChain(v => Err(7))
.errOrElse(100),
).toBe(7);
});

it('chained Err value is evaluated correctly', () => {
expect(
getErr(3, '4')
.chain(v => Ok(1))
.okOrElse(100),
).toBe(100);
});

it('chained Err value is evaluated correctly', () => {
expect(
getErr(3, '4')
.errChain(v => Ok(1))
.okOrElse(100),
).toBe(1);
});

it('chain returning a Err returns a Err correctly', () => {
expect(getOk(3, '4').chain(v => Err('Error')).ok).toBe(false);
});

it('errChain returning an Ok returns an Ok correctly', () => {
expect(getOk(3, '4').chain(v => Ok('Error')).ok).toBe(true);
});

it('Thens correctly on Ok', () => {
expect(
getOk(3, '4')
.then(v => 30)
.then(v => v.toString())
.okOrElse('Bad'),
).toBe('30');
});

it('Thens correctly on Err', () => {
expect(
getErr(3, '4')
.then(v => 30)
.then(v => v.toString())
.okOrElse('Bad'),
).toBe('Bad');
});

it('orThens correctly on Err', () => {
expect(
getErr(3, '4')
.errThen(v => 30)
.errThen(v => v.toString())
.errOrElse('Bad'),
).toBe('30');
});

it('orThens correctly on Ok', () => {
expect(
getOk(3, '4')
.errThen(v => 30)
.errThen(v => v.toString())
.errOrElse('Bad'),
).toBe('Bad');
});

it('resolve calls Ok function with Ok object value', () => {
const spyLeft = jest.fn();
const spyRight = jest.fn();
getOk(3, '4').resolve(spyRight, spyLeft);
expect(spyRight).toHaveBeenCalled();
expect(spyRight).toHaveBeenCalledWith(3);
});

it('resolve calls Err function with Err object value', () => {
const spyLeft = jest.fn();
const spyRight = jest.fn();
getErr(3, '4').resolve(spyRight, spyLeft);
expect(spyLeft).toHaveBeenCalled();
expect(spyLeft).toHaveBeenCalledWith('4');
});

it('resolve calls Err function with Err object value after then', () => {
const spyLeft = jest.fn();
const spyRight = jest.fn();
getErr(3, '4')
.then(v => 10)
.resolve(spyRight, spyLeft);
expect(spyLeft).toHaveBeenCalled();
expect(spyLeft).toHaveBeenCalledWith('4');
});
});

0 comments on commit 65b946b

Please sign in to comment.