简体   繁体   中英

How to implement array joins in functional way?

I have a function that joins an array of objects with a conditional separator.

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

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

Usages:

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"

How can the above getSegmentsLabel function be written in a purely functional way without mutating variables? We can use lodash functions.

recursion

or instead of map/reduce/join, you can use direct recursion – the benefit here is that we don't iterate thru the collection multiple times to compute the result – oh and the program is really small so it's easy to digest

be careful of stack overflows in javascript tho; relevant: 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 


functional programming

that last implementation of our function is awfully specific - functional programming isn't just about using map and reduce, it's about making meaningful abstractions and writing generic procedures that can easily be reused

this example is intentionally very different from your original code with the hope that it will get you to think about programs in a different way – if this stuff interests you, as a follow up to this post, you could start reading about monoids .

by writing our program this way, we've represented this idea of "joinable pieces of text with conditional separators" in a generic text module that could be used in any other program – writers can create units of text using Text.make and combine them using Text.concat

another advantage in this program is the separator is parameter-controlled

 // 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 

You can use map() method that will return new array and then join() to get string form that array.

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

You could use an array for the separators and decide, if a spacer, a dash or no separator for strings at the end.

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

Here I separate out the functions:

// 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

It's better to use reduceRight instead of map in this case:

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

As you can see, it's better to iterate through the array from right to left.


Here's a more efficient version of the program, although it uses mutation:

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

It's not purely functional but if we treat reduceRight as a black box then you can define getSegmentsLabel in a purely functional way.

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

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM