0

Task

I need to make an app that reads a CSV file, transforms its data and then outputs it as another CSV file.

The input has the following format:

13:25:37 up 19 days, 21:35,  4 users,  load average: 0.02, 0.02, 0.00
13:25:38 up 19 days, 21:35,  4 users,  load average: 0.02, 0.02, 0.00
... so on

For those of you who are UNIX fans, you will recognise this as the output from the console command uptime.

The output format I want is the following:

 RowNum, Avg Load
 1,0.02
 2,0.02

Where the first column is the row number in the CSV and the second is the number part of load average: 0.02.

All other columns are to be ignored.

Problem

Trying to do this as functionally as I can, I decided to use ramda.

This has been ... a challenge, to say the least. Right now, my code has several structural issues, but I want to focus on the main function, which is not working. Every time I execute my code I get the error:

index.js:54
    .then( () => console.log("Done!") )
     ^

TypeError: main(...).then is not a function

Which is confusing because in both functions I pass to R.ifElse I return a Promise.

Code

const fs = require("fs");
const csvReader = require("csvreader");
const R = require("ramda");
const isString = require("lodash.isstring");
const { promisify } = require("util");
const argv = require("minimist")(process.argv.slice(2));
const appedFileAsync = promisify( fs.appendFile );

const createProcessData = () => {
    const stringifyArray = array => `${array.toString()}\n`;
    const write = str => fs.appendFileSync( argv.outputFile, str );

    const getAvg = R.pipe(
        R.replace("load average:", ""),
        R.trim
    );

    let elapsedTime = 1;
    const transform = list => [ elapsedTime++, getAvg ( R.nth( 3, list ) ) ];

    return R.pipe(
        transform,
        stringifyArray,
        write
    );
};

const printHelp = () => {
    console.log(`
        === MAN HELP ===
        Usage: npm start -- --inputFile input.csv --outputFile output.csv
        --inputFile: location of an input file in CSV format
        --outputFile: location of an output file to append the new information to.
        If this file does not exist, it will be created.
    `);
    return Promise.resolve();
};

const execute = () => appedFileAsync( argv.outputFile, "Time, Avg Load\n" )
    .then( ( ) => csvReader.read( argv.inputFile, createProcessData() ) );

const main = () => {
    const isInvalidFileName = R.anyPass( [ R.isNil, R.isEmpty, R.pipe ( isString, R.not ) ] );
    const hasInvalidArgs = R.either( isInvalidFileName( argv.inputFile ), isInvalidFileName( argv.outputFile ) );

    return R.ifElse(
        hasInvalidArgs,
        printHelp,
        execute
    );
};

main()
    .then( () => console.log("Done!") )
    .catch( console.error );

Question

  • What is wrong with my code ?
Martin Schmelzer
  • 23,283
  • 6
  • 73
  • 98
Flame_Phoenix
  • 16,489
  • 37
  • 131
  • 266
  • According to [the documentation](http://ramdajs.com/docs/#ifElse), `R.ifElse` returns a *function* that will then return one of two things, not the things themselves (promises, in your case). You're treating it as though it returned the things themselves. – T.J. Crowder Dec 13 '17 at 15:38
  • But even if I had `R.ifElse( ... )()` it still fails with the same error.... – Flame_Phoenix Dec 13 '17 at 15:47

1 Answers1

2

This is how to think of ifElse:

const ifElse = (predicate, consequent, alternative) => 
    (...val) => predicate(...val) ? consequent(...val) : alternative(...val);

So

const comp = ifElse(
  (a, b) => a < b,
  (a, b) => `${a} is smaller than ${b}`,
  (a, b) => `${a} is at least as large as ${b}`
)

comp(12, 7) //=> "12 is at least as large as 7"

The main point is that the first argument to ifElse is a function. But you pass it the result of this:

R.either( isInvalidFileName( argv.inputFile ), isInvalidFileName( argv.outputFile ) )

Now normally, either returns a function. But that depends upon you supplying functions to it. The assumption is that if you don't supply functions, you know what you are doing and are supplying container types with ap and map methods, so that either is slightly more generic. But you're supplying booleans such as the result of isInvalidFileName( argv.inputFile ). At that point the behavior is not well defined. Perhaps that should be changed, but Ramda's philosophy is generally garbage-in-garbage-out. So that either call, for whatever reason, is returning [undefined].

And that means that you're supplying [undefined] as the predicate function to ifElse. You should receive an error when you try to call it. I haven't tried to trace down why that error is being shadowed by the one you see.

As to how to fix this using Ramda, I'm not really sure where to start. This is pretty far from Ramda's usual style. If nothing else, functions that accept no parameters are extremely rare in Ramda. I guess I would start the Ramda part of this with a function that accepts argv.inputFile and argv.outputFile, either as individual objects or perhaps as that single object argv.

Scott Sauyet
  • 49,207
  • 4
  • 49
  • 103
  • I was under the impression that the `point free` style was incredibly common in Ramda. In fact, they even suggest a set of tutorials in their home page about functions with point free style! Any review you could have would be greatly appreciated! – Flame_Phoenix Dec 14 '17 at 07:51
  • I'll see if I can find time this evening to look it over more carefully, but no promises. As to point-free, it is very common in Ramda code (disclaimer: I'm one of the Ramda authors.) But this is not what [point-free](https://stackoverflow.com/questions/944446) means. Having no arguments to a function is a code smell in functional programming. Point-free is not about *having* no arguments, only about writing in a style that doesn't explicitly *mention* them. It's the difference between (pointed) `let total = (lines) => reduce(add, 0, lines)` and (point-free) `let total = reduce(add, 0)`. – Scott Sauyet Dec 14 '17 at 15:51
  • Ahh, i get what you mean now. I don't hold you accountable for any promises, but I will say I would be really interested to see how you would tackle such a common task :D – Flame_Phoenix Dec 14 '17 at 22:15