Skip to content

Instantly share code, notes, and snippets.

@jlongster
Created July 20, 2016 01:53
Show Gist options
  • Save jlongster/80daed0621243919428c90254858b4b3 to your computer and use it in GitHub Desktop.
Save jlongster/80daed0621243919428c90254858b4b3 to your computer and use it in GitHub Desktop.
// @flow
// Goal: define a top-level app state like the following. `sources`
// would be state managed by a redux reducer
type SourceState = { count: number };
type AppState = { sources: SourceState};
// Define a selector that takes a piece of the state, like sources
// (this is contrived)
function getNumSources(state: SourceState, x : number) {
return state.count * x;
}
// Define a `wrap` function that takes a selector and returns a
// new selector with the same signature, but it takes the full
// AppState (not just a piece of it)
function wrap<S,A,R>(selector: (state: S, ...args: A[]) => R, field : $Keys<AppState>) {
return function(state: AppState, ...args: A[]) : R {
return selector(state[field], ...args);
}
}
const wrapped = wrap(getNumSources, "sources");
wrapped({ sources: { count: 5 }}, 10);
@jlongster
Copy link
Author

This gives the following errors:

public/js/count.js:19
 19:     return selector(state[field], ...args);
                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function call
 19:     return selector(state[field], ...args);
                         ^^^^^^^^^^^^ object type. This type is incompatible with
 17: function wrap<S,A,R>(selector: (state: S, ...args: A[]) => R, field : $Keys<AppState>) {
     ^ some incompatible instantiation of `S`

public/js/count.js:24
 24: wrapped({ sources: { count: 5 }}, 10);
     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ function call
 24: wrapped({ sources: { count: 5 }}, 10);
                                       ^^ number. This type is incompatible with
 18:   return function(state: AppState, ...args: A[]) : R {
                                                 ^ some incompatible instantiation of `A`

The first one I can probably figure out, but I'm at a loss for the second one. I tried just doing A, but it tells me that rest parameters has to be an explicit array type. I don't know how to "copy" the function signature of the selector argument (or copy the type of the rest params).

@jimmyhmiller
Copy link

Here is one stab at it. Far from perfect, but maybe some progress.

// @flow

// Goal: define a top-level app state like the following. `sources`
// would be state managed by a redux reducer
type SourceState = { count: number };
type AppState = { sources: SourceState};

// Define a selector that takes a piece of the state, like sources
// (this is contrived)
function getNumSources(state: SourceState, x : number) {
  return state.count * x;
}

function error(state: {a: string}) {
    return state.a;
}

// Define a `wrap` function that takes a selector and returns a
// new selector with the same signature, but it takes the full
// AppState (not just a piece of it)
function wrap<A,R>(selector: (state: Object, ...args: A[]) => R, field : $Keys<AppState>) : (state: AppState, ...args: A[]) => R {
  return function(state: AppState, ...args: A[]) : R {
    return selector(state[field], ...args);
  }
}

const wrapped = wrap(getNumSources, "sources");
wrapped({ sources: { count: 5 }}, 10);

// doesn't type check
// const wrapped2 = wrap(getNumSources, "sources");
// wrapped2({ sources: { count: 5 }}, "10");

//shouldn't type check, but does because of Object
const wrapped3 = wrap(error, "sources")

// Really what I think we want is dependently typed, but flow doesn't support that
// It would look something like this.

// function wrap<A,R>(field : $Keys<AppState>, selector: (state: AppState[field], ...args: A[]) => R) : (state: AppState, ...args: A[]) => R {
//   return function(state: AppState, ...args: A[]) : R {
//     return selector(state[field], ...args);
//   }
// }

// If you think about it state is whatever type AppState[field] is.

@jlongster
Copy link
Author

I actually think I found the solution. I was fighting flow's inference system too much. If we just let it infer types, things actually just work! @jimmyhmiller thanks for trying to figure it out.

The solution is just to remove all the polymorphism and let it infer types:

// @flow

// Goal: define a top-level app state like the following. `sources`
// would be state managed by a redux reducer
type SourceState = { count: number };
type AppState = { sources: SourceState};

// Define a selector that takes a piece of the state, like sources
// (this is contrived)
function getNumSources(state: SourceState, x: number) {
  return state.count * x;
}

function getNumSources2(state: SourceState, x: number, y: number, z: number) {
  return state.count * x * y * z;
}


// Define a `wrap` function that takes a selector and returns a
// new selector with the same signature, but it takes the full
// AppState (not just a piece of it)
function wrap(selector, field : $Keys<AppState>) {
  return function(state: AppState, ...args) {
    return selector(state[field], ...args);
  }
}

const wrapped1 = wrap(getNumSources, "sources");
const wrapped2 = wrap(getNumSources2, "sources");
wrapped1({ sources: { count: 5 }}, 3);
wrapped2({ sources: { count: 5 }}, 3, 4, 5);

// Error! No "wrongKey" field in the AppState type
// wrapped1({ wrongKey: { count: 5 }}, 3);

// Error! All args to this selector should be numbers
// wrapped2({ sources: { count: 5 }}, 3, "hi", 5);

@jlongster
Copy link
Author

Actually, when I tried to use this in my project, I hit an error that I remember now. The problem is that I want to export the wrapped functions, so flow forces me to fully annotate them. Above, ...args is not annotated so flow complains when I try to export it.

@Gozala
Copy link

Gozala commented Jul 20, 2016

This works as expected, but you can't have variadic selectors. You'd need to pack params into a single object or a tuple instead, which is common limitation in languages with ADTs:

// @flow

// Goal: define a top-level app state like the following. `sources`
// would be state managed by a redux reducer
type SourceState = { count: number };
type AppState = { sources: SourceState};

// Define a selector that takes a piece of the state, like sources
// (this is contrived)
function getNumSources(state, x) {
  return state.count * x;
}

function getNumSources2(state, xs:Array<number>) {
  return xs.reduce((acc, x) => acc * x, state.count);
}


// Define a `wrap` function that takes a selector and returns a
// new selector with the same signature, but it takes the full
// AppState (not just a piece of it)
type Selector <outer, inner, settings> =
  (input:outer, options:settings) => inner

// Define a `wrap` function that takes a selector and returns a
// new selector with the same signature, but it takes the full
// AppState (not just a piece of it)
export const wrap = <outer:Object, inner, value, settings>
  ( selector: Selector<inner, value, settings>
  , field: $Keys<outer>
  ): <state:outer, config:settings> (input:state, options:config) => value =>
  (input, options) =>
  selector(input[field], options)


export const wrapped1 = wrap(getNumSources, "sources");     
wrapped1({ sources: { count: 5 }}, 3);
wrapped1({ sources: { count: 5 }}, 'Not a number');
/*
38: wrapped1({ sources: { count: 5 }}, 'Not a number');
                                       ^ string. This type is incompatible with
10: function getNumSources(state: SourceState, x: number) {
                                                  ^ number
*/

export const wrapped2 = wrap(getNumSources, "bar");
wrapped2({ sources: { count: 5 }}, 3);
/*
46: export const wrapped2 = wrap(getNumSources, "bar");
                                                ^ property `bar`. Property not found in
30:   , field: $Keys<outer>               ^ object literal
*/

export const wrapped3 = wrap(getNumSources2, "sources")
wrapped3({ sources: { count: 5 }}, [3, 4, 5])
wrapped3({ sources: { count: 5 }}, [3, 4, "5"])
/*
56: wrapped3({ sources: { count: 5 }}, [3, 4, "5"])
                                              ^ string. This type is incompatible with
14: function getNumSources2(state, xs:Array<number>) {
                                            ^ number
*/

Alternatively you can add extra type parameters to Selector type, that way if your selector just uses 1 parameter rest will be void if you use two than other then first two would be void etc.. So you can still do variadic functions up to limited number of parameters but annotations will be more complex and tim to type check will also increase.

P.S.: Trick was to make returned selector also polymorphic which is due to issue #2105

@gcanti
Copy link

gcanti commented Jul 20, 2016

@Gozala 1 nitpick and 2 possible issues.

Nitpick. This is a valid syntax for Flow

export const wrap = <outer:Object, inner, value, settings>
  ( selector: Selector<inner, value, settings>
  , field: $Keys<outer>
  ): <state:outer, config:settings> (input:state, options:config) => value =>
  (input, options) =>
  selector(input[field], options)

but surprisingly not for babel, which throws. Easy fix: transform to function

export function wrap<outer:Object, inner, value, settings>
  ( selector: Selector<inner, value, settings>
  , field: $Keys<outer>
  ): <state:outer, config:settings> (input:state, options:config) => value {
  return (input, options) =>
  selector(input[field], options)
}

Issues. You are using neither SourceState nor AppState, thus you can do something like (and Flow doesn't complain):

function getNumSources(state, x) {
  // return state.count * x;
  return state * x; // <= error
}

and

// wrapped1({ sources: { count: 5 }}, 3);
wrapped1({ sources: {}}, 3); // <= error

I'd like to propose a new implementation based on a more general (and type-checkable) slicer function instead of a field string:

lib.js

// @flow

type Selector<S, O, R> = (slice: S, options: O) => R;

export function wrap<ST, S, A, R>(selector: Selector<S, A, R>, slicer: (state: ST) => S): Selector<ST, A, R> {
  return function(state, options) {
    return selector(slicer(state), options)
  }
}

client.js

import { wrap } from './lib'

type SourceState = { count: number };
type AppState = { sources: SourceState};

const slicer = (state: AppState): SourceState => state.sources

function getNumSources(state, x: number) {
  return state.count * x
}

function getNumSources2(state, xs: Array<number>) {
  return xs.reduce((acc, x) => acc * x, state.count)
}

function getNumSources3(state, options: { n1: number, n2?: number }) {
  return state.count * options.n1 * (options.n2 ? options.n2 : 1)
}

export const wrapped1 = wrap(getNumSources, slicer)
wrapped1({ sources: { count: 5 }}, 3)
// wrapped1({ sources: {}}, 3) // throws
// wrapped1({ sources: { count: 5 }}, 'Not a number') // throws

// export const wrapped2 = wrap(getNumSources, "bar") // throws

export const wrapped3 = wrap(getNumSources2, slicer)
wrapped3({ sources: { count: 5 }}, [3, 4, 5])
// wrapped3({ sources: { counta: 5 }}, [3, 4, 5]) // throws
// wrapped3({ sources: { count: 5 }}, [3, 4, "5"]) // throws

export const wrapped4 = wrap(getNumSources3, slicer)
wrapped4({ sources: { count: 5 }}, { n1: 2 })
// wrapped4({ sources: { count: 5 }}, { n1: 2, n2: 'a' }) // throws

@Gozala
Copy link

Gozala commented Jul 21, 2016

In response @gcanti

@Gozala 1 nitpick and 2 possible issues.

Nitpick. This is a valid syntax for Flow

export const wrap = <outer:Object, inner, value, settings>
( selector: Selector<inner, value, settings>
, field: $Keys
): <state:outer, config:settings> (input:state, options:config) => value =>
(input, options) =>
selector(input[field], options)
but surprisingly not for babel, which throws. Easy fix: transform to function

There was a babel issue (can remember the number) that I've reported and has being solved not too long ago, so in latest versions of babel it works fine.

Issues. You are using neither SourceState nor AppState, thus you can do something like (and Flow doesn't complain):

function getNumSources(state, x) {
// return state.count * x;
return state * x; // <= error
}
and

// wrapped1({ sources: { count: 5 }}, 3);
wrapped1({ sources: {}}, 3); // <= error

Seems like another bug in flow, but can be avoid by annotating getNumSources

I'd like to propose a new implementation based on a more general (and type-checkable) slicer function instead of a field string:

Yeah I have suggested to use getter function to @jlongster as well on IRC, which avoids $Keys<object> hacks and is also more JIT friendly approach. To be honest going all the way and use something like lenses makes the most sense to me, but there were some Redux related issues preventing it, if I understood correctly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment