6

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.

TheKojuEffect
  • 20,103
  • 19
  • 89
  • 125

6 Answers6

4

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
Mulan
  • 129,518
  • 31
  • 228
  • 259
  • 1
    Awesome. I'm glad that you brought up recursion. Quite often people worry too much about using the right abstraction like `map` or `reduce` when a simple recursive function would be the most readable. – Aadit M Shah Aug 30 '17 at 04:45
  • It would be nice if you added some comments to your `Text` monoid to explain how all the functions relate to each other and why you implemented them the way you did. Signatures are definitely helpful but a statement or two describing what each function does is even more helpful. – Aadit M Shah Aug 30 '17 at 04:49
  • 1
    @AaditMShah thanks for the remarks; `Text.make` is a constructor to make new a new Text from an input string and optional separator – `Text.empty` constructs the empty Text, `Text.isEmpty` checks if an input Text is equivalent to the empty text, `Text.concat` joins two pieces of Text together, `Text.concatAll` joins a variable-length JavaScript Array of Text pieces together – Mulan Sep 04 '17 at 17:42
2

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));
Nenad Vracar
  • 118,580
  • 15
  • 151
  • 176
1

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));
Nina Scholz
  • 376,160
  • 25
  • 347
  • 392
  • 1
    clever use of the static array and index – maybe initialise it outside of `getSegmentsLabel` so it's not created once per iteration tho ? – Mulan Aug 29 '17 at 19:05
0

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

Andy
  • 61,948
  • 13
  • 68
  • 95
0

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.

Aadit M Shah
  • 72,912
  • 30
  • 168
  • 299
0

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);
TheKojuEffect
  • 20,103
  • 19
  • 89
  • 125