Skip to content

Commit

Permalink
feat(Maybe): introduce Maybe
Browse files Browse the repository at this point in the history
  • Loading branch information
pierrebeitz committed Feb 20, 2019
1 parent c8e1207 commit 8d45090
Show file tree
Hide file tree
Showing 2 changed files with 206 additions and 0 deletions.
158 changes: 158 additions & 0 deletions src/js/utils/Maybe.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/*
`Maybe` can help you with optional arguments, error handling, and records with optional fields.
*/

///////////////////////////////////////////////////////////////////////////////
// TYPES AND CONSTRUCTORS //
///////////////////////////////////////////////////////////////////////////////

/*
Represent values that may or may not exist. It can be useful if you have a
field that is only filled in sometimes. Or if a function takes a value
sometimes, but does not absolutely need it.
type Person = {
name: string;
age: Maybe<number>;
}
const sue = { name: "Sue", age: Nothing }
const tom = { name: "Tom", age: Just(42) }
*/
type Maybe<Value> = Just<Value> | Nothing;

interface Nothing {
tag: "Nothing";
}

interface Just<T> {
tag: "Just";
value: T;
}

const Just = <Value>(value: Value): Maybe<Value> => ({ tag: "Just", value });

const Nothing: Nothing = { tag: "Nothing" };

///////////////////////////////////////////////////////////////////////////////
// FUNCTIONS //
///////////////////////////////////////////////////////////////////////////////

/*
Convenience function to turn a value that might be undefined into a maybe.
const unsafeResult = ["muffins", "chocolate"].find(a => a == "cake")
const maybeCake = Maybe.fromValue(unsafeResult)
// typeof maybeCake === Maybe<string>
*/
type fromValue = <A>(value: A | undefined) => Maybe<A>;
const fromValue: fromValue = value =>
value !== undefined ? Just(value) : Nothing;

/*
Transform a `Maybe` value with a given function:
Maybe.map(Math.sqrt)(Just(9)) === Just(3)
Maybe.map(string => string.length)(Just("hallo")) === Just(5)
*/
type map = <A, B>(fn: (value: A) => B) => (maybe: Maybe<A>) => Maybe<B>;
const map: map = fn => maybe => {
switch (maybe.tag) {
case "Just":
return Just(fn(maybe.value));
case "Nothing":
return Nothing;
}
};

/*
Provide a default value, turning an optional value into a normal
value.
function ageInWords(person: Person): string {
const maybeText : Maybe<string> = Maybe.map(age => `is ${age} years old`)
return Maybe.withDefault("unknown")(maybeText)
}
*/
type withDefault = <V>(defaultValue: V) => (maybe: Maybe<V>) => V;
const withDefault: withDefault = defaultValue => maybe => {
switch (maybe.tag) {
case "Just":
return maybe.value;
case "Nothing":
return defaultValue;
}
};

/*
Chain together computations that may fail. It is helpful to see its
definition:
const andThen: andThen = callback => maybe => {
switch (maybe.tag) {
case "Just": return callback(maybe.value);
case "Nothing": return Nothing;
}
}
We only continue with the callback if things are going well. For
example, say you need to get entries in an array based on the value of
indices of other arrays:
const arrayOne = [2, 0, 1];
const arrayTwo = [0, 1];
const arrayThree = [42];
type getIndex = (array: string[]) => (index: number) => Maybe<string>
const getIndex : getIndex = array => index => {
const value = array[index]
return value ? Just(value) : Nothing
}
// let's use `pipe` from utils/pipe.js to better see what's going on.
const computeFinalValue = pipe(
Maybe.andThen(getIndex(arrayOne))
Maybe.andThen(getIndex(arrayTwo))
Maybe.andThen(getIndex(arrayThree))
)
console.log(computeFinalValue(Just(1))) // => Just 42
// it is given Just(1)
// andThen it retrieves the 1st index of arrayOne => 0
// andThen it retrieves the 0th index of arrayTwo => 0
// andThen it retrieves the 0th index of arrayThree => 42
console.log(computeFinalValue(Just(0))) // => Nothing
// it is given Just(0)
// andThen it retrieves the 0th index of arrayOne => 2
// andThen it retrieves the 1st index of arrayTwo => Nothing
// next steps are omitted and Nothing is returned immediately.
If any operation in the chain fails, the computation will short-circuit and
result `Nothing`. This may come in handy if we wanted to skip e.g. some
network requests.
*/
type andThen = <A, B>(fn: (v: A) => Maybe<B>) => (maybe: Maybe<A>) => Maybe<B>;
const andThen: andThen = callback => maybe => {
switch (maybe.tag) {
case "Just":
return callback(maybe.value);
case "Nothing":
return Nothing;
}
};

// Exporting only the type contructors and a `Maybe` bundling the functions,
// so application code is nudged to use the functions fully qualified.
const Maybe = {
andThen,
fromValue,
map,
withDefault
};
export { Just, Nothing, Maybe as default };
48 changes: 48 additions & 0 deletions src/js/utils/__tests__/Maybe-test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import Maybe, { Just, Nothing } from "../Maybe";

describe("Maybe", () => {
describe("#andThen", () => {
it("chains", () => {
expect(Maybe.andThen((v: number) => Just(v))(Just(1))).toEqual(Just(1));
});

it("short-circuits", () => {
const fn = jest.fn(x => x);

expect(Maybe.andThen(fn)(Nothing)).toEqual(Nothing);
expect(fn.mock.calls.length).toBe(0);
});
});

describe("#fromValue", () => {
it("turns `A | undefined` into Maybe<A>", () => {
const test = [1, 2].find(Number.isInteger);

expect(Maybe.fromValue(test)).toEqual(Just(1));
});

it("does not map Nothing", () => {
expect(Maybe.map(Number.isInteger)(Nothing)).toEqual(Nothing);
});
});

describe("#map", () => {
it("maps", () => {
expect(Maybe.map(Number.isInteger)(Just(1))).toEqual(Just(true));
});

it("does not map Nothing", () => {
expect(Maybe.map(Number.isInteger)(Nothing)).toEqual(Nothing);
});
});

describe("#withDefault", () => {
it("provides a default value for Nothing", () => {
expect(Maybe.withDefault(42)(Nothing)).toBe(42);
});

it("unwraps the value in Just", () => {
expect(Maybe.withDefault(42)(Just(1))).toBe(1);
});
});
});

0 comments on commit 8d45090

Please sign in to comment.