如何重写这些命令式代码以使其更具功能性?

时间:2013-08-09 17:27:33

标签: functional-programming coffeescript underscore.js reduce

我发现an answer on SO解释了如何为游戏编写随机加权掉落系统。我会更喜欢以更多功能编程风格编写这段代码,但我无法想办法为这段代码做到这一点。我将在这里内联伪代码:

R = (some random int); 
T = 0; 
for o in os 
    T = T + o.weight; 
    if T > R 
        return o;

如何以更实用的风格编写?我正在使用CoffeeScript和underscore.js,但我更喜欢这个答案是语言不可知的,因为我无法以功能的方式思考

3 个答案:

答案 0 :(得分:3)

以下是Clojure和JavaScript中的两个功能版本,但这里的想法应该适用于任何支持闭包的语言。基本上,我们使用递归而不是迭代来完成同样的事情,而不是在中间断开,我们只返回一个值并停止递归。

原始伪代码:

R = (some random int); 
T = 0; 
for o in os 
    T = T + o.weight; 
    if T > R 
        return o;

Clojure版本(对象只被视为clojure贴图):

(defn recursive-version
  [r objects]
  (loop [t 0
         others objects]
    (let [obj (first others)
          new_t (+ t (:weight obj))]
      (if (> new_t r)
        obj
        (recur new_t (rest others))))))

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);
};

答案 1 :(得分:2)

您可以使用折叠(aka Array#reduce或Underscore的_.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

你可以多次at coffeescript.org运行它,看看结果是否有意义:)

话虽这么说,我发现折叠有点人为,因为你必须记住所选项目迭代之间的累积权重,并且当项目为时它不会短路找到。

可以考虑在纯FP和重新实现查找算法的繁琐之间进行折衷解决(使用_.find):

total = 0
{item} = _.find items, ({weight}) ->
  total += weight
  total > r

Runnable example

我发现(没有双关语)这个算法比第一个算法更容易访问(并且它应该表现得更好,因为它不会创建中间对象,并且它会短路)。


更新/侧注:第二个算法不是“纯”,因为传递给_.find的函数不是referentially transparent(它具有修改外部total变量的副作用),但是整个算法引用透明的。如果要将其封装在findItem = (items, r) ->函数中,则该函数将是纯函数,并且将始终为同一输入返回相同的输出。这是一件非常重要的事情,因为这意味着你可以在使用一些非FP构造(性能,可读性或任何原因)的同时获得FP的好处:D

答案 2 :(得分:0)

我认为基础任务是从数组os中随机选择“事件”(对象),其频率由各自的weight定义。该方法是将随机数(具有均匀分布)映射(即搜索)到阶梯累积概率分布函数上。

对于正权重,它们的累积总和从0增加到1.您给我们的代码只是从0结尾开始搜索。要通过重复调用最大化速度,请预先计算总和,并对事件进行排序,以便最大权重为第一。

无论您是使用迭代(循环)还是递归进行搜索,都无关紧要。递归在一种试图“纯功能”的语言中是很好的,但却无助于理解潜在的数学问题。它并没有帮助您将任务打包成一个干净的功能。 underscore函数是打包迭代的另一种方法,但不会更改基本功能。 any只有在找到目标时才会提前退出。

对于小型all数组,此简单搜索就足够了。但是对于大型数组,二进制搜索会更快。查看os我发现underscore使用此策略。从sortedIndexLo-Dash dropin),“使用二进制搜索来确定应将值插入数组的最小索引,以便维护已排序数组的排序顺序”

underscore的基本用法是:

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]

在coffeescript中,Jed Clinger的递归搜索可以这样写:

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 }

使用相同基本思想的循环版本是:

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)

测试jsPerf http://jsperf.com/sortedindex 建议foo=(x,r)-> t=0 while x.length and t<=r [y,x...]=x # the [first, rest] split t+=y y sortedIndex大约为1000时更快,但在长度更接近30时比简单循环慢。