Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Call-site polymorphism for lensIndex and lensProp #187

Closed
masaeedu opened this issue Aug 4, 2017 · 4 comments
Closed

Call-site polymorphism for lensIndex and lensProp #187

masaeedu opened this issue Aug 4, 2017 · 4 comments

Comments

@masaeedu
Copy link
Contributor

masaeedu commented Aug 4, 2017

lensIndex should be defined as follows:

        lensIndex<T extends number>(n: T): { 
            <U>(arr: ReadonlyArray<U>): U;
            <U>(newVal: U, arr: ReadonlyArray<U>): ReadonlyArray<U>;
        };

lensProp should be defined as follows:

        lensProp<TProp extends string>(prop: TProp): { 
            <U extends { [T in TProp] }>(obj: U): U[TProp];
            <U extends { [T in TProp] }>(newVal: U[TProp], obj: U): U;
        };

There's probably some mindbending recursive interface stuff that could be done to make lensPath("foo")("bar")("baz") be typed as <T extends { foo: { bar: { baz } } }>(x: T) => T["foo"]["bar"]["baz"], but my type tetris is too weak.

@masaeedu
Copy link
Contributor Author

masaeedu commented Aug 4, 2017

Just spotted that T is useless in lensIndex, so that can just be simplified to:

        lensIndex(n: number): { 
            <U>(arr: ReadonlyArray<U>): U;
            <U>(newVal: U, arr: ReadonlyArray<U>): ReadonlyArray<U>;
        };

It'd be cool if the type checking there could have been deferred similarly to lensProp, so that R.lensIndex(2)(["foo", "bar", "potato"]) is inferred as "potato", but alas.

@KiaraGrouwstra
Copy link
Member

Hm. Trying these in the console at the Ramda docs seems to be given something that matches with neither of these:

R.lensIndex(0)
// function
R.lensIndex(0)([1,2,3])
// function
R.lensIndex(0)('a', [1,2,3])
// function

Seems the getter / setter methods are somehow wrapped in that lens function.

It'd be cool if the type checking there could have been deferred similarly to lensProp, so that R.lensIndex(2)(["foo", "bar", "potato"]) is inferred as "potato", but alas.

Hm. The primary obstacle there would be in demanding a string. Guess we had a way around that by now. PoC for simplified versions (no setter), even if they Ramda might have structured them differently now:

export type NumberToString = { /*[i: number]: string;*/ 0:'0',1:'1',2:'2',3:'3',4:'4',5:'5'};
export type StringToNumber = { /*[k: string]: number;*/ 0:0,1:1,2:2,3:3,4:4,5:5};
// ^ adding `[k: string]: number;` gives a fallback but causes a bunch of `Type 'StringToNumber[I]' cannot be used to index type ...`

declare function lensProp<TProp extends string>(prop: TProp): {
    <U extends {[T in TProp]: any; }>(obj: U): U[TProp];
};
const obj: { a: 1, b: 2 } = { a: 1, b: 2 };
let x = lensProp('a')(obj);
// ^ 1
let y = lensProp('c')(obj);
// ^ error
let z = lensProp(null! as string)(obj);
// ^ any

declare function lensIndex<
    TProp extends number,
    TString extends string = NumberToString[StringToNumber[TProp]]
    >(prop: TProp): {
        <U extends {[T in TString]: any; }>(obj: U): U[TString];
    };

const arr: [1, 2] = [1, 2];
let a = lensIndex(0)(arr);
// ^ 1
let b = lensIndex(123)(arr);
// ^ error
let c = lensIndex(null! as number)(arr);
// ^ `any` with index, `1 | 2` without
type d = NumberToString[StringToNumber[number]];
// ^ from `lensIndex`, but errors when separated... wth?

I'll admit I'm confused by the result here, but yeah, progress. I guess since this doesn't have other dependencies we could already use it too, unlike much else.

Longer versions here; I'm on the fence over those indices now, and don't yet know a workaround to solve the issue with adding one to StringToNumber as well.
Still baffled the function actually already appears to work well even though the simpler scenario still just explodes.

@masaeedu
Copy link
Contributor Author

masaeedu commented Aug 6, 2017

@tycho01 I actually don't know how well the types actually align with the runtime Ramda library; I didn't try it out at runtime.

I was just describing how you can avoid losing type information with the type signatures you have now, which are:

    interface ManualLens<U> {
        <T extends Struct<any>>(obj: T): U; // get
        set<T extends Struct<any>>(v: U, obj: T): T;
        // <T extends Struct<any>>map(fn: (v: U) => U, obj: T): T
    }
    ///...
    lensIndex<T>(n: number): ManualLens<T>

This still demands you do lensIndex(10)(/* lens target */), but neither constrains the second argument nor produces string when applied to a ["foo"].

If Ramda returns some other kind of thing (functor apparently), we need to repeat ourselves here and make a special cased ArrayFunctor type that we return from lensIndex. Unless TypeScript supports arbitrary polymorphic values, instead of just polymorphic functions, it will be necessary to special case things down to the function level like this. See microsoft/TypeScript#17574

@KiaraGrouwstra
Copy link
Member

Thanks, yeah, ManualLens was admittedly a terrible last resort.

nor produces string when applied to a ["foo"]

R.view(R.lensIndex(10), 'foo')
// -> ""

😅

Unless TypeScript supports arbitrary polymorphic values, instead of just polymorphic functions, it will be necessary to special case things down to the function level like this. See microsoft/TypeScript#17574

Yeah, something like that.
If juggling generics gets tough maybe we could have a lens type based on the input getter/setter function types, with return values based on plugging the value in a la 6606. I dunno.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants