gcanti / monocle-ts

Functional optics: a (partial) porting of Scala monocle
https://gcanti.github.io/monocle-ts/
MIT License
1.05k stars 52 forks source link

Optic implemented using Kliesli Arrows #186

Closed baetheus closed 1 year ago

baetheus commented 2 years ago

🚀 Feature request

Today I took a stab at implementing Optics via Kliesli Arrows here. The work was based on this gist by @serras. Ultimately, I think I've achieved a majority of the API of the experimental optics here without implementing separated ADTs for Iso, Lens, Prism, Optional, and Traversal.

EDIT: I've implemented them more fully here.

Current Behavior

Crossing the Optic barrier between any of the individual optics (ie. Lens => Prism), causes one to need to switch which combinators one is using to compose Optics. Generally, this leads one to having a large import list and remembering which Optic to start with. Following is an example:

import * as L from "monocle-ts/Lens";
import * as O from "monocle-ts/Optional";
import * as T from "monocle-ts/Traversal";
import * as A from "fp-ts/Array";
import { pipe } from "fp-ts/function";

type Friends = {
  people?: readonly {
    name: string;
    pets?: readonly {
      name: string;
      nickname?: string;
      toys: readonly string[];
    }[];
  }[];
}

const toysOptic = pipe(
  L.id<Friends>(),
  L.nullableProp("people"),
  O.traverse(A.Traversable),
  T.prop("pets"),
  T.fromNullable,
  T.traverse(A.Traversable),
  T.prop("toys"),
  T.traverse(A.traversable),
);

declare const friends: Friends;
const toys = pipe(friends, T.getAll(toysOptic)); // readonly string[]

Desired Behavior

I'd like to not think about whether I'm in a Lens or an Optional or a Prism or a Traversable. ie.

import * as L from "./arrow-optics.ts";
import { pipe } from "../fn.ts";

export type Friends = {
  people?: readonly {
    name: string;
    pets?: readonly {
      name: string;
      nickname?: string;
      toys: readonly string[];
    }[];
  }[];
};

const toysOptic = pipe(
  L.id<Friends>(), // Lens Type
  L.prop("people"),
  L.nilable, // Prism
  L.array, // Traversal
  L.prop("pets"),
  L.nilable,
  L.array,
  L.prop("toys"),
  L.array,
);

declare const friends: Friends;
const toys = toysOptic.get(friends); // readonly string[]

Suggested Solution

Obviously, I think the work I've already done here is the best way to start.

I've also looked at tagging the existing experimental Optics and building a generic compose here but that path very quickly hits limitations in TS type checking.

Who does this impact? Who is this for?

I think generic Optics are much easier to teach to beginners as there are fewer concepts to learn. One can quite literally forget about the implementation entirely and focus (heh) on using the library.

Describe alternatives you've considered

N/A They are described in Suggested Solution.

Additional context

I happened on Kliesli optics while implementing ploy profunctor optics. I am not great at Category Theory or Functional Programming yet, but I noticed that PureScript prefers profunctor optics but also prefers Profunctor Strong and Category compose over the equivelent formulation using Arrows. Even my implementation ended up effectively using the Identity, Option, and Array monads instead of implementing arrows. However, I think there might be some fertile ground building >=> in TS that I haven't had the time to look at.

baetheus commented 1 year ago

This is implemented in another library and it looks like optics are going a different direction in fp-ts. Thanks for looking at it!