单人军队
一个简单的递归程序,在一个函数中处理所有事情。这里有一个明显的混合问题,这会损害函数的整体可读性。我们将在下面看到针对此问题的一种此类补救措施
const main = ([x, ...xs], [y, ...ys]) =>
x === undefined || y === undefined
? []
: [ { itemLabel: x.name, itemValue: x.value - y.value } ] .concat (main (xs, ys))
const arr1 =
[ { name: 1, value: 10 }, { name: 2, value: 15 } ]
const arr2 =
[ { name: 3, value: 5 }, { name: 4, value: 3 } ]
console.log (main (arr1, arr2))
// [ { itemLabel: 1, itemValue: 5 },
// { itemLabel: 2, itemValue: 12 } ]
思考类型
这部分答案受到 Monoid 类别的类型理论的影响——我不会深入探讨它,因为我认为代码应该能够自我展示。
所以我们的问题中有两种类型:我们称它们为 Foo 和 Bar
-
Foo - 有 name 和 value 字段
-
Bar - 有 itemLabel 和 itemValue 字段
我们可以随心所欲地表示我们的“类型”,但我选择了一个构造对象的简单函数
const Foo = (name, value) =>
({ name
, value
})
const Bar = (itemLabel, itemValue) =>
({ itemLabel
, itemValue
})
创建类型的值
要构造我们类型的新值,我们只需将函数应用于字段值
const arr1 =
[ Foo (1, 10), Foo (2, 15) ]
const arr2 =
[ Foo (3, 5), Foo (4, 3) ]
让我们看看到目前为止的数据
console.log (arr1)
// [ { name: 1, value: 10 },
// { name: 2, value: 15 } ]
console.log (arr2)
// [ { name: 3, value: 5 },
// { name: 4, value: 3 } ]
一些高层规划
我们有了一个良好的开端。我们有两个 Foo 值数组。我们的目标是通过从每个数组中获取一个 Foo 值,将它们组合起来(稍后将详细介绍),然后移动到下一对
,来处理这两个数组
const zip = ([ x, ...xs ], [ y, ...ys ]) =>
x === undefined || y === undefined
? []
: [ [ x, y ] ] .concat (zip (xs, ys))
console.log (zip (arr1, arr2))
// [ [ { name: 1, value: 10 },
// { name: 3, value: 5 } ],
// [ { name: 2, value: 15 },
// { name: 4, value: 3 } ] ]
组合值:concat
通过将 Foo 值正确组合在一起,我们现在可以更专注于组合过程是什么。这里,我要定义一个泛型的concat,然后在我们的Foo类型上实现它
// generic concat
const concat = (m1, m2) =>
m1.concat (m2)
const Foo = (name, value) =>
({ name
, value
, concat: ({name:_, value:value2}) =>
// keep the name from the first, subtract value2 from value
Foo (name, value - value2)
})
console.log (concat (Foo (1, 10), Foo (3, 5)))
// { name: 1, value: 5, concat: [Function] }
concat 听起来很熟悉吗? Array 和 String 也是 Monoid 类型!
concat ([ 1, 2 ], [ 3, 4 ])
// [ 1, 2, 3, 4 ]
concat ('foo', 'bar')
// 'foobar'
高阶函数
所以现在我们有一种方法可以将两个 Foo 值组合在一起。保留第一个 Foo 的 name,并减去 value 属性。现在我们将其应用于“压缩”结果中的每一对。函数式程序员喜欢高阶函数,所以你会欣赏这种高阶和谐
const apply = f => xs =>
f (...xs)
zip (arr1, arr2) .map (apply (concat))
// [ { name: 1, value: 5, concat: [Function] },
// { name: 2, value: 12, concat: [Function] } ]
转换类型
所以现在我们有了正确的 name 和 value 值的 Foo 值,但我们希望我们的最终答案是 Bar 值。我们只需要一个专门的构造函数
Bar.fromFoo = ({ name, value }) =>
Bar (name, value)
Bar.fromFoo (Foo (1,2))
// { itemLabel: 1, itemValue: 2 }
zip (arr1, arr2)
.map (apply (concat))
.map (Bar.fromFoo)
// [ { itemLabel: 1, itemValue: 5 },
// { itemLabel: 2, itemValue: 12 } ]
努力有回报
美丽、纯粹的功能性表达。我们的程序读起来非常好;由于采用了声明式风格,数据的流动和转换很容易遵循。
// main :: ([Foo], [Foo]) -> [Bar]
const main = (xs, ys) =>
zip (xs, ys)
.map (apply (concat))
.map (Bar.fromFoo)
当然还有完整的代码演示
const Foo = (name, value) =>
({ name
, value
, concat: ({name:_, value:value2}) =>
Foo (name, value - value2)
})
const Bar = (itemLabel, itemValue) =>
({ itemLabel
, itemValue
})
Bar.fromFoo = ({ name, value }) =>
Bar (name, value)
const concat = (m1, m2) =>
m1.concat (m2)
const apply = f => xs =>
f (...xs)
const zip = ([ x, ...xs ], [ y, ...ys ]) =>
x === undefined || y === undefined
? []
: [ [ x, y ] ] .concat (zip (xs, ys))
const main = (xs, ys) =>
zip (xs, ys)
.map (apply (concat))
.map (Bar.fromFoo)
const arr1 =
[ Foo (1, 10), Foo (2, 15) ]
const arr2 =
[ Foo (3, 5), Foo (4, 3) ]
console.log (main (arr1, arr2))
// [ { itemLabel: 1, itemValue: 5 },
// { itemLabel: 2, itemValue: 12 } ]
备注
我们上面的程序是用.map-.map 链实现的,这意味着多次处理和创建中间值。我们还在对zip 的调用中创建了一个[[x1,y1], [x2,y2], ...] 的中间数组。范畴论为我们提供了诸如等式推理之类的东西,因此我们可以用m.map(compose(f,g)) 替换m.map(f).map(g) 并获得相同的结果。所以还有改进的空间,但我认为这足以让你咬牙切齿并开始以不同的方式思考问题。