镜头等效进化或镜头到多个值
Lens equivalent of evolve or lens to multiple values
我的理解是镜头是包含 get
和 set
值的函数。
我有这个辅助功能:
const overEach = uncurryN(3,
fn => lenses =>
lenses.length > 0 ?
compose(...map(flip(over)(fn), lenses)) :
identity
);
正在使用
const annual = ["yearsPlayed", "age"];
const annualInc = overEach(
inc,
map(lensProp, annual),
);
console.log(
annualInc({
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
})
);
输出:
{
jersey: 41,
age: 11,
yearsPlayed: 3,
id: 3.14159
}
这很有趣,因为(就像进化一样),我可以定义某种形状的东西是如何改变的。这比进化更好,因为它让我清楚地分离了对数据形状和我对它所做的事情的关注。这比进化更糟糕,因为它创建了一个我从未使用过的中间值。我拥有的镜头越多,我创造的中间值就越多。
{
jersey: 148,
age: 10,
yearsPlayed: 3,
id: 3.14159
}
我很想知道是否有一种方法可以定义指向多个值的镜头。 compose(lenseIndices([1,7,9]), lensProp('parents'), lensIndex(0))
可能指向三个不同人中的第一个 parent。
在我看来这真的应该是可能的,但我不知道要搜索什么,我宁愿不 re-invent 轮子(特别是因为我没有带着镜头在杂草丛中yet),如果可以而且也已经完成了。
I'd be curious to know if there's a way to define a lens that points to more than one value.
我们对“镜头”的直觉应该是它“聚焦”在数据结构的特定部分。所以真的,不。镜头就是要处理特定的东西。 (但请参阅下面的更新,该更新表明此特定内容 而不是 必须是单个 属性。)
Ramda's issue #2457 更详细地讨论了镜头的使用。
与 evolve
相比,我认为我不同意你对你的函数提供的额外灵活性的解释。事实上,如果我要实现它,我可能会在 evolve
之上这样做:
const {evolve, fromPairs, inc} = R
const overEach = (fn, names) =>
evolve (fromPairs (names .map (name => [name, fn])))
const annualInc = overEach (inc, ["yearsPlayed", "age"])
console .log (annualInc ({
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
}))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js"></script>
并且 evolve
让您可以轻松地为不同的属性选择不同的函数,允许您嵌套转换,并且非常具有声明性。
overEach
只是允许我们将相同的转换函数应用于许多不同的节点。这当然很有用,但似乎不像 evolve
.
的正常情况那么常见
更新
我想澄清一下我上面说的。虽然 lenses focus 在数据结构的特定部分,但这并不意味着它们只能影响一个 field 或 属性 一个对象。我们需要更全面地考虑这一点。该部分可以是多个字段,可能带有子字段。我认为这是最容易通过示例来描述的。
让我们假设您有完善的 box
函数来描述笛卡尔网格上的一个框。它具有只读 position、width 和 height 属性,以及移动它的方法、缩放它,列出角落,找到区域 所有这些都正常运行,返回新框而不是改变原始框。您对这段代码非常满意:
const box = (l, t, w, h) => ({
move: (dx, dy) => box (l += dx, t += dy, w, h),
scale: (dw, dh) => box (l, t, w *= dw, h *= dh),
area: () => w * h,
get position () { return {x: l, y: t} },
get width () { return w},
get height () { return h },
corners: () => [{x: l, y: t}, {x: l + w, y: t}, {x: l + w, y: t + h}, {x: l, y: t + h}],
toString: () => `Box (left: ${l}, top: ${t}, height: ${h}, width: ${w})`
})
但是现在您想将您的工具应用到一个新的环境中,您的小部件看起来像这样:
const widget = {
topLeft: {x: 126, y: 202},
bottomRight: {x: 776, y: 682},
borderColor: 'red',
borderWidth: 3,
backgroundUrl: 'http://example.com/img.png',
// ...
}
虽然 topRight
和 bottomLeft
点是描述矩形的兼容方式,但您将不得不重写一堆已经处理框的代码来处理这些新的小部件。此外,盒子似乎是这种情况的逻辑视图。高度和宽度似乎比右下角更重要。这里我们可以用镜头来处理顾虑。也就是说,我们可以完全在盒子里思考,从widget中提取一个盒子,通过调整盒子来调整值。我们只需要写一个镜头就可以了:
const boxLens = lens (
({topLeft: {x: x1, y: y1}, bottomRight: {x: x2, y: y2}}) =>
box (x1, y1, x2 - x1, y2 - y1),
({position: {x, y}, width, height}, widget) => ({
...widget,
topLeft: {x, y},
bottomRight: {x: x + width, y: y + height}
})
)
现在我们可以像用框描述一样处理小部件的位置和范围:
view (boxLens, widget) .toString ()
//=> "Box (left: 126, top: 202, height: 480, width: 650)"
view (boxLens, widget) .corners ()
//=> [{x: 126, y: 202}, {x: 776, y: 202}, {x: 776, y: 682}, {x: 126, y: 682}]
set (boxLens, box (200, 150, 1600, 900), widget)
//=> {topLeft: {x: 200, y: 150}, bottomRight: {x: 1800, y: 1050}, borderColor: "red", ...}
over (boxLens, box => box .scale (.5, .5), widget)
//=> {topLeft: {x: 126, y: 202}, bottomRight: {x: 451, y: 442}, borderColor: "red", ...}
const moveWidget = (dx, dy) =>
over(boxLens, box => box .move (dx, dy))
moveWidget (10, 50) (widget)
//=> {topLeft: {x: 136, y: 252}, bottomRight: {x: 786, y: 732}, borderColor: "red", ...}
您可以在以下代码段中确认这一点:
const {lens, view, set, over} = R
const box = (l, t, w, h) => ({
move: (dx, dy) => box (l += dx, t += dy, w, h),
scale: (dw, dh) => box (l, t, w *= dw, h *= dh),
area: () => w * h,
get position () { return {x: l, y: t} },
get width () { return w},
get height () { return h },
corners: () => [{x: l, y: t}, {x: l + w, y: t}, {x: l + w, y: t + h}, {x: l, y: t + h}],
toString: () => `Box (left: ${l}, top: ${t}, height: ${h}, width: ${w})`
})
const boxLens = lens(
({topLeft: {x: x1, y: y1}, bottomRight: {x: x2, y: y2}}) => box (x1, y1, x2 - x1, y2 - y1),
({position: {x, y}, width, height}, widget) => ({
...widget,
topLeft: {x, y},
bottomRight: {x: x + width, y: y + height}
})
)
const widget = {
topLeft: {x: 126, y: 202},
bottomRight: {x: 776, y: 682},
borderColor: 'red',
borderWidth: 3,
backgroundUrl: 'http://example.com/img.png',
// ...
}
console .log (
view (boxLens, widget) .toString ()
)
console .log (
view (boxLens, widget) .corners ()
)
console .log (
set (boxLens, box (200, 150, 1600, 900), widget)
)
console .log (
over (boxLens, box => box .scale (.5, .5), widget)
)
const moveWidget = (dx, dy) =>
over(boxLens, box => box .move (dx, dy))
console .log (
moveWidget (10, 50) (widget)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
重点
这表明我们可以使用镜头一次处理多个字段,正如 Mrk Sef 的自我回答也解释的那样。但是我们必须以某种与原始同构的方式处理它们。这实际上是镜头的一个非常强大的用途。但这并不意味着我们可以简单地使用它们来处理任意属性。
到目前为止我学到了什么
这可能不是个好主意。问题是镜片需要具有某些特性才能工作。这些属性之一是:
view(lens, set(lens, a, store)) ≡ a — If you set a value into the store, and immediately view the value through the lens, you get the value that was set.
如果您希望镜头在没有进一步限制的情况下指向多个值,则该信息必须(以某种方式)在被更改的数据结构中进行编码。如果一个键被设置为一个数组,该数组编码它自己的大小。但是,如果设置数组实际上对应于设置 something else
,那么 something else
的某些子集必须与数组同构(增长、收缩、重新排序,整个 shebang)。这样就可以随时来回转换了。
如果您对进一步的限制感到满意,您可以做更多的事情,但结果并不理想。
这是一个功能齐全(据我所知)的镜头实现,它指向多个属性但限制了允许设置的属性。
const subsetOf = pipe(without, length, equals(0));
const subset = flip(subsetOf);
const lensProps = propNames => lens(
pick(propNames),
(update, data) =>
subset(keys(update), propNames) ?
({ ...data, ...update }) :
call(() => {throw new Error("OH NO! LENS LAW BROKEN!");})
);
正在使用:
const annualLens = lensProps(["yearsPlayed", "age"]);
const timmy = {
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
};
console.log(
"View Timmy's Annual Props: ",
view(annualLens, timmy)
);
console.log(
"Set Timmy's Annual Props: ",
set(annualLens, {yearsPlayed: 100, age: 108}, timmy)
);
console.log(
"Update Timmy's Annual Props: ",
over(annualLens, map(inc), timmy)
);
// Break the LAW
set(annualLens, {newKey: "HelloWorld"}, timmy);
输出:
View Timmy's Annual Props: { age: 10, yearsPlayed: 2 }
Set Timmy's Annual Props: { jersey: 148, age: 108, yearsPlayed: 100, id: 3.14159 }
Update Timmy's Annual Props: { jersey: 148, age: 11, yearsPlayed: 3, id: 3.14159 }
Error: OH NO! LENS LAW BROKEN!
您可以想象编写一个采用路径而不是名称的版本,但是从那以后为了使用 set
,这实际上并没有帮助,您需要知道镜头的路径期待 set
.
但情况变得更糟。你可以组成这些类型的镜头,但实际上没有任何意义:
compose(lensIndex(0), lensProps(["a","b"]), lensProp("b"))
与
相同
compose(lensIndex(0), lensProp("b"))
所以虽然它没有破坏任何东西,但它很快就会变得非常无趣。实际上,它的唯一用途是作为构图中的 'outermost' 镜头。即使那样,它也可能必须受到限制才能有用。
好的一面是,作为最外面的镜头,它实际上可以在没有中间物体的情况下改变多个值。但这不是很好,因为您可以使用 evolve
作为传递给 over
的函数,并且在不丢失任何东西的情况下加入额外的功能。
我的理解是镜头是包含 get
和 set
值的函数。
我有这个辅助功能:
const overEach = uncurryN(3,
fn => lenses =>
lenses.length > 0 ?
compose(...map(flip(over)(fn), lenses)) :
identity
);
正在使用
const annual = ["yearsPlayed", "age"];
const annualInc = overEach(
inc,
map(lensProp, annual),
);
console.log(
annualInc({
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
})
);
输出:
{
jersey: 41,
age: 11,
yearsPlayed: 3,
id: 3.14159
}
这很有趣,因为(就像进化一样),我可以定义某种形状的东西是如何改变的。这比进化更好,因为它让我清楚地分离了对数据形状和我对它所做的事情的关注。这比进化更糟糕,因为它创建了一个我从未使用过的中间值。我拥有的镜头越多,我创造的中间值就越多。
{
jersey: 148,
age: 10,
yearsPlayed: 3,
id: 3.14159
}
我很想知道是否有一种方法可以定义指向多个值的镜头。 compose(lenseIndices([1,7,9]), lensProp('parents'), lensIndex(0))
可能指向三个不同人中的第一个 parent。
在我看来这真的应该是可能的,但我不知道要搜索什么,我宁愿不 re-invent 轮子(特别是因为我没有带着镜头在杂草丛中yet),如果可以而且也已经完成了。
I'd be curious to know if there's a way to define a lens that points to more than one value.
我们对“镜头”的直觉应该是它“聚焦”在数据结构的特定部分。所以真的,不。镜头就是要处理特定的东西。 (但请参阅下面的更新,该更新表明此特定内容 而不是 必须是单个 属性。)
Ramda's issue #2457 更详细地讨论了镜头的使用。
与 evolve
相比,我认为我不同意你对你的函数提供的额外灵活性的解释。事实上,如果我要实现它,我可能会在 evolve
之上这样做:
const {evolve, fromPairs, inc} = R
const overEach = (fn, names) =>
evolve (fromPairs (names .map (name => [name, fn])))
const annualInc = overEach (inc, ["yearsPlayed", "age"])
console .log (annualInc ({
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
}))
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.js"></script>
并且 evolve
让您可以轻松地为不同的属性选择不同的函数,允许您嵌套转换,并且非常具有声明性。
overEach
只是允许我们将相同的转换函数应用于许多不同的节点。这当然很有用,但似乎不像 evolve
.
更新
我想澄清一下我上面说的。虽然 lenses focus 在数据结构的特定部分,但这并不意味着它们只能影响一个 field 或 属性 一个对象。我们需要更全面地考虑这一点。该部分可以是多个字段,可能带有子字段。我认为这是最容易通过示例来描述的。
让我们假设您有完善的 box
函数来描述笛卡尔网格上的一个框。它具有只读 position、width 和 height 属性,以及移动它的方法、缩放它,列出角落,找到区域 所有这些都正常运行,返回新框而不是改变原始框。您对这段代码非常满意:
const box = (l, t, w, h) => ({
move: (dx, dy) => box (l += dx, t += dy, w, h),
scale: (dw, dh) => box (l, t, w *= dw, h *= dh),
area: () => w * h,
get position () { return {x: l, y: t} },
get width () { return w},
get height () { return h },
corners: () => [{x: l, y: t}, {x: l + w, y: t}, {x: l + w, y: t + h}, {x: l, y: t + h}],
toString: () => `Box (left: ${l}, top: ${t}, height: ${h}, width: ${w})`
})
但是现在您想将您的工具应用到一个新的环境中,您的小部件看起来像这样:
const widget = {
topLeft: {x: 126, y: 202},
bottomRight: {x: 776, y: 682},
borderColor: 'red',
borderWidth: 3,
backgroundUrl: 'http://example.com/img.png',
// ...
}
虽然 topRight
和 bottomLeft
点是描述矩形的兼容方式,但您将不得不重写一堆已经处理框的代码来处理这些新的小部件。此外,盒子似乎是这种情况的逻辑视图。高度和宽度似乎比右下角更重要。这里我们可以用镜头来处理顾虑。也就是说,我们可以完全在盒子里思考,从widget中提取一个盒子,通过调整盒子来调整值。我们只需要写一个镜头就可以了:
const boxLens = lens (
({topLeft: {x: x1, y: y1}, bottomRight: {x: x2, y: y2}}) =>
box (x1, y1, x2 - x1, y2 - y1),
({position: {x, y}, width, height}, widget) => ({
...widget,
topLeft: {x, y},
bottomRight: {x: x + width, y: y + height}
})
)
现在我们可以像用框描述一样处理小部件的位置和范围:
view (boxLens, widget) .toString ()
//=> "Box (left: 126, top: 202, height: 480, width: 650)"
view (boxLens, widget) .corners ()
//=> [{x: 126, y: 202}, {x: 776, y: 202}, {x: 776, y: 682}, {x: 126, y: 682}]
set (boxLens, box (200, 150, 1600, 900), widget)
//=> {topLeft: {x: 200, y: 150}, bottomRight: {x: 1800, y: 1050}, borderColor: "red", ...}
over (boxLens, box => box .scale (.5, .5), widget)
//=> {topLeft: {x: 126, y: 202}, bottomRight: {x: 451, y: 442}, borderColor: "red", ...}
const moveWidget = (dx, dy) =>
over(boxLens, box => box .move (dx, dy))
moveWidget (10, 50) (widget)
//=> {topLeft: {x: 136, y: 252}, bottomRight: {x: 786, y: 732}, borderColor: "red", ...}
您可以在以下代码段中确认这一点:
const {lens, view, set, over} = R
const box = (l, t, w, h) => ({
move: (dx, dy) => box (l += dx, t += dy, w, h),
scale: (dw, dh) => box (l, t, w *= dw, h *= dh),
area: () => w * h,
get position () { return {x: l, y: t} },
get width () { return w},
get height () { return h },
corners: () => [{x: l, y: t}, {x: l + w, y: t}, {x: l + w, y: t + h}, {x: l, y: t + h}],
toString: () => `Box (left: ${l}, top: ${t}, height: ${h}, width: ${w})`
})
const boxLens = lens(
({topLeft: {x: x1, y: y1}, bottomRight: {x: x2, y: y2}}) => box (x1, y1, x2 - x1, y2 - y1),
({position: {x, y}, width, height}, widget) => ({
...widget,
topLeft: {x, y},
bottomRight: {x: x + width, y: y + height}
})
)
const widget = {
topLeft: {x: 126, y: 202},
bottomRight: {x: 776, y: 682},
borderColor: 'red',
borderWidth: 3,
backgroundUrl: 'http://example.com/img.png',
// ...
}
console .log (
view (boxLens, widget) .toString ()
)
console .log (
view (boxLens, widget) .corners ()
)
console .log (
set (boxLens, box (200, 150, 1600, 900), widget)
)
console .log (
over (boxLens, box => box .scale (.5, .5), widget)
)
const moveWidget = (dx, dy) =>
over(boxLens, box => box .move (dx, dy))
console .log (
moveWidget (10, 50) (widget)
)
.as-console-wrapper {max-height: 100% !important; top: 0}
<script src="//cdnjs.cloudflare.com/ajax/libs/ramda/0.27.1/ramda.min.js"></script>
重点
这表明我们可以使用镜头一次处理多个字段,正如 Mrk Sef 的自我回答也解释的那样。但是我们必须以某种与原始同构的方式处理它们。这实际上是镜头的一个非常强大的用途。但这并不意味着我们可以简单地使用它们来处理任意属性。
到目前为止我学到了什么
这可能不是个好主意。问题是镜片需要具有某些特性才能工作。这些属性之一是:
view(lens, set(lens, a, store)) ≡ a — If you set a value into the store, and immediately view the value through the lens, you get the value that was set.
如果您希望镜头在没有进一步限制的情况下指向多个值,则该信息必须(以某种方式)在被更改的数据结构中进行编码。如果一个键被设置为一个数组,该数组编码它自己的大小。但是,如果设置数组实际上对应于设置 something else
,那么 something else
的某些子集必须与数组同构(增长、收缩、重新排序,整个 shebang)。这样就可以随时来回转换了。
如果您对进一步的限制感到满意,您可以做更多的事情,但结果并不理想。
这是一个功能齐全(据我所知)的镜头实现,它指向多个属性但限制了允许设置的属性。
const subsetOf = pipe(without, length, equals(0));
const subset = flip(subsetOf);
const lensProps = propNames => lens(
pick(propNames),
(update, data) =>
subset(keys(update), propNames) ?
({ ...data, ...update }) :
call(() => {throw new Error("OH NO! LENS LAW BROKEN!");})
);
正在使用:
const annualLens = lensProps(["yearsPlayed", "age"]);
const timmy = {
jersey: 148,
age: 10,
yearsPlayed: 2,
id: 3.14159
};
console.log(
"View Timmy's Annual Props: ",
view(annualLens, timmy)
);
console.log(
"Set Timmy's Annual Props: ",
set(annualLens, {yearsPlayed: 100, age: 108}, timmy)
);
console.log(
"Update Timmy's Annual Props: ",
over(annualLens, map(inc), timmy)
);
// Break the LAW
set(annualLens, {newKey: "HelloWorld"}, timmy);
输出:
View Timmy's Annual Props: { age: 10, yearsPlayed: 2 }
Set Timmy's Annual Props: { jersey: 148, age: 108, yearsPlayed: 100, id: 3.14159 }
Update Timmy's Annual Props: { jersey: 148, age: 11, yearsPlayed: 3, id: 3.14159 }
Error: OH NO! LENS LAW BROKEN!
您可以想象编写一个采用路径而不是名称的版本,但是从那以后为了使用 set
,这实际上并没有帮助,您需要知道镜头的路径期待 set
.
但情况变得更糟。你可以组成这些类型的镜头,但实际上没有任何意义:
compose(lensIndex(0), lensProps(["a","b"]), lensProp("b"))
与
相同compose(lensIndex(0), lensProp("b"))
所以虽然它没有破坏任何东西,但它很快就会变得非常无趣。实际上,它的唯一用途是作为构图中的 'outermost' 镜头。即使那样,它也可能必须受到限制才能有用。
好的一面是,作为最外面的镜头,它实际上可以在没有中间物体的情况下改变多个值。但这不是很好,因为您可以使用 evolve
作为传递给 over
的函数,并且在不丢失任何东西的情况下加入额外的功能。