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

Modifying properties with Lenses #140

Open
Nvveen opened this issue Sep 7, 2020 · 3 comments
Open

Modifying properties with Lenses #140

Nvveen opened this issue Sep 7, 2020 · 3 comments

Comments

@Nvveen
Copy link

Nvveen commented Sep 7, 2020

🚀 Feature request

Current Behavior

Current modify and set functions in Prism, Optional, or Traverse only allow modifications to properties of the same type.

Desired Behavior

Methods to return new structures with changed values. For example:

const foo = { bar: { baz: '123' } } as const;
const fooL = pipe(L.id<typeof foo>(), L.prop('bar'));
const result = set(p => parseInt(p, 10))(fooL)(foo);
// { bar: { baz: 123 } }

If I understand correctly, I believe there's a way to use Iso and/or the imap function to be able to switch loss-less between two types, but expanding the interface to have different output types might be worth the effort. For example, Haskell provides Setter: http://hackage.haskell.org/package/lens-4.19.2/docs/Control-Lens-Setter.html which as 4 type parameters.
I am not proficient enough to come up with a proper solution in monocle-ts though.

@glebec
Copy link

glebec commented Sep 8, 2020

Related to #59 if I am reading this correctly.

@Nvveen
Copy link
Author

Nvveen commented Sep 8, 2020

So I was experimenting with a bunch of Typescript 4 features (including 4.1), and came up with the following proof of concept (that incidentally also goes for the same idea as in #136 ):
You can play around with it at https://tsplay.dev/OwEv4N
(note: the playground is a bit buggy, so I'm not 100% convinced this code runs correctly).

import * as fpTs from 'fp-ts';

const { pipe } = fpTs;

type Foo = {
    foo: {
        bar: {
            baz: number;
        };
    };
};

type GetType<S, P extends unknown[]> = P extends []
    ? S
    : P extends [infer Head, ...infer Tail]
    ? Head extends keyof S
    ? GetType<S[Head], Tail>
    : never
    : never;

type SetType<S, P extends unknown[], N = unknown> = P extends []
    ? N
    : P extends [infer Head, ...infer Tail]
    ? {
        [K in keyof S]: Tail extends [] ? N : K extends Head ? SetType<S[K], Tail, N> : S[K];
    }
    : never;

interface Lens<S, P extends unknown[]> {
    get: (s: S) => GetType<S, P>;
    set: <A>(a: A) => (s: S) => SetType<S, P, A>;
}

const id: <S>() => Lens<S, []> = () => ({
    get: (s) => s,
    set: a => s => a
})

const prop = <S, P extends unknown[], Prop extends keyof GetType<S, P>>(prop: Prop) => (lens: Lens<S, P>): Lens<S, [...P, Prop]> => ({
    get: s => lens.get(s)[prop] as GetType<S, [...P, Prop]>,
    set: <A>(ap: A) => s => {
        const oa = lens.get(s);
        if (ap === oa[prop]) return s as SetType<S, [...P, Prop], A>;
        return lens.set({ ...oa, [prop]: ap })(s) as SetType<S, [...P, Prop], A>;
    }
});

const set = <A>(a: A) => <S, P extends unknown[]>(lens: Lens<S, P>) => (s: S): SetType<S, P, A> => {
    const o = lens.get(s);
    return lens.set(o)(s) as SetType<S, P, A>;
};

const a = pipe(
    id<Foo>(),
    prop('foo'),
    prop('bar'),
    prop('baz'),
    set('abc')
);

const b = a({ foo: { bar: { baz: 123 }}}); // { foo: { bar: { baz: string } } }

As you can see there are a few casts going on, which I haven't figured out how to avoid yet.

@kylegoetz
Copy link

kylegoetz commented Sep 25, 2020

So this is a request for "polymorphic" optics. I asked something similar on SO with a functional programming tag (and Scala tag) and got the name, and then realized that the Scala Monocle lib has Lens/Prism/etc. as a special case (using a type alias) of the polymorphic PLens/PPrism/etc. For example, Lens[S,A] is a typealias of PLens[S,S,A,A].

I started porting the polymorphic versions over to TypeScript from Scala (primarily because a project I'm working on really could benefit from a polymorphic traversal (in the UI needing to traverse my Partial<State> that contains as properties various Partial<Child>[] and turn it into a State with Child[]) and adding them to a locally cloned copy of monocle-ts.

I figured if I made headway I'd offer a PR. Otherwise I'd just keep using it on my own projects. It would mean all the existing code for a non-polymorphic could be deleted and replaced with just a typealias, and I'm a bit uncomfortable with parachuting in with my code and wiping out real work people have done here.

I wonder if there was a reason the full polymorphic versions weren't ported over to this project, or if it was because it just wasn't important at the time things started.

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

3 participants