Coder Social home page Coder Social logo

results's Introduction

⚠️ Consider alternatives ⚠️

You may want to consider tools like TypeScript, which can now cover most of the use cases for results. (IMO less ergonomically in some ways, but, 🤷)

While short of what I might call "complete", I do still use this on projects where I just want to write reasonable JS code that runs directly in the browser. It's been pretty solid, and I'll probably continue using it! But it's unlikely to change much. So, use it or don't, but don't expect much evolution beyond bug-fixes :)

Results Build Status

Results is a tiny library bringing Discriminated Unions (aka Sum Types or Algebraic Types) to JavaScript, with match for better program flow control.

Results ships with full-featured Maybe (sometimes called an Option Type) and Result unions built-in, helping you safely deal with optional data and error handling.

The goal of Results is JavaScript with fewer bugs.

Install

$ npm install results

Quickstart

Result for error handling

import { Result, Ok, Err } from 'results';

function validateNumber(number) {
  if (isFinite(number)) {
    return Ok(number);
  } else {
    return Err(`expected a finite number but got '${number}' (a '${typeof number}')`);
  }
}

function computeSum(numbers) {
  if (!(numbers instanceOf Array)) {
    return Err(`expected an Array but got '${numbers}' (a '${typeof numbers}')`);
  }
  return Result.all(numbers.map(validateNumber))
    .andThen(nums => nums.reduce((a, b) => a + b));
}

// Since computeSum returns a Result (eiter an Err() or an Ok()), we can match
// for it and handle all possible cases:
Result.match(computeSum([1, 2, 3, 4, -5]), {
  Ok: sum => console.log(`The sum is: ${sum}`),
  Err: err => console.error(`Something went wrong: ${err}`)
});

// Result is a synchronous compliment to Promise, and plays nicely with it:
fetch('http://example.com/numbers')
  .then(resp => resp.json())
  .then(nums => computeSum(nums).toPromise())
  .then(sum => console.log(`The sum is: ${sum}`))
  .catch(err => console.error(`Something went wrong: ${err}`));

Maybe for nullable references

import { Maybe, Some, None } from 'results';

// Take a tree of Maybe({val: any, left: Maybe, right: Maybe}) and flatten it
// into an array of values:
function flattenDepthFirst(root) {
  return Maybe.match(root, {
    None: () => [],
    Some: node => [node.val]
                    .concat(flattenDepthFirst(node.left))
                    .concat(flattenDepthFirst(node.right))
  });
}

Maybe for default values and possibly-undefined return values

import { Maybe, Some, None } from 'results';

function printGreeting(name) {
  // get the name, or set a default if name is None()
  const nameToPrint = Maybe.match(name, {
    Some: n => n,
    None: () => 'friend'
  });
  console.log(`Hello, oh wonderful ${nameToPrint}!`);
}

// The Maybe union has helpful methods, like .unwrapOr for getting the value
// with a default:
function printGreeting(name) {
  const nameToPrint = name.unwrapOr('friend');
  console.log(`Hello, oh wonderful ${nameToPrint}!`)
}

// For functions whose result may not be defined, using Maybe encourages the
// caller to handle all cases
function get(obj, key) {
  if (obj.hasOwnProperty(key)) {
    return Some(obj[key]);
  } else {
    return None();
  }
}

Union as a powerful Enum

import { Union } from 'results';

const HTTPVerbs = Union({
  Options: {},  // the {} values are just placeholders, only the keys are used
  Head: {},
  Get: {},
  Post: {},
  Put: {},
  Delete: {}
}, {
  // the optional second object parameter to Union creates prototype methods:
  isIdempotent() {
    return HTTPVerbs.match(this, {
      Post: () => false,
      _: () => true  // "_" is reserved as a catch-all in match
    });
  }
});

let myVerb = HTTPVerbs.Get();
console.log(`Get ${myVerb.isIdempotent() ? 'is' : 'is not'} idempotent.`);
// => "Get is idempotent"
myVerb = HTTPVerbs.Post();
console.log(`Post ${myVerb.isIdempotent() ? 'is' : 'is not'} idempotent.`);
// => "Post is not idempotent"

HTTPVerbs.match(myVerb, {
  Delete: () => console.warn('some data was deleted!'),
  _: () => null
});

While there is nothing react-specific in Results, it does enable some nice patterns:

import React from 'react';
import { Union } from 'results';

const AsyncState = Union({
  Pending: {},
  Success: {},
  Failed: {}
});

class Spinner extends React.Component {
  static propTypes = {
    reqState: React.PropTypes.instanceOf(AsyncState.OptionClass)
  }
  render() {
    return AsyncState.match(this.props.reqState, {
      Pending: loaded => (
        <div className="spinner overlay spinning">
          <div className="spinner-animation">
            Loading {loaded}%...
          </div>
        </div>
      ),
      Failed: errMsg => (
        <div className="spinner overlay failed">
          <div className="spinner-err-message">
            <h3>Failed to load :( </h3>
            <p>{errMsg}</p>
          </div>
        </div>
      ),
      Success: <div style={{display: 'none'}}></div>
    });
  }
}

API

Union(options[, proto[, static_[, factory]]])

Creates a discriminated union with members specified in the options object.

Returns a union object.

  • options An object defining the members of the set. One member is added for each key of options, and the values are ignored. Almost any name can be used for the members except for two reserved names:

    • toString, which is automatically added for nicer debugging, and
    • OptionClass, which is used to attach the constructor function for member instances, for typechecking purposes. Union() will throw if either of those names are used as members in options.

    Maybe.None() is an example of a member added via options.

  • proto will be used to set the protoype of member instances. toString will automatically be added to the prototype by default, but if you define it in proto it will override the built-in implementations.

    Result.Ok(1).toPromise() is an example of a method attached through proto.

  • static_ like proto but for the object returned by Union(): functions defined here can inspect the union, like accessing this.OptionClass. By default, toString is added for you, but defining it in static_ will override the default implementation. Union() will throw if a key in static_ already exists in options.

    Result.all() is an example of a function attached through static_.

  • factory is not stable and should not be considered part of the public API :) It is used internally by Maybe and Result, check the source if you want to get down and dirty.

Union.is(first, second)

Deeply checks two union option members. This passes if:

  • first and second are strictly equal (===), or
  • They are instances of the same UnionOptionClass, and
    • They are the same member of the UnionOptionClass, and
    • Each matching payload parameter satisfies:
      • A recursive check of equality as defined by Union.is
  • or they both implement .valueOf which passes strict equality, or
  • they both implement .equals and first.equals(second)

These criteria and the implementation are stolen borrowed from Immutable, and in fact results's equality checks are compatible with Immutable's. Nesting Immutable collections in OptionClassInstances, and nesting OptionClassInstance in immutable collections are both supported.

This compatibility is totally decoupled from immutablejs -- results has no dependency on immutable whatsoever.

union object

Created by Union(), this is an object with a key for each member of the union, plus anything attached via static_, which include OptionClass and toString by default. It is not safe to iterate the keys of a union object.

Each member name's key maps to a factory to create a member instance, from a constructor called OptionClass (whose reference is also attached to the union object via they key "OptionClass").

match(option, paths) static method on union object

Automatically attached to every union object, .match is a better way to control program flow depending on which member of Union you are dealing with.

  • option the OptionClass instance to match against, like Some('hi') or Err(new Error(':(')). If option is not an instance of the union's OptionClass, match will throw.

  • paths an object, mapping member names to callback functions. The object must either exhaustively cover all members in the Union with callbacks, or map zero or more members to callbacks and provide a catch-all callback for the name '_'. If the coverage is not exhaustive, or if unrecognized names are included as keys, .match will throw.

.match will synchronously call the matching callback and return its result, passing all arguments given to the Union Option as arguments to the callback.

import { Union } from 'results';
const Stoplight = Union({  // Union(), creating a `union` object called StopLight.
  Red: {},
  Amber: {},
  Green: {}
});
Stoplight.match(Stoplight.Green(), {
  Red: () => console.error('STOP!!!'),
  Amber: () => console.warn('stop if you can'),
  Green: () => console.info('ok, continue')
});

options static property on union object

After creating a union object, the .options property references an object containing keys for each union option specified. It's not usually that useful unless you want to introspect the union and see what options it has -- powerful, but usually not necessary!

OptionClass() constructor

A function for creating OptionClass instances. You should not call this constructor directly -- it's exposed just for instanceof checks.

In the Stoplight example above, the following is ok:

assert(Stoplight.Green() instanceof Stoplight.OptionClass)

OptionClassFactory(...payloads) functions

Attached to union objects by keys named after the union's members. These functions create the "values" used in result. Maybe.Some(), Maybe.None(), Result.Ok(), and Result.Err() are all OptionClass factories. In the Stoplight example above, Stoplight.Green is an OptionClassFactory.

  • payloads a payload of any type can be passed as the only param. It will be stored on the OptionClass instance, and is accessible via .match. Proto methods may also extract the value for you, like .unwrap() on Maybe.

OptionClassInstance objects

The values that are usually passed around when using Results. They have three properties that you should consider an implementation detail, never access directly. Custom proto methods may access these properties if they wish. The property names are:

  • .options A reference to the object used to create the union with Union(). You can inspect its keys to find the members of this instance's union.
  • .name The member name of this OptionClass instance. Maybe.None().name === 'None'.
  • .payload The payload provided to OptionClassFactory. Stoplight.Red(1).payload is 1.

OptionClassInstance.equals(other)

Deep equality testing with another instance of a union option. See Union.is above. As with Union.is, this method is fully compatible with ImmutableJS.

Maybe -- [Union { Some, None }]

An optional type.

Maybe.Some(payload)

Also exported as Some from Results (import { Some } from 'results';).

  • payload A single parameter of any type. If it is an instance of Maybe.OptionClass, it will just be returned.

Maybe.None()

Also exported as None from Results (import { None } from 'results';). Accepts no parameters

Maybe.match(thing, paths)

defers to match (see above), but will only pass a single payload parameter to a callback for Some (no parameters are passed to a None callback).

Maybe.all(maybes)

Like Promise.all: takes an array of Some()s and None()s, and returns a Some([unwrapped maybes]) if they are all Some(), or None() if any are None(). Values in maybes that are not instances of Maybe.OptionClass are wrapped in Some().

  • maybes an array of Some()s and None()s or any other value.

Maybe.undefined(value)

Returns None() if value is undefined, otherwise wraps it as Some(value).

Maybe.null(value)

Like Maybe.undefined(value), but returns None() when value is null instead of when it is undefined.

Maybe.nan(value)

Like Maybe.undefined and Maybe.null but returns None() when value is NaN.

Prototype methods on Maybe (available on any instance of Some or None)

isSome() and isNone()

What you would hopefully expect :)

import { Some, None } from 'results';
assert(Some(1).isSome() && !Some(1).isNone());
assert(!None().isSome() && None().isNone());
unwrap()

Get the payload of a Some(), or throw if it's None().

expect(msg)

Like unwrap(), but throws an Error(msg) if it is None().

  • msg The message to throw with if it is None()
import { Some, None } from 'results';
const n = Some(1).unwrap();  // n === 1
const m = None().unwrap();  // throws an Error instance
const o = Some(1).expect('msg')  // o === 1
const p = None().expect('msg')  // throws Error('msg')
unwrapOr(def)

Like unwrap(), but returns def instead of throwing for None()

  • def A default value to use in case it's None()
unwrapOrElse(fn)

Like unwrapOr, but calls fn() to get a default value for None()

  • fn A callback accepting no parameters, returning a value for None()
import { None } from 'results';
const x = None().unwrapOr('z');  // x === 'z';
const y = None().unwrapOrElse(() => new Date());  // y === the current date.
okOr(err)

Get a Result from a Maybe

  • err an error payload for Err() if it's None()
okOrElse(errFn)
  • errFn a callback to get a payload for Err() if it's None()
import { Some, None } from 'results';
assert(Some(1).okOr(2).isOk() && None().okOr(2).isErr());
assert(None().okOrElse(() => 3).unwrapErr() === 3);
promiseOr(err) and promiseOrElse(errFn)

Like okOr and okOrElse, but returning a resolved or rejected promise.

import { Some, None } from 'results';
// the following will log "Some"
Some(1).promiseOr(2).then(d => console.log('Some'), e => console.error('None!'));
None().promiseOrElse(() => 1).catch(err => console.log(err));  // logs 1
and(other) and or(other)
  • other Some() or None(), or any value of any type which will be wrapped in Some().

Analogous to && and ||:

import { Some, None } from 'results';
Some(1).and(Some(2));  // Some(2)
Some(1).and(None());  // None()
None().and(Some(2));  // None()
Some(1).or(Some(2)).or(None());  // Some(1)
None().or(Some(1)).or(Some(2)); // Some(1);
andThen(fn)

Like and, but call a function instead of providing a hard-coded value. If fn returns a raw value instead of a Some or a None, it will be wrapped in Some().

  • fn If called on Some, fn is called with the payload as a param.
orElse(fn)

Like andThen but for Errs.

  • fn If called on Err, fn is called with the Error payload as a param.

Since andThen's callback is only executed if it's Some() and orElse if it's None, these two methods can be used like .then and .catch from Promise to chain data-processing tasks.

filter(fn)

Test a condition against the payload of a Some(payload). If fn returns something false-y, None is returned. Otherwise, the same Some(payload) is returned.

  • fn If called on Some, fn is called with the payload as a param.
import { Maybe } from 'results';

const isEven = x => x % 2 === 0;

Maybe.Some(42).filter(isEven);  // Some(42)
Maybe.Some(41).filter(isEven);  // None()

Result -- [Union { Ok, Err }]

An error-handling type.

Result.Ok(payload)

Also exported as Ok from Results (import { Ok } from 'results';).

  • payload A single parameter of any type. If it is an instance of Result.OptionClass, it will simply be returned.

Result.Err(err)

Also exported as Err from Results (import { Err } from 'results';).

  • err A single parameter of any type, but consider making it an instance of Error to follow Promise conventions.

Static methods on Result

Result.match(thing, paths)

defers to match (see above), but will only pass a single payload parameter to a callback for Ok or Err.

Result.all(results)

Like Promise.all: takes an array of Ok()s and Err()s, and returns a Ok([unwrapped oks]) if they are all Ok(), or the first Err() if any are Err(). Values in results that are not instances of Result.OptionClass are wrapped in Ok().

  • results an array of Ok()s and Err()s or any other value.
Result.try(fnMaybeThrows)

Return a Result.Ok() of the result of calling fnMaybeThrows(), or catch any error it throws and return it wrapped in Result.Err() instead.

Prototype methods on Result (available on any instance of Ok or Err)

isOk() and isErr()

What you would hopefully expect :)

import { Ok, Err } from 'results';
assert(Ok(1).isOk() && !Ok(1).isErr());
assert(!Err(2).isOk() && Err(2).isErr());
expect(err)

Returns the payload from an Ok(payload), or throws Error(err).

unwrap()

Get the payload of a Ok(), or throw payload if it's Err(payload).

import { Ok, Err } from 'results';
const n = Ok(1).unwrap();  // n === 1
const m = Err(2).unwrap();  // throws an Error instance
unwrapOr(def)

Like unwrap(), but returns def instead of throwing for Err()

  • def A default value to use in case it's Err()
unwrapOrElse(fn)

Like unwrapOr, but calls fn() to get a default value for Err()

  • fn A callback accepting the err payload as a parameter, returning a value for Err()
import { Err } from 'results';
const x = Err(1).unwrapOr('z');  // x === 'z';
const y = Err(2).unwrapOrElse(e => e * 2);  // y === 4.
ok() and err()

Get a Maybe from a Result

import { Ok, Err } from 'results';
assert(Ok(1).ok().isSome() && Err(2).err().isSome());
assert(Err(2).ok().isNone());
promise() and promiseErr()

Like ok() and err(), but returning a resolved or rejected promise.

import { Ok, Err } from 'results';
// the following will log "Ok"
Ok(1).promise().then(d => console.log('Ok'), e => console.error('Err!'));
Err(2).promise().catch(n => console.log(n));  // logs 2
Err(2).promiseErr().then(n => console.log(n));  // logs 2
and(other) and or(other)
  • other Ok() or Err(), or any value of any type which will be wrapped in Ok().

Analogous to && and ||:

import { Ok, Err } from 'results';
Ok(1).and(Ok(2));  // Ok(2)
Ok(1).and(Err(8));  // Err(8)
Err(8).and(Ok(2));  // Err(8)
Ok(1).or(Ok(2)).or(Err(8));  // Ok(1)
Err(8).or(Ok(1)).or(Ok(2)); // Ok(1);
andThen(fn)

Like and, but call a function instead of providing a hard-coded value. If fn returns a raw value instead of a Ok or a Err, it will be wrapped in Ok().

  • fn If called on Ok, fn is called with the payload as a param.
orElse(fn)

Like andThen but for Errs.

  • fn If called on Err, fn is called with the Error payload as a param.

Since andThen's callback is only executed if it's Ok() and orElse if it's Err, these two methods can be used like .then and .catch from Promise to chain data-processing tasks.

Credits

Results is written and maintained by uniphil, with help, support, and opinions from mystor.

The APIs for Maybe, and Result are heavily influenced by rust's Option and Result.

Changes

See changelog.md

results's People

Contributors

uniphil avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

results's Issues

Consider catching errors in `.andThen` and `.orElse`

Promise catches errors in .then and .catch, returning them as rejected promises. Does that make sense for Results too?

const v = Ok().andThen(() => { throw new Error(v); });
assert(v.isErr());
assert(v.unwrapErr() instanceof Error);

This might make Results automatically play nice with other functions or libraries that either return a value or throw, without users having to do anything.

rename Enum

since enum is a thing already.

Sum and Union are top contenders.

Consider only allowing one payload

After a few months of heavy use at OpenRide, I haven't wanted to use multiple payloads for any custom Unions at all. It's not clear that it's a useful feature.

Positive effects:

  • Simpler source code for Results
  • Faster option instance creation (early benchmarks are the reason Some and Ok are special-cased to only take 1 paylod)
  • Ok and Some's single-payload behaviour won't be special
  • Consistency with the Promise API
  • Easier to override the factory function
  • Simpler public API

The most interesting use-case I can think of for multiple payloads is something like a collection of Members, where there's a Pro class, Elite class, and Regular (Union({ Pro: null, Elite: null, Regular: null })). You could have a collection of members, and they could have different fields of data that would become accessible through the correct match callback, as multiple payloads.

However, the match callback args aren't checked, so it's still easy to make bugs there, and in any case, they could just take objects as their single payloads to do the same thing but probably better and more self-document-y.

I think I'm going to deprecate the feature.

Add Result.all(r1, r2, ...rn) function

Should look like Promise.all(), and work like it.

Maybe an Option.all as well

I guess these are methods on Enum instances, which is a thing that should be possible...

Missing (!a || !b) in _equals

_equals should have this check, that's currently missing.

I think I might have left it out because a should usually be an instance of a Union, but that assumption is false.

I don't have a failing test case yet, but here's a stack trace from a real piece of software:

/home/ubuntu/[redacted]/node_modules/results/index.js:121
  if (typeof a.valueOf === 'function' && typeof b.valueOf === 'function') {
              ^

TypeError: Cannot read property 'valueOf' of undefined
    at _equals (/home/ubuntu/[redacted]/node_modules/results/index.js:121:15)
    at /home/ubuntu/[redacted]/node_modules/results/index.js:116:14
    at Array.every (native)
    at _equals (/home/ubuntu/[redacted]/node_modules/results/index.js:115:19)
    at UnionOption.equalsProto [as equals] (/home/ubuntu/[redacted]/node_modules/results/index.js:138:10)
    at is (/home/ubuntu/[redacted]/node_modules/immutable/dist/immutable.js:716:16)
    at /home/ubuntu/[redacted]/node_modules/immutable/dist/immutable.js:3996:50
    at /home/ubuntu/[redacted]/node_modules/immutable/dist/immutable.js:1940:16
    at ArrayMapNode.iterate.HashCollisionNode.iterate [as iterate] (/home/ubuntu/[redacted]/node_modules/immutable/dist/immutable.js:2289:11)
    at src_Map__Map.__iterate (/home/ubuntu/[redacted]/node_modules/immutable/dist/immutable.js:1938:32)

I don't think a test-case requires interaction with Immutable.js, but it might.

Unwrapping constructor gets weird

Maybe.undefined(Maybe.None()) => Maybe.None(), not Maybe.Some(Maybe.None()).

I mean, if I wrote the second, it would also become Maybe.None() because of the unwrapping constructor, so this makes sense and does what it's supposed to, but it makes it weird when trying to do something like check for an optional parameter that is a Maybe to give it a default case.

Throw a custom error type

Should be a subclass, such that errFromResults instanceof Error still passes, but also lets users do errFromSomewhere instanceof Results.ResultsError or whatever.

Hopefully nice for debugging and user error-checking code in some cases (though hopefully the code is throwing fewer errors at all if it's using results).

consider adding .typeEquals proto

probably with a better name.

Something that checks that other is a) a member of this's Union AND other.name === this.name (ie., they are instances of the same member).

This is doable in user code, but not super-nice:

const MyUnion = Union({ A: {}, B: {}, C: {} },
{
  typeEquals(other) {
    if (!(other instanceof MyUnion.OptionClass)) {
      return false;
    } else {
      return other.name === this.name;
    }
  },
});

Having to reference MyUnion by name in the proto method makes this particularly annoying.

Consider options for Symbol instead of strings for `match` keys

the problem:

const U1 = Union({
  A: {},
  B: {},
});
const U2 = Union({
  A: {},
  Z: {},
});

const u1thing = U1.A(2, 3);
const u2thing = U2.A();

function takesAU1(thing) {
  return thing.match({
    A: (x, y) => x * y,
    [_]: () => 0,
  });
}

takesAU1(u1thing);  // U1's `.match` check validates, and we're good
takesAU1(u2thing);  // U1's `.match` check validates, but takesAU1 is not getting the thing it's expecting! this could be a hard bug.

The problem is that they only validation that match is doing is comparing strings. For safety, users have to find ways to try and make member names unique (like with namespacing: Union({U1A: {}, U1B: {}})) so that match can properly validate the options.

It would be better to compare based on a reference that comes from the union.

Here are some possible fixes:

1. with a Symbol() attached the the union

const U1 = Union({
  A: {},
  B: {},
});

const u1thing = U1.A(2, 4);

u1thing.match({
  [U1.A.symbol]: (x, y) => x * y,  // maybe with a better prop name than 'symbol'
  [_] => 0,
});

2. make union members classes and use an instanceof check

const U1 = Union({
  A: {},
  B: {},
});

const u1thing = U1.A(2, 4);

u1thing.match([
  [U1.A, (x, y) => x * y],
  [_, () => 0],
]);

// 2A (thanks Alexei)

u1thing.match(
  U1.A, (x, y) => x * y,
  _, () => 0
);

3. make union members match-aware

const U1 = Union({
  A: {},
  B: {},
});

const u1thing = U1.A(2, 4);

u1thing.match([
  U1.A.matchThen((x, y) => x * y),
  U1[_].matchThen(() => 0),
]);

4. Put match on the union instead of its members

const U1 = Union({
  A: {},
  B: {},
});

const u1thing = U1.A(2, 4);

U1.match(u1thing, {
  A: (x, y) => x * y,
  [_]: () => 0,
});

// or like,

U1(u1thing).match({
  A: (x, y) => x * y,
  [_]: () =. 0,
});

This last one can work because it's possible for U1 to know if any object is an instance of one of its members thanks to #31

enum typechecking

It would be nice to be able to check

  • Is this EnumOption X a member of Enum Y?

and more...

Consider removing Result and Maybe's .and and .or methods

So far I've used a Maybe's .and exactly once ever, and I think it would have still been clearer to use .andThen(() => thing) instead of .and(thing), since the point of it was to set a different value if Some(), not to do a logical comparison.

Shrinking the API by another 4 methods would kind of be nice.

equals breaks on date instances?

The equals implementation is supposed to match immutable's, which is supposed to use .valueOf to compare date instances.

We had unit tests break in a way that seemed like it was caused by date instances comparing false from results' .equals. Results should add some unit tests for equals with date instances.

clean up API

admit that it's not rust, and we can make the best small API that makes sense for javascript

  • Remove .map, .mapOr, .mapOrElse, .mapErr -- use .andThen, .andThen(x).or(), .andThen(x).orElse.
  • Remove .array, it's not useful...

Create singleton enum options

For things that don't need a payload, like (maaaybe) None and some use-cases of custom enums.

They should support .match and strict equality ===, which would make them great for redux reducers.

drop the UnionError custom error type

just use js Errors. no compelling use-case in real code has come up yet, for the custom errors.

(you should be returning errors via a results.Result type, not throwing exceptions anyway!)

nicer stack trackes
less weird error stuff to worry about

Maybe.all and Result.all flatten arrays in Somes and Oks

It should not. Promise.all does not unwrap arrays (as if something this wrong would be ok if Promise did it anyway 😄 )

eg:

Maybe.all([Maybe.some([1, 2, 3])])

should return

Maybe.Some([[1, 2, 3]]);

but instead yields

Maybe.Some([1, 2, 3]);

Apply params to the _ callback, not the instance.

To call .match, the user already has a ref to the instance, so that was completely pointless.
Many Unions have members taking payloads of the same types, so sometimes it is useful implement a catch-all route and expect the value. Otherwise it's necessary to implement .unwrap() or something

Consider adding Maybe.prototype.filter

We have a number of pieces of code at OpenRide where we want to conditionally operate on a Maybe value like

someMaybeThing
  .andThen(x => (expr) ? x : Maybe.None())
  .andThen(...);

This feels a lot like a blahArray.filter(x => (expr)).map(...), but it's more awkward to write. It would be nice to be able to do the same:

someMaybeThing
  .filter(x => (expr))
  .andThen(...);
  1. I don't know if filter is the right name -- makes it look like an Array.
  2. A helper function could also do this:
const filterMaybe = test => value =>
  test(value) ? Maybe.Some(value) : Maybe.None();


someMaybeThing(
  .andThen(filterMaybe(x => (expr)))
  .andThen(...);

@mystor I'm interested in your thoughts on this. I haven't seen a method like .filter on other Maybes, like Rust's or Elm's.

Add a Maybe.undefined

Wrap a value in Some unless it is undefined, in which case make it None.

This can be used to wrap foreign APIs that return undefined for a special null case. It's complimentary to #44 (Result.try), more easily bringing plain-js behaviours and values into results' model.

Consider supporting a promise-like 2-arg form of .andThen

Proposed:

Maybe.None().andThen(
  someVal => someExpr,
  () => noneExpr
);

not sure if it's actually valuable, since unlike Promise, we have .match

Maybe.match(Maybe.None(), {
  Some: someVal => someExpr,
  None: () => noneExpr
});

which is a little more explicit and clear anyway IMO. The only reason to add this would be further aligning with the Promise API.

Does not play nice with ImmutableJS

Specifically, Immutable.Map.prototype.merge tries to recurse into a UnionOption's props and everything an really does a number on it.

Is it possible to play better with Immutable? It would be awesome to hook into its equality checking and be make option instances fully Immutable-compatible...

Expose a friendly way to have an unwrapping constructor

...like the way Some(Some(1)) unwraps the outer Some and you get Some(1) back, which is inspired by Promise.resolve(promiseOrValue) which either wraps a plain value in Promise.resolve or returns the promise.

A specific use case example is a user-level AsyncData union with Done, Pending, and Errored. AsyncData.Done() unwraps an AsyncData or wraps a value in Done.

The current way to achieve this is to override the fourth param to Union, which sets a custom factory for creating option instances. It's not a friendly API.

Rethink keys

Right now .match is checking the name as a string. It's ok and basically works. But: probably worth looking up:

Symbol

can also maybe be used instead of the special string _ for wild-card matches?

Map can accept arbitrary things as keys (even NaN lol)

Use Union.isUnion(x) instead of instanceOf

Would also be key for custom unions, which will be much harder because the union might need to know its own name.

This change will be nice because it will mean that Results instances from different copies of Results will have compatible checks -- if I do Maybe.match(x, {...}) right now and x comes from a package with its own copy of Results, it will throw.

Immutable used to have this problem, and worked around it by making Immutable.Map.isMap(x) a thing instead of x instanceof Immutable.Map

Not sure how to get the .isXXX methods. Something generic like .isMember? Or break the api and tell custom unions their name,

const AsyncData = Union('AsyncData', {
  Done: {},
  Pending: {},
  Errored: {}
});
assert(typeof AsyncData.isAsyncData === 'function');

is sort of gross magic, but

AsyncData.isMember(AsyncData.Done())

is not totally offensive to me. It does mean reserving another prop name on Unions.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.