本篇文章首 lens over tea https://web.archive.org/web/20210521023726/https://artyom.me/lens-over-tea-1第一篇文章的启发,可以看作它的私人意译版本,关注点在与尝试用 ts 来取代原文中的 hs 语言,使得 ts 开发者能在更熟悉的语言环境中领悟原文中的精髓。

第一次尝试

文章开始,让我们从 lens 的基础语义出发,即 get 与 set。现在,我们先假设存在这样的一个需求,我们需要定义一组方法,用来向某个数组的第 i 个位置赋值和取值,需求很简单,很快,我们实现了第一版:

tsx
...
type Func<P, R = P> = (v: P) => R;
type Lens1<T> = {
get: (source: T[]) => T;
set: (nVal: T) => (source: T[]) => T[];
};

const lens1 = <T = any>(i: number) => {
const get = (a: T[]) => a[i];
const set = (nVal: T) => (source: T[]) => {
const newArr = [...source];
newArr[i] = nVal;
return newArr;
};

return {
get,
set,
} as Lens1<T>;
};

const view1 = <T>(l: Lens1<T>, source: T[]) => l.get(source);
const over1 = <T>(l: Lens1<T>, f: Func<T>, source: T[]) =>
l.set(f(l.get(source)))(source);

接着,我们希望对需求作出进一步约束,即我们需要实现一个方法,它可以同时用来实现 get 和 set 方法。我们很快可以得到:


键入'/'获得帮助

tsx
...
const getOrSet = <T>(
operator: keyof Lens1,
l: Lens1<T>,
f: Func<T>,
source: T[],
) => {
const focus = l.get(source);
if (operator === 'get') return focus; // look here
return l.set(f(focus))(source);
};

第二次尝试

但高亮这一行的实现细节依赖了字符串的相等,让我们寻求一个更好的解决方案。事实上,同时实现 get 和 set 的另一种表述是让一个方法同时返回 get 和 set 值。接下来,让我们修改一下 lens1 方法。

tsx
...
type Lens2_1<T = any> = (f: Func<T>) => (source: T[]) => [T, T[]];

const lens2_1 = <T = any>(i: number) => {
return (f: Func<T>) => (source: T[]) => {
const oVal = source[i];
const nVal = f(oVal);

return [oVal, createNewArray(nVal)]; // look here
function createNewArray(nVal: T) {
const newArr = [...source];
newArr[i] = nVal;
return newArr;
}
};
};

const view2_1 = <T>(l: Lens2_1<T>, source: T[]) => l((x) => x)(source)[0];
const over2_1 = <T>(l: Lens2_1<T>, f: Func<T>, source: T[]) => l(f)(source)[1];

这运行的很好,但与此同时,我们注意到当前的实现存在两个问题,注意看高亮行:

1.

在调用 get 方法的时候我们实际上也创建了一个 set 的结果(运行了多余的代码)

2.

lens 的实现细节使用了 tuple 来用作中间承接结果,即 lens 的实现细节耦合了 tuple 的先验知识。

面对问题 1,让我们仔细想想 set 和 get 的区分场景,实际上用来确定当前运行结果是 get 还是 set 的是在 lens 的调用阶段,即 view 和 over 函数的实现细节上。那么我们是否可以把当前运行环境是 get/set 的信息交由 view 和 over 来决定。实际上是可以的。

jsx
...
type Lens2_2<T = any> = (obj: {
isGet: boolean;
f: Func<T>;
}) => (source: T[]) => [T, T[]];
const lens2_2 = <T = any>(i: number) => {
return ({ f, isGet }: { isGet: boolean; f: Func<T> }) =>
(source: T[]) => {
const oVal = source[i];
if (isGet) return [oVal, null as any]; // look here
const nVal = f(oVal); // look here

return [oVal, createNewArray(nVal)];
function createNewArray(nVal: T) {
const newArr = [...source];
newArr[i] = nVal;
return newArr;
}
};
};

const view2_2 = <T>(l: Lens2_2<T>, source: T[]) =>
l({ isGet: true, f: (x: T) => x })(source)[0];
const over2_2 = <T>(l: Lens2_2<T>, f: Func<T>, source: T[]) =>
l({
isGet: false,
f,
})(source)[1];

实际上使用 isGet 这种硬编码字符串不太易于维护,通过观察,我们可以尝试使用 cps 变换来转交后续的控制权.

jsx
...
type Lens2_3<T = any> = (
toFunctor: (v: T) => (createArray: Func<T, T[]>) => [T, T[]],
) => (source: T[]) => [T, T[]];
const lens2_3 = <T = any>(i: number): Lens2_3<T> => {
return (toFunctor: (v: T) => Func<Func<T, T[]>, [T, T[]]>) =>
(source: T[]) => {
return toFunctor(source[i])(createNewArray);
function createNewArray(nVal: T) {
const newArr = [...source];
newArr[i] = nVal;
return newArr;
}
};
};

const view2_3 = <T>(l: Lens2_3<T>, source: T[]) =>
l((v) => () => [v, null as any])(source)[0];
const over2_3 = <T>(l: Lens2_3<T>, f: Func<T>, source: T[]) =>
l((v) => (createNewArray) => [null as any, createNewArray(f(v))])(source)[1];
```

事实上,我们也并不需要一定要 tuple 来中转返回值,view 和 over 直接返回目标结果就可以了


键入'/'获得帮助

jsx
...
const view2_temp = <T>(l: Lens2_3<T>, source: T[]) => l((v) => () => v)(source);
const over2_temp = <T>(l: Lens2_3<T>, f: Func<T>, source: T[]) =>
l((v) => (createNewArray) => createNewArray(f(v)))(source);
😱

注意,在我们疯狂调整代码的具体实现的时候,我们开始涉足较为复杂的ts类型提示的错误!!!

此时 Lens2_3 的类型报错,我们先看一下本次改动的需求

jsx
...
// 在view调用的时候,期望为
type Lens_View<T = any> = (
toFunctor: (v: T) => (createArray: Func<T, T[]>) => T,
) => (source: T[]) => T;

// 在set调用的时候,期望为
type Lens_Set<T = any> = (
toFunctor: (v: T) => (createArray: Func<T, T[]>) => T[],
) => (source: T[]) => T[];

这里的 T 与 T[]的区别来源于 set 和 view 的调用环境,这对 lens 本身应该是个黑盒行为,可以统一抽象为:

jsx
...
// 如果要统一的话应该为
type Lens_Placeholder<T = any, Placeholder = any> = (
toFunctor: (v: T) => (createArray: Func<T, T[]>) => Placeholder,
) => (source: T[]) => Placeholder;
😀

但事实上,我们并不想凭空多出一个额外的 Placeholder 类型变量,故我们重新思考 (createArray: Func<T, T[]>) => Placeholder的语义,它有另外一重表述,即接受一个Func<T,T[]>类型的函数,返回一个和 T 类型相关连的值,这里我们为了忽略不同返回值的定义(这里不能返回 any,这会直接丢掉类型信息),可以采取一个小技巧,将改函数看作值,即函数本身在类型上可以定义为包括 Paramameters 和 Return 两个属性的数据类型。

首先,我们将于Placeholder相关的部分提取出来,获得以下内容

jsx
...
// 这里我们可以提取于Placeholder关键的一个环节
type Part1<T = any, Placeholder = any> = (createArray: Func<T,T[]>)=>Placeholder;
type Part2<Placeholder = any> = Placeholder;

让我们思考如何定义一个和T相关的类型,用来表述上述的Part1和Part2,现在重新思考这个类型的需求,可以发现:Part2本质上是Part1的返回类型,而面对Part1,我们可以选择把Placeholder后置。即

jsx
...
type Part<T> = <Placeholder>(createArray:Func<T,T[]>)=>Placeholder;

type Len = <T = any> = (
toFunctor: (v: T) => Part<T>,
) => (source: T[]) => ReturnType<Part<T>>;

但首先于TS本身并不支持高阶类型变量,ReturnType<Part<T>>的实际结果是unkown,我们现在似乎完全没有办法获得Part<T>的返回值类型,那让我们转换一下思考方式,即我们是否能通过添加限制条件的方式来约束Part2使得我们可以使用现有的元素来拼凑出原本的内容。事实上是可以的,我们只要添加

tsx
...
type A = <T, R>(f: Func<T, T[]>) => R;
type AView = <T, R>(f: Func<T, T[]>) => T;
type ASet = <T, R>(f: Func<T, T[]>) => T[];
// 在忽略R的具体类型的时候,我们可以定义出等价的结构

type AFunctor<T> = {
fmap: (f: (v: T) => T[]) => AFunctor<T[]>;
};

type AView<T, T> = { getView: () => T } & AFunctor<T>;
type ASet<T, T[]> = { getSet: () => T[] } & AFunctor<T>;

仿照以上思路,我们重新定义 Lens2,注意这里特意分别定义了getViewgetSet来演示区分不同的返回值的情况,实际上,你可以将返回值都定义为 getValue,但你不应该在 lens 中直接 callgetValue,因为 lens 不应该假设这样的场景。

jsx
...
type AFunctor<T> = {
fmap: Func<Func<T, T[]>, unknown>;
};
type Lens2<T = any> = (
toFunctor: (v: T) => AFunctor<T>,
) => (source: T[]) => AFunctor<T>;

const lens2 = <T = any>(i: number): Lens2<T> => {
return (toFunctor: (v: T) => AFunctor<T>) => (source: T[]) => {
return toFunctor(source[i]).fmap(createNewArray);
function createNewArray(nVal: T) {
const newArr = [...source];
newArr[i] = nVal;
return newArr;
}
};
};

const view2 = <T>(l: Lens2<T>, source: T[]) =>
l((v) => {
return {
fmap() {
return this;
},
getView: () => v,
};
})(source).getView();
const over2 = <T>(l: Lens2<T>, f: Func<T>, source: T[]) =>
l((v) => {
let ans: T[];
return {
fmap(createArray) {
ans = createArray(f(v));
return this;
},
getSet: () => ans,
};
})(source).getSet();

至此, 我们定义了聚焦于某个数组的第 i 个位置的 lens 函数。接下来,让我们想一想是否可以将其中包含的思想类比到更宽泛的类型约束上。让我们重新回顾最终的 Lens2 类型