// Common Utilities

function compose(...fns) {
  const apply = (arg, fn) => fn(arg);
  return (initial) => fns.reduceRight(apply, initial);
}

function curry(arity, fn, ...rest) {
  if(arity <= rest.length) {
    return fn(...rest);
  }

  return curry.bind(null, arity, fn, ...rest);
}

// Transducers stuff

const Combine = {
  push: (state, value) => (state.push(value), state),
  add: (state, value) => state.add(value),
  concat: (state, value) => state.concat(value)
};

function reduce(reducer, initial, collection) {
  let state = initial;

  for(let value of collection) {
    state = reducer(state, value);
  }

  return state;
}

function transduce(combine, initial, transducer, collection) {
  return reduce(transducer(combine), initial, collection);
}

const Into = {
  array: curry(2, function(transducer, collection) {
    return transduce(Combine.push, [], transducer, collection);
  }),
  string: curry(2, function(transducer, collection) {
    return transduce(Combine.concat, "", transducer, collection)
  }),
  set: curry(2, function(transducer, collection) {
    return transduce(Combine.add, new Set(), transducer, collection);
  }),
};

function filter(predicate, next) {
  if(arguments.length === 1) {
    return (_next) => filter(predicate, _next);
  }

  return function reducer(state, value) {
    if(predicate(value)) {
      return next(state, value);
    }

    return state;
  };
}

function map(transform, next) {
  if(arguments.length === 1) {
    return (_next) => map(transform, _next);
  }

  return function reducer(state, value) {
    return next(state, transform(value));
  };
}

// Contrived example

const is_even = number => number % 2 === 0;
const is_positive = number => number > 0;
const add_message = number => `The number is: ${number}`;

const transducer = compose(
  filter(is_positive),
  filter(is_even),
  map(add_message)
);

const array = [-2, -1, 0, 1, 2, 3];
const set = new Set([-2, -1, 0, 1, 2, 3]);

const to_string = Into.string(transducer);

console.log(
  '\nTransducer with Array.reduce\n',
  array.reduce(transducer(Combine.push), []),
  '\n'
);

console.log(
  'Transducer with custom reduce (on Array)\n',
  Into.array(transducer, array),
  '\n'
);

console.log(
  'Transducer with custom reduce (on Set)\n',
  Into.set(transducer, set),
  '\n'
);

console.log(
  'Transducer with custom reduce (from Array to String)\n',
  Into.string(transducer, array),
  '\n'
);

console.log(
  'Transducer with custom reduce (from Set to String)\n',
  to_string(set),
);