函数式编程:条件列表 branching/filtering (Javascript)

Functional programming: list conditional branching/filtering (Javascript)

我有命令式编程背景 (Java),并开始尝试更好地理解 FP 概念。特别是条件 branching/filtering 以及它如何应用于 streams/lists 数据。

这是一个愚蠢的人为示例...我们有一个玩家列表,并希望根据他们的技能水平将他们分成不同的列表。基本的命令式方法可能类似于:

const excluded = []; // LOW skilled
const reserves = []; // only MEDIUM/HIGH skilled
const team = []; // only HIGH skilled

const allPlayers = [
    {
        name: 'personh1',
        skillLevel: 'HIGH'
    },
    {
        name: 'personh2',
        skillLevel: 'HIGH'
    },
    {
        name: 'personh3',
        skillLevel: 'HIGH'
    },
    {
        name: 'personm1',
        skillLevel: 'MEDIUM'
    },
    {
        name: 'personm2',
        skillLevel: 'MEDIUM'
    },
    {
        name: 'personl1',
        skillLevel: 'LOW'
    },
    {
        name: 'personl2',
        skillLevel: 'LOW'
    }
];

const maxTeamSize = 2;
const maxReservesSize = 2;

allPlayers.forEach(p => {
    if (p.skillLevel === 'HIGH') {
        if (team.length < maxTeamSize) {
            team.push(p);
        } else {
            reserves.push(p);
        }
    } else if (p.skillLevel === 'MEDIUM') {
        if (reserves.length < maxReservesSize) {
            reserves.push(p);
        } else {
            excluded.push(p);
        }
    } else {
        excluded.push(p);
    }
});

// functions defined elsewhere...
notifyOfInclusion(team.concat(reserves));
notifyOfExclusion(excluded);

我可以以更实用的方式完成此操作:(使用 JS 和 Ramda 库):

team = R.slice(0, maxTeamSize, R.filter(p => p.skillLevel === 'HIGH', allPlayers));
reserves = R.slice(0, maxReservesSize, R.filter(p => (p.skillLevel === 'HIGH' || p.skillLevel === 'MEDIUM') && !R.contains(p, team), allPlayers));
excluded = R.filter(p => !R.contains(p, team) && !R.contains(p, reserves), allPlayers);

notifyOfInclusion(team.concat(reserves));
notifyOfExclusion(excluded);

但它看起来很粗糙,重复而且不是很明确。从功能性 POV 实现类似目标的更好(更多 elegant/declarative)方法是什么?在任何答案中使用 Ramda 都是一种奖励,但不是必需的。谢谢。

虽然这个答案并不是真正有效的,但我会把一些逻辑移到某个变量,这些变量代表一些依赖关系,以便在必要时使用更多具有统一访问和决策机制的组,实际项目必须进入哪个组.

毕竟逻辑很简单,就是以skillLevel为起始层,迭代到下一层,直到找到一个长度小于该组给定最大值的组。然后将项目推送到该组。

const
    excluded = [], // LOW skilled
    reserves = [], // only MEDIUM/HIGH skilled
    team = [],     // only HIGH skilled
    allPlayers = [{ name: 'personh1', skillLevel: 'HIGH' }, { name: 'personh2', skillLevel: 'HIGH' }, { name: 'personh3', skillLevel: 'HIGH' }, { name: 'personm1', skillLevel: 'MEDIUM' }, { name: 'personm2', skillLevel: 'MEDIUM' }, { name: 'personl1', skillLevel: 'LOW' }, { name: 'personl2', skillLevel: 'LOW' }],
    maxTeamSize = 2,
    maxReservesSize = 2,
    temp = { HIGH: team, MEDIUM: reserves, LOW: excluded },
    lowerLevel = { HIGH: 'MEDIUM', MEDIUM: 'LOW' },
    max = { HIGH: maxTeamSize, MEDIUM: maxReservesSize, LOW: Infinity };

allPlayers.forEach(p => {
    var level = p.skillLevel;
    while (temp[level].length >= max[level]) {
        level = lowerLevel[level];
    }
    temp[level].push(p);
});

console.log(team);
console.log(reserves);
console.log(excluded);
.as-console-wrapper { max-height: 100% !important; top: 0; }

您正在寻找 R.groupBy:

const allPlayers = [
    { name: 'personh1', skillLevel: 'HIGH' },
    { name: 'personh2', skillLevel: 'HIGH' },
    { name: 'personh3', skillLevel: 'HIGH' },
    { name: 'personm1', skillLevel: 'MEDIUM' },
    { name: 'personm2', skillLevel: 'MEDIUM' },
    { name: 'personl1', skillLevel: 'LOW' },
    { name: 'personl2', skillLevel: 'LOW'  }
];

const skillLevel = R.prop('skillLevel');

console.log(R.groupBy(skillLevel, allPlayers));
<script src="//cdn.jsdelivr.net/npm/ramda@0.25/dist/ramda.min.js"></script>

我的版本比你的更明确一点,但只是一点点:

const skills = groupBy(prop('skillLevel'), allPlayers)
const ordered = flatten([skills['HIGH'], skills['MEDIUM'], skills['LOW']])
const team = filter(propEq('skillLevel', 'HIGH'), take(maxTeamSize, ordered))
const reserves = reject(propEq('skillLevel', 'LOW'), 
                        take(maxReservesSize, drop(length(team), ordered)))
const excluded = drop(length(team) + length(reserves), ordered)

这个假设你只希望团队中有高技能的球员,即使他们没有足够的人来填补空缺。如果您想在这种情况下包括中等技能的玩家,那么您可以将 filter(propEq('skillLevel', 'HIGH') 替换为 reject(propEq('skillLevel', 'LOW')。如果您想要填充到 max[Team/Reserves]Size,即使它们与技能水平不匹配,您也可以删除 filter/reject 调用。 (这也会使代码看起来更简洁。)

我最初尝试为所有这些函数使用一个函数是非常糟糕的,而且它甚至不能像这些函数那样工作:

chain(
  selected => allPlayers => assoc(
    'excluded', 
    difference(allPlayers, concat(selected.team, selected.reserves)), 
    selected
  ),
  pipe(
    groupBy(prop('skillLevel')),
    lift(concat)(prop('HIGH'), prop('MEDIUM')),
    eligible => ({
      team: take(maxTeamSize, eligible),
      reserves: take(maxReservesSize, drop(maxTeamSize, eligible))
    }),
  )
)(allPlayers)

当然,您也可以在排序列表中的单个 reduce 中执行此操作,这可能仍然是您解决实际问题的最佳选择,但我怀疑规则和长度之间的相互作用各种结果列表中的一个也将允许在此处编写非常漂亮的代码。

所有这些都可以在 Ramda REPL.

上找到