{"action":"created","issue":{"url":"https://api.github.com/repos/UrbanDoor/mobx-failable/issues/4","repository_url":"https://api.github.com/repos/UrbanDoor/mobx-failable","labels_url":"https://api.github.com/repos/UrbanDoor/mobx-failable/issues/4/labels{/name}","comments_url":"https://api.github.com/repos/UrbanDoor/mobx-failable/issues/4/comments","events_url":"https://api.github.com/repos/UrbanDoor/mobx-failable/issues/4/events","html_url":"https://github.com/UrbanDoor/mobx-failable/issues/4","id":301311452,"node_id":"MDU6SXNzdWUzMDEzMTE0NTI=","number":4,"title":"Joined futures","user":{"login":"kourge","id":6445,"node_id":"MDQ6VXNlcjY0NDU=","avatar_url":"https://avatars3.githubusercontent.com/u/6445?v=4","gravatar_id":"","url":"https://api.github.com/users/kourge","html_url":"https://github.com/kourge","followers_url":"https://api.github.com/users/kourge/followers","following_url":"https://api.github.com/users/kourge/following{/other_user}","gists_url":"https://api.github.com/users/kourge/gists{/gist_id}","starred_url":"https://api.github.com/users/kourge/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kourge/subscriptions","organizations_url":"https://api.github.com/users/kourge/orgs","repos_url":"https://api.github.com/users/kourge/repos","events_url":"https://api.github.com/users/kourge/events{/privacy}","received_events_url":"https://api.github.com/users/kourge/received_events","type":"User","site_admin":false},"labels":[{"id":852086762,"node_id":"MDU6TGFiZWw4NTIwODY3NjI=","url":"https://api.github.com/repos/UrbanDoor/mobx-failable/labels/proposal","name":"proposal","color":"8ed9e5","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":4,"created_at":"2018-03-01T08:06:35Z","updated_at":"2019-02-28T01:59:21Z","closed_at":null,"author_association":"CONTRIBUTOR","body":"Joined futures allow us to combine multiple futures into one future.\r\n\r\n## Motivation\r\n\r\nOften we want to do something such as render data only when two different failables are both successes. One way we can already achieve this is by performing nested matches:\r\n\r\n```ts\r\nclass EntryState {\r\n  @observable user = new Failable<User>();\r\n  @observable post = new Failable<Post>();\r\n}\r\n\r\n@observer\r\nclass Entry extends React.Component<{st: EntryState}> {\r\n  render() {\r\n    const {user: userF, post: postF} = this.props.st;\r\n    return userF.match({\r\n      success: user => (\r\n        postF.match({\r\n          success: post => (\r\n            // use both `user` and `post`\r\n          ),\r\n          failure: e => null,\r\n          pending: () => null,\r\n        })\r\n      ),\r\n      failure: e => null,\r\n      pending: () => null,\r\n    });\r\n  }\r\n}\r\n```\r\n\r\nThis excessively increases the number of cases to handle (the \"pyramid of doom\") as the number of successful things we want increases.\r\n\r\n## Illustration\r\n\r\nThere is prior art in solving this problem in the JavaScript Promise API. [`Promise.all`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) allows us to combine multiple promises, essentially allowing us to \"join\" them. With joined futures, the above code becomes:\r\n\r\n```ts\r\nclass EntryState {\r\n  @observable user = new Failable<User>();\r\n  @observable post = new Failable<Post>();\r\n}\r\n\r\n@observer\r\nclass Entry extends React.Component<{st: EntryState}> {\r\n  render() {\r\n    const {user: userF, post: postF} = this.props.st;\r\n    return Failable.all({user: userF, post: postF}).match({\r\n      success: ({user, post}) => (\r\n        // use both `user` and `post\r\n      ),\r\n      failure: e => null,\r\n      pending: () => null,\r\n    });\r\n  }\r\n}\r\n```\r\n\r\nNote that in this example, we exploit JavaScript destructuring assignments and object literal shorthands to avoid boilerplate code.\r\n\r\n## Interlude: `Promise.all`, prior art\r\n\r\nIn an earlier passage, `Promise.all` was mentioned. The signature of `Promise.all` is comprised of roughly the following definition:\r\n\r\n```ts\r\ninterface PromiseConstructor {\r\n  // higher-arity overloads, descending\r\n  // ...\r\n  all<T1, T2, T3>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>, T3 | PromiseLike<T3>]): Promise<[T1, T2, T3]>;\r\n  all<T1, T2>(values: [T1 | PromiseLike<T1>, T2 | PromiseLike<T2>]): Promise<[T1, T2]>;\r\n  all<T>(values: (T | PromiseLike<T>)[]): Promise<T[]>;\r\n}\r\n```\r\n\r\n- Each of the earlier overloads — containing multiple type parameters (e.g. `T1`, `T2`) as well as annotating `values` and the return type as a tuple type —  is considered _heterogeneous joining_, because each type parameter is a distinct type.\r\n- The last overload with a single generic type — taking an array of promises and joining them into a single promise of an array — is called _homogenous joining_, because although multiple values are involved, only one distinct type is involved.\r\n\r\nThe explosion of high arity overloads for heterogenous joining illustrates how such an API is still hard for TypeScript's type system to describe, since it does not yet support [variadic kinds](https://github.com/Microsoft/TypeScript/issues/5453).\r\n\r\nThere is, however, hope. Heterogenous joining is usually a bounded set rather than unbounded. In other words, at compile time, the number of distinct types is usually known and non-infinite. Using mapped types, we can hypothetically rewrite `Promise.all`'s API to be shaped like an object:\r\n\r\n```ts\r\ninterface PromiseConstructor {\r\n  all<T>(values: (T | PromiseLike<T>)[]): Promise<T[]>;\r\n  all<T extends object>(values: {[K in keyof T]: T[K] | PromiseLike<T[K]>}): Promise<T>;\r\n}\r\n```\r\n\r\nThis hypothetical rewrite would allow us to write `Promise.all` like this:\r\n\r\n```ts\r\nPromise.all({\r\n  user: fetchUser(userId),\r\n  post: fetchPost(postId),\r\n}).then(({user, post}) => {\r\n  // use both `user` and `post`\r\n});\r\n```\r\n\r\n## Specification\r\n\r\nThe proposed joined futures would be defined roughly as the following, with the `export` modifier omitted for brevity:\r\n\r\n```ts\r\nnamespace Failable {\r\n  function all<T>(values: Failable<T>[]): Failable<T[]>;\r\n  function all<T extends object>(values: {[K in keyof T]: Failable<T[K]>}): Failable<T>;\r\n}\r\n\r\nnamespace Loadable {\r\n  function all<T>(values: Loadable<T>[]): Loadable<T[]>;\r\n  function all<T extends object>(values: {[K in keyof T]: Loadable<T[K]>}): Loadable<T>;\r\n}\r\n```\r\n\r\n## Behavior\r\n\r\nAs with all monad-like structures with mapping behavior, we must be exact when specifying exactly _how_ different failables and loadables combine. For example, per the specification, `Promise.all` \"rejects with the reason of the first passed promise that rejects\". In other words, if one of the passed promises rejects, then the resulting combined promise rejects, even if all the other promises resolve.\r\n\r\n### `Failable`\r\n\r\nOn an attempt to join multiple failables, the following decisions are made in the order they appear. Therefore earlier cases take precedence over latter cases.\r\n\r\nWhen given an empty object or empty array, the joined failable is a success whose value is an empty array (giving the type `Failable<never[]>`) or empty object (giving the type `Failable<{}>`).\r\n\r\nWhen any failable among multiple given failables is a pending, the joined failable is a pending.\r\n\r\nWhen any failable among multiple given failables is a failure, the joined failable is a failure:\r\n\r\n- If given an array, the error value of the failure is that of the first failable that is a failure.\r\n- If given an object, the error value of the failure is arbitrary and random. (See later sections for ways around this limitation.)\r\n\r\nLastly, when all failables given are successes, the joined failable is a success:\r\n\r\n- If given an array, the value of the success is that of an array containing the inner success values of every failable in the order that those failables appear in the array.\r\n- If given an object, the value of the success is an object with the same keys as the given object, where every value is the inner success value of the failable corresponding to that key.\r\n\r\n### `Loadable`\r\n\r\nAs a quick reminder, there are six loadable states, categorized by two traits: _availability_ (none, value, error) and _flight_ (idle, busy):\r\n\r\nAvailability / Flight | Idle | Busy\r\n-- | -- | --\r\nNone | Empty | Pending\r\nValue | Success | Reloading\r\nError | Failure | Retrying\r\n\r\nThe abstract operation **JoinFlight(_f1_, _f2_, ...)** takes one or more flights and is defined as thus:\r\n\r\n- If any one of the flights is _busy_, the result is _busy_.\r\n- Otherwise all of the flights are _idle_, and therefore the result is _idle_.\r\n\r\nThe abstract operation **JoinAvailability(_a1_, _a2_, ...)** takes one or more availabilities and is defined as thus:\r\n\r\n- If any one of the availabilities is _none_, the result is _none_.\r\n- Else, if any one of the availabilities is _error_, the result is _error_.\r\n- Otherwise all of the flights are _value_, and therefore the result is _value.\r\n\r\nBaseline: first, if given an empty object or empty array, the joined loadable is a success whose value is an empty array (giving the type `Loadable<never[]>`) or empty object (giving the type `Loadable<{}>`).\r\n\r\nOn an attempt to join multiple loadables:\r\n\r\n- The joined loadable's flight is the result of gathering the flight of each loadable and applying **JoinFlight** to them.\r\n- The joined loadable's flight is the result of gathering the availability of each loadable and applying **JoinAvailability** to them.\r\n\r\nWhen the joined loadable's availability is error:\r\n\r\n- If given an array, the error value of the joined loadable is that of the first loadable with an error availability.\r\n- If given an object, the error value of the joined loadable is arbitrary and random. (See later sections for ways around this limitation.)\r\n\r\nWhen the joined loadable's availability is value:\r\n\r\n- If given an array, the value of the joined loadable is that of an array containing the inner values of every loadable in the order that those loadables appear in the array.\r\n- If given an object, the value of the joined loadable is an object with the same keys as the given object, where every value is the inner value of the loadable corresponding to that key.\r\n\r\n## Supplementary computed properties\r\n\r\nAs noted in earlier sections, there are a few flaws when attempting to combine multiple pending or failure futures:\r\n\r\n- Obtaining a random error value is not helpful in identifying which future the error originated from.\r\n- It is also difficult to identify which specific future is loading and which isn't.\r\n\r\nWe further propose three additional computed properties to be made available on joined futures:\r\n\r\n- `errors`, an aggregate of all the possible error values\r\n- `pendings`, an aggregate of all the pending states in the form of booleans\r\n- `values`, an aggregate of all the possible values\r\n"},"comment":{"url":"https://api.github.com/repos/UrbanDoor/mobx-failable/issues/comments/468105653","html_url":"https://github.com/UrbanDoor/mobx-failable/issues/4#issuecomment-468105653","issue_url":"https://api.github.com/repos/UrbanDoor/mobx-failable/issues/4","id":468105653,"node_id":"MDEyOklzc3VlQ29tbWVudDQ2ODEwNTY1Mw==","user":{"login":"kourge","id":6445,"node_id":"MDQ6VXNlcjY0NDU=","avatar_url":"https://avatars3.githubusercontent.com/u/6445?v=4","gravatar_id":"","url":"https://api.github.com/users/kourge","html_url":"https://github.com/kourge","followers_url":"https://api.github.com/users/kourge/followers","following_url":"https://api.github.com/users/kourge/following{/other_user}","gists_url":"https://api.github.com/users/kourge/gists{/gist_id}","starred_url":"https://api.github.com/users/kourge/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/kourge/subscriptions","organizations_url":"https://api.github.com/users/kourge/orgs","repos_url":"https://api.github.com/users/kourge/repos","events_url":"https://api.github.com/users/kourge/events{/privacy}","received_events_url":"https://api.github.com/users/kourge/received_events","type":"User","site_admin":false},"created_at":"2019-02-28T01:59:21Z","updated_at":"2019-02-28T01:59:21Z","author_association":"CONTRIBUTOR","body":"Another possibility is that it still has a single error, but the error is the last one that occurred.\r\n\r\nI'm also open to the possibility of completely dropping the array version of `Failable.all` and only take objects. After all, an array is still an object, but one whose keys are numbers."}}
{"actor":{"display_login":"kourge"}}