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