An educational pure functional programming library in TypeScript
1/**
2 * Type-safe optic composition.
3 *
4 * Composition table (outer \ inner → result):
5 *
6 * | outer \ inner | Lens | Prism | Optional | Traversal |
7 * |---------------|-----------|----------|----------|-----------|
8 * | Lens | Lens | Optional | Optional | Traversal |
9 * | Prism | Optional | Prism | Optional | Traversal |
10 * | Optional | Optional | Optional | Optional | Traversal |
11 * | Traversal | Traversal | Traversal| Traversal| Traversal |
12 *
13 * @module optics/compose
14 */
15
16import { isSome, flatMapOption, mapOption } from "../prelude/option"
17import type { Lens, Optional, Prism, Traversal } from "./types"
18
19// ── Most specific overloads ──────────────────────────────────────────────────
20
21export function compose<S, A, B>(outer: Lens<S, A>, inner: Lens<A, B>): Lens<S, B>
22export function compose<S, A, B>(outer: Prism<S, A>, inner: Prism<A, B>): Prism<S, B>
23
24// ── Optional-yielding overloads ──────────────────────────────────────────────
25
26export function compose<S, A, B>(outer: Lens<S, A>, inner: Prism<A, B>): Optional<S, B>
27export function compose<S, A, B>(outer: Prism<S, A>, inner: Lens<A, B>): Optional<S, B>
28export function compose<S, A, B>(outer: Lens<S, A>, inner: Optional<A, B>): Optional<S, B>
29export function compose<S, A, B>(outer: Optional<S, A>, inner: Lens<A, B>): Optional<S, B>
30export function compose<S, A, B>(outer: Prism<S, A>, inner: Optional<A, B>): Optional<S, B>
31export function compose<S, A, B>(outer: Optional<S, A>, inner: Prism<A, B>): Optional<S, B>
32export function compose<S, A, B>(outer: Optional<S, A>, inner: Optional<A, B>): Optional<S, B>
33
34// ── Traversal-yielding overloads (any + Traversal, Traversal + any) ──────────
35
36export function compose<S, A, B>(outer: Lens<S, A>, inner: Traversal<A, B>): Traversal<S, B>
37export function compose<S, A, B>(outer: Prism<S, A>, inner: Traversal<A, B>): Traversal<S, B>
38export function compose<S, A, B>(outer: Optional<S, A>, inner: Traversal<A, B>): Traversal<S, B>
39export function compose<S, A, B>(outer: Traversal<S, A>, inner: Lens<A, B>): Traversal<S, B>
40export function compose<S, A, B>(outer: Traversal<S, A>, inner: Prism<A, B>): Traversal<S, B>
41export function compose<S, A, B>(outer: Traversal<S, A>, inner: Optional<A, B>): Traversal<S, B>
42export function compose<S, A, B>(outer: Traversal<S, A>, inner: Traversal<A, B>): Traversal<S, B>
43
44// ── Implementation ───────────────────────────────────────────────────────────
45// eslint-disable-next-line @typescript-eslint/no-explicit-any
46export function compose(outer: any, inner: any): any {
47 const ot = outer._tag as string
48 const it = inner._tag as string
49
50 if (ot === "Lens" && it === "Lens") {
51 return {
52 _tag: "Lens",
53 get: (s: unknown) => inner.get(outer.get(s)),
54 set: (b: unknown) => (s: unknown) => outer.set(inner.set(b)(outer.get(s)))(s),
55 }
56 }
57
58 if (ot === "Prism" && it === "Prism") {
59 return {
60 _tag: "Prism",
61 preview: (s: unknown) => flatMapOption(inner.preview)(outer.preview(s)),
62 review: (b: unknown) => outer.review(inner.review(b)),
63 }
64 }
65
66 // Lens + Prism → Optional
67 if (ot === "Lens" && it === "Prism") {
68 return {
69 _tag: "Optional",
70 getOption: (s: unknown) => inner.preview(outer.get(s)),
71 set: (b: unknown) => (s: unknown) => outer.set(inner.review(b))(s),
72 }
73 }
74
75 // Prism + Lens → Optional
76 if (ot === "Prism" && it === "Lens") {
77 return {
78 _tag: "Optional",
79 getOption: (s: unknown) => mapOption(inner.get)(outer.preview(s)),
80 set: (b: unknown) => (s: unknown) => {
81 const oa = outer.preview(s)
82 return isSome(oa) ? outer.review(inner.set(b)(oa.value)) : s
83 },
84 }
85 }
86
87 // Optional-yielding: covers Lens+Opt, Opt+Lens, Prism+Opt, Opt+Prism, Opt+Opt
88 if (ot !== "Traversal" && it !== "Traversal") {
89 const outerGet = (s: unknown) =>
90 ot === "Lens" ? { _tag: "Some" as const, value: outer.get(s) }
91 : (ot === "Prism" ? outer.preview(s) : outer.getOption(s))
92 return {
93 _tag: "Optional",
94 getOption: (s: unknown) => {
95 const oa = outerGet(s)
96 if (!isSome(oa)) return oa
97 return it === "Lens" ? { _tag: "Some" as const, value: inner.get(oa.value) }
98 : it === "Prism" ? inner.preview(oa.value)
99 : inner.getOption(oa.value)
100 },
101 set: (b: unknown) => (s: unknown) => {
102 const oa = outerGet(s)
103 if (!isSome(oa)) return s
104 const newA = it === "Prism" ? inner.review(b) : inner.set(b)(oa.value)
105 return ot === "Lens" ? outer.set(newA)(s)
106 : ot === "Prism" ? outer.review(newA) : outer.set(newA)(s)
107 },
108 }
109 }
110
111 // Traversal-yielding: at least one side is a Traversal
112 const outerGetAll = (s: unknown): readonly unknown[] =>
113 ot === "Lens" ? [outer.get(s)]
114 : ot === "Prism" ? (isSome(outer.preview(s)) ? [outer.preview(s).value] : [])
115 : ot === "Optional" ? (isSome(outer.getOption(s)) ? [outer.getOption(s).value] : [])
116 : outer.getAll(s)
117 const innerGetAll = (a: unknown): readonly unknown[] =>
118 it === "Lens" ? [inner.get(a)]
119 : it === "Prism" ? (isSome(inner.preview(a)) ? [inner.preview(a).value] : [])
120 : it === "Optional" ? (isSome(inner.getOption(a)) ? [inner.getOption(a).value] : [])
121 : inner.getAll(a)
122 const innerModify = (f: (x: unknown) => unknown) => (a: unknown): unknown =>
123 it === "Lens" ? inner.set(f(inner.get(a)))(a)
124 : it === "Prism" ? (isSome(inner.preview(a)) ? inner.review(f(inner.preview(a).value)) : a)
125 : it === "Optional" ? (isSome(inner.getOption(a)) ? inner.set(f(inner.getOption(a).value))(a) : a)
126 : inner.modify(f)(a)
127 const outerModify = (g: (a: unknown) => unknown) => (s: unknown): unknown =>
128 ot === "Lens" ? outer.set(g(outer.get(s)))(s)
129 : ot === "Prism" ? (isSome(outer.preview(s)) ? outer.review(g(outer.preview(s).value)) : s)
130 : ot === "Optional" ? (isSome(outer.getOption(s)) ? outer.set(g(outer.getOption(s).value))(s) : s)
131 : outer.modify(g)(s)
132 return {
133 _tag: "Traversal",
134 getAll: (s: unknown) => outerGetAll(s).flatMap((a) => innerGetAll(a)),
135 modify: (f: (x: unknown) => unknown) => (s: unknown) => outerModify(innerModify(f))(s),
136 }
137}