Functional programming 如何重写此命令式代码以使其更具功能性?
我发现这解释了如何为游戏编写一个随机加权的投球系统。我更喜欢用一种功能性更强的编程风格来编写这段代码,但我想不出一种方法来完成这段代码。我将在此处内联伪代码:Functional programming 如何重写此命令式代码以使其更具功能性?,functional-programming,coffeescript,underscore.js,reduce,Functional Programming,Coffeescript,Underscore.js,Reduce,我发现这解释了如何为游戏编写一个随机加权的投球系统。我更喜欢用一种功能性更强的编程风格来编写这段代码,但我想不出一种方法来完成这段代码。我将在此处内联伪代码: R = (some random int); T = 0; for o in os T = T + o.weight; if T > R return o; 这篇文章怎么能写得更实用呢?我使用的是CoffeeScript和Underline.js,但我更希望这个答案与语言无关,因为我很难用
R = (some random int);
T = 0;
for o in os
T = T + o.weight;
if T > R
return o;
这篇文章怎么能写得更实用呢?我使用的是CoffeeScript和Underline.js,但我更希望这个答案与语言无关,因为我很难用函数的方式来思考这个问题 这里还有两个Clojure和JavaScript的函数版本,但是这里的思想应该适用于任何支持闭包的语言。基本上,我们使用递归而不是迭代来完成同样的事情,而不是在中间中断,我们只返回一个值,停止递归。 原始伪代码:
R = (some random int);
T = 0;
for o in os
T = T + o.weight;
if T > R
return o;
Clojure版本(对象仅被视为Clojure映射):
JavaScript版本(为方便起见,使用下划线)。
小心,因为这可能会炸掉烟囱。
这在概念上与clojure版本相同
var js_recursive_version = function(objects, r) {
var main_helper = function(t, others) {
var obj = _.first(others);
var new_t = t + obj.weight;
if (new_t > r) {
return obj;
} else {
return main_helper(new_t, _.rest(others));
}
};
return main_helper(0, objects);
};
我认为基本任务是从数组
os
中随机选择“事件”(对象),频率由它们各自的权重定义。该方法是将随机数(均匀分布)映射(即搜索)到阶梯累积概率分布函数上
使用正权重时,它们的累积和从0增加到1。您给我们的代码只是从0结尾开始搜索。要使重复调用的速度最大化,请预先计算总和,并对事件进行排序,以使最大权重排在第一位
无论您是使用迭代(循环)还是递归进行搜索,这都无关紧要。递归在一种试图“纯函数化”但无助于理解潜在数学问题的语言中很好。它也不能帮助你将任务打包成一个干净的函数。下划线
函数是打包迭代的另一种方式,但不会更改基本功能。只有any
和all
在找到目标时提前退出
对于小型os
array,这种简单的搜索就足够了。但是对于大数组,二进制搜索会更快。查看下划线
我发现sortedIndex
使用此策略。从Lo-Dash
(一个下划线
下拉菜单),“使用二进制搜索确定值应插入数组的最小索引,以保持排序数组的排序顺序”
sortedIndex
的基本用途是:
os = [{name:'one',weight:.7},
{name:'two',weight:.25},
{name:'three',weight:.05}]
t=0; cumweights = (t+=o.weight for o in os)
i = _.sortedIndex(cumweights, R)
os[i]
可以使用嵌套函数隐藏累积和计算,如:
osEventGen = (os)->
t=0; xw = (t+=y.weight for y in os)
return (R) ->
i = __.sortedIndex(xw, R)
return os[i]
osEvent = osEventGen(os)
osEvent(.3)
# { name: 'one', weight: 0.7 }
osEvent(.8)
# { name: 'two', weight: 0.25 }
osEvent(.99)
# { name: 'three', weight: 0.05 }
在coffeescript中,Jed Clinger的递归搜索可以这样编写:
foo = (x, r, t=0)->
[y, x...] = x
t += y
return [y, t] if x.length==0 or t>r
return foo(x, r, t)
使用相同基本思想的循环版本是:
foo=(x,r)->
t=0
while x.length and t<=r
[y,x...]=x # the [first, rest] split
t+=y
y
foo=(x,r)->
t=0
当x.length和t时,您可以通过折叠(aka或下划线的\uu.reduce
)来实现这一点:
SSCCE:
items = [
{item: 'foo', weight: 50}
{item: 'bar', weight: 35}
{item: 'baz', weight: 15}
]
r = Math.random() * 100
{item} = items.reduce (memo, {item, weight}) ->
if memo.sum > r
memo
else
{item, sum: memo.sum + weight}
, {sum: 0}
console.log 'r:', r, 'item:', item
您可以多次运行它,并查看结果是否有意义:)
这就是说,我发现折叠有点做作,因为您必须记住所选项目和迭代之间的累积重量,并且在找到项目时不会短路
也许可以考虑在纯FP和重新实现查找算法的繁琐之间找到折衷方案(使用):
我发现(没有双关语的意思)这个算法比第一个算法更容易理解(而且它的性能应该更好,因为它不会创建中间对象,而且会短路)
更新/旁注:第二个算法不是“纯”的,因为传递给.find
的函数不是(它有修改外部总计
变量的副作用),但整个算法是透明的。如果要将其封装在findItem=(items,r)->
函数中,该函数将是纯函数,并且将始终为相同的输入返回相同的输出。这是一件非常重要的事情,因为这意味着您可以在幕后使用一些非FP结构(出于性能、可读性或任何原因)的同时获得FP的好处:D能够在功能上做到这一点很好,但除非我遗漏了一些显而易见的东西,否则任何功能性解决方案(在CoffeeScript中实现)结果将不如命令式版本清晰。如果这是您反复使用的东西,使用相同的os
,但不同的R
,则可能需要预先计算累积总和,或者构建os
,以优化搜索。
items = [
{item: 'foo', weight: 50}
{item: 'bar', weight: 35}
{item: 'baz', weight: 15}
]
r = Math.random() * 100
{item} = items.reduce (memo, {item, weight}) ->
if memo.sum > r
memo
else
{item, sum: memo.sum + weight}
, {sum: 0}
console.log 'r:', r, 'item:', item
total = 0
{item} = _.find items, ({weight}) ->
total += weight
total > r