【问题标题】:How to implement array joins in functional way?如何以功能方式实现数组连接?
【发布时间】:2018-02-03 22:29:57
【问题描述】:

我有一个用条件分隔符连接对象数组的函数。

function getSegmentsLabel(segments) {
    var separator = '-';

    var segmentsLabel = '';
    var nextSeparator = '';
    _.forEach(segments, function(segment) {
        segmentsLabel += nextSeparator + segment.label;
        nextSeparator = segment.separatorUsed ? separator : ' ';
    });
    return segmentsLabel;
}

用法:

var segments = [
    {label: 'First', separatorUsed: true},
    {label: 'Second', separatorUsed: false},
    {label: 'Third', separatorUsed: true},
    {label: 'Forth', separatorUsed: true}
];

getSegmentsLabel(segments); // Result: "First-Second Third-Forth"

上面的getSegmentsLabel 函数如何在不改变变量的情况下以纯函数方式编写?我们可以使用 lodash 函数。

【问题讨论】:

    标签: javascript functional-programming lodash


    【解决方案1】:

    递归

    或者代替 map/reduce/join,你可以使用直接递归——这里的好处是我们不会多次遍历集合来计算结果——哦,程序真的很小,所以很容易消化

    小心 javascript 中的堆栈溢出;相关:How do I replace while loops with a functional programming alternative without tail call optimization?

    var segments = [
      {label: 'First', separatorUsed: true},
      {label: 'Second', separatorUsed: false},
      {label: 'Third', separatorUsed: true},
      {label: 'Forth', separatorUsed: true}
    ];
    
    const main = ([x,...xs]) =>
      x === undefined
        ? ''
        : xs.length === 0
          ? x.label
          : x.label + (x.separatorUsed ? '-' : ' ') + main (xs)
          
    console.log (main (segments))
    // First-Second Third-Forth

    函数式编程

    我们函数的最后一个实现非常具体 - 函数式编程不仅仅是使用 map 和 reduce,它是关于制作 有意义 抽象和编写 generic 过程易于重复使用

    这个示例有意与您的原始代码大不相同,希望它能让您以不同的方式思考程序 - 如果您对这些内容感兴趣,作为本文的后续内容,您可以开始阅读有关 @ 987654322@.

    通过以这种方式编写程序,我们在 generic 文本模块中表达了“带有条件分隔符的可连接文本片段”的想法,该模块可用于任何其他程序 - 编写者可以创建使用Text.make 的文本单元并使用Text.concat 组合它们

    这个程序的另一个优点是分隔符是参数控制的

    // type Text :: { text :: String, separator :: String }
    const Text =
      {
        // Text.make :: (String × String?) -> Text
        make: (text, separator = '') =>
          ({ type: 'text', text, separator }),
          
        // Text.empty :: Text
        empty: () =>
          Text.make (''),
          
        // Text.isEmpty :: Text -> Boolean
        isEmpty: l =>
          l.text === '',
          
        // Text.concat :: (Text × Text) -> Text
        concat: (x,y) =>
          Text.isEmpty (y)
            ? x
            : Text.make (x.text + x.separator + y.text, y.separator),
        
        // Text.concatAll :: [Text] -> Text
        concatAll: ts =>
          ts.reduce (Text.concat, Text.empty ())  
      }
    
    // main :: [Text] -> String
    const main = xs =>
      Text.concatAll (xs) .text
      
    // data :: [Text]
    const data =
      [ Text.make ('First', '-'), Text.make ('Second', ' '), Text.make ('Third', '-'), Text.make ('Fourth', '-') ]
      
    console.log (main (data))
    // First-Second Third-Fourth

    【讨论】:

    • 太棒了。我很高兴你提出了递归。当一个简单的递归函数最易读时,人们常常过于担心使用正确的抽象,如 mapreduce
    • 如果你在Text monoid 中添加一些 cmets 来解释所有函数如何相互关联以及为什么你按照你的方式实现它们,那就太好了。签名肯定是有帮助的,但一两句话描述每个函数的作用更有用。
    • @AaditMShah 感谢您的评论; Text.make 是一个构造函数,用于从输入字符串和可选分隔符中生成新的 Text - Text.empty 构造空 Text,Text.isEmpty 检查输入 Text 是否等效于空文本,Text.concat 连接两个文本在一起,Text.concatAll 将可变长度的 JavaScript 文本数组连接在一起
    【解决方案2】:

    您可以使用map() 方法返回新数组,然后使用join() 从该数组中获取字符串。

    var segments = [
        {label: 'First', separatorUsed: true},
        {label: 'Second', separatorUsed: false},
        {label: 'Third', separatorUsed: true},
        {label: 'Forth', separatorUsed: true}
    ];
    
    function getSegmentsLabel(segments) {
      return segments.map(function(e, i) {
        return e.label + (i != segments.length - 1 ? (e.separatorUsed ? '-' : ' ') : '')
      }).join('')
    }
    
    console.log(getSegmentsLabel(segments));

    【讨论】:

    • 这将在末尾添加空间。
    • @TheKojuEffect 已更新。
    【解决方案3】:

    您可以使用数组作为分隔符,并决定是否在末尾使用分隔符、破折号或不使用分隔符。

    const separators = [' ', '', '-'];
    var getSegmentsLabel = array => array
            .map(({ label, separatorUsed }, i, a) =>
                label + separators[2 * separatorUsed - (i + 1 === a.length)])
            .join('');
    
    var segments = [{ label: 'First', separatorUsed: true }, { label: 'Second', separatorUsed: false }, { label: 'Third', separatorUsed: true }, { label: 'Forth', separatorUsed: true }];
    
    console.log(getSegmentsLabel(segments));

    【讨论】:

    • 巧妙地使用静态数组和索引——也许在 getSegmentsLabel 之外初始化它,所以每次迭代都不会创建一次?
    【解决方案4】:

    这里我把功能分开了:

    // buildSeparatedStr returns a function that can be used
    // in the reducer, employing a template literal as the returned value
    const buildSeparatedStr = (sep) => (p, c, i, a) => {
      const separator = !c.separatorUsed || i === a.length - 1 ? ' ' : sep;
      return `${p}${c.label}${separator}`;
    }
    
    // Accept an array and the buildSeparatedStr function
    const getSegmentsLabel = (arr, fn) => arr.reduce(fn, '');
    
    // Pass in the array, and the buildSeparatedStr function with
    // the separator
    const str = getSegmentsLabel(segments, buildSeparatedStr('-'));
    

    DEMO

    【讨论】:

      【解决方案5】:

      在这种情况下最好使用reduceRight 而不是map

      const segments = [
          {label: 'First',  separatorUsed: true},
          {label: 'Second', separatorUsed: false},
          {label: 'Third',  separatorUsed: true},
          {label: 'Forth',  separatorUsed: true}
      ];
      
      const getSegmentsLabel = segments =>
          segments.slice(0, -1).reduceRight((segmentsLabel, {label, separatorUsed}) =>
              label + (separatorUsed ? "-" : " ") + segmentsLabel,
          segments[segments.length - 1].label);
      
      console.log(JSON.stringify(getSegmentsLabel(segments)));

      如您所见,最好从右到左遍历数组。


      这是一个更高效的程序版本,虽然它使用了变异:

      const segments = [
          {label: 'First',  separatorUsed: true},
          {label: 'Second', separatorUsed: false},
          {label: 'Third',  separatorUsed: true},
          {label: 'Forth',  separatorUsed: true}
      ];
      
      const reduceRight = (xs, step, base) => {
          const x = xs.pop(), result = xs.reduceRight(step, base(x));
          return xs.push(x), result;
      };
      
      const getSegmentsLabel = segments =>
          reduceRight(segments, (segmentsLabel, {label, separatorUsed}) =>
              label + (separatorUsed ? "-" : " ") + segmentsLabel,
          ({label}) => label);
      
      console.log(JSON.stringify(getSegmentsLabel(segments)));

      它不是纯函数式的,但如果我们将 reduceRight 视为一个黑盒子,那么您可以以纯函数式的方式定义 getSegmentsLabel

      【讨论】:

        【解决方案6】:

        const segments = [
            {label: 'First',  separatorUsed: true},
            {label: 'Second', separatorUsed: false},
            {label: 'Third',  separatorUsed: true},
            {label: 'Forth',  separatorUsed: true}
        ];
        
        const segmentsLabel = segments.reduce((label, segment, i, arr) => {
            const separator = (i === arr.length - 1) ? '' : (segment.separatorUsed) ? '-' : ' ';
            return label + segment.label + separator;
         }, '');
        
         console.log(segmentsLabel);

        【讨论】:

          猜你喜欢
          • 1970-01-01
          • 2011-10-20
          • 2016-02-22
          • 2019-03-29
          • 1970-01-01
          • 2015-07-30
          • 2010-12-18
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多