SpaceBeers
SpaceBeers

Reputation: 13947

Mapping over an array of Tasks in Javascript

So I've started looking at Ramda / Folktale. I'm having an issue trying to map over an array of Tasks that comes from a directory. I'm trying to parse file contents.

var fs = require('fs');
var util = require('util');
var R = require('ramda');
var Task = require('data.task');

var compose = R.compose;
var map = R.map;
var chain = R.chain;


function parseFile(data) {
      console.log("Name: " + data.match(/\$name:(.*)/)[1]);
      console.log("Description: " + data.match(/\$description:(.*)/)[1]);
      console.log("Example path: " + data.match(/\$example:(.*)/)[1]);
}

// String => Task [String]
function readDirectories(path) {
    return new Task(function(reject, resolve) {
        fs.readdir(path, function(err, files) {
            err ? reject(err) : resolve(files);
        })
    })
}

// String => Task String
function readFile(file) {
    return new Task(function(reject, resolve) {
        fs.readFile('./src/less/' + file, 'utf8', function(err, data) {
            err ? reject(err) : resolve(data);
        })
    })
}

var app = compose(chain(readFile), readDirectories);

app('./src/less').fork(
    function(error) { throw error },
    function(data)  { util.log(data) }
);

I'm reading the files in a directory and returning a Task. When this resolves it should go into the readFile function (which returns a new task). Once it reads the file I want it to just parse some bits out of there.

With the following:

var app = compose(chain(readFile), readDirectories);

It gets into the readFile function but 'file' is an array of files so it errors.

With:

var app = compose(chain(map(readFile)), readDirectories);

We never get into fs.readfile(), but 'file' is the actual file name.

I'm pretty stumped on this and the documentation is baffling. Any suggestions welcome.

Thanks

Upvotes: 9

Views: 1895

Answers (3)

davidchambers
davidchambers

Reputation: 24806

'use strict';

const fs = require('fs');

const Task = require('data.task');
const R = require('ramda');


//    parseFile :: String -> { name :: String
//                           , description :: String
//                           , example :: String }
const parseFile = data => ({
  name:         R.nth(1, R.match(/[$]name:(.*)/, data)),
  description:  R.nth(1, R.match(/[$]description:(.*)/, data)),
  example:      R.nth(1, R.match(/[$]example:(.*)/, data)),
});

//    readDirectories :: String -> Task (Array String)
const readDirectories = path =>
  new Task((reject, resolve) => {
    fs.readdir(path, (err, filenames) => {
      err == null ? resolve(filenames) : reject(err);
    })
  });

//    readFile :: String -> Task String
const readFile = filename =>
  new Task(function(reject, resolve) {
    fs.readFile('./src/less/' + filename, 'utf8', (err, data) => {
      err == null ? resolve(data) : reject(err);
    })
  });

//    dirs :: Task (Array String)
const dirs = readDirectories('./src/less');

//    files :: Task (Array (Task String))
const files = R.map(R.map(readFile), dirs);

//    sequenced :: Task (Task (Array String))
const sequenced = R.map(R.sequence(Task.of), files);

//    unnested :: Task (Array String)
const unnested = R.unnest(sequenced);

//    parsed :: Task (Array { name :: String
//                          , description :: String
//                          , example :: String })
const parsed = R.map(R.map(parseFile), unnested);

parsed.fork(err => {
              process.stderr.write(err.message);
              process.exit(1);
            },
            data => {
              process.stdout.write(R.toString(data));
              process.exit(0);
            });

I wrote each of the transformations on a separate line so I could include type signatures which make the nested maps easier to understand. These could of course be combined into a pipeline via R.pipe.

The most interesting steps are using R.sequence to transform Array (Task String) into Task (Array String), and using R.unnest to transform Task (Task (Array String)) into Task (Array String).

I suggest having a look at plaid/async-problem if you have not already done so.

Upvotes: 12

Brian
Brian

Reputation: 967

I had a similar problem of reading all the files in a directory and started with ramda's pipeP:

'use strict';

const fs = require('fs');
const promisify = require("es6-promisify");
const _ = require('ramda');

const path = './src/less/';
const log  = function(x){ console.dir(x); return x };
const map  = _.map;
const take = _.take;

const readdir = promisify(fs.readdir);
const readFile = _.curry(fs.readFile);
const readTextFile = readFile(_.__, 'utf8');
const readTextFileP = promisify(readTextFile);

var app = _.pipeP(
  readdir,
  take(2),  // for debugging, so don’t waste time reading all the files
  map(_.concat(path)),
  map(readTextFileP),
  promiseAll,
  map(take(50)),
  log
  );

function promiseAll(arr) {
  return Promise.all(arr)
}

app(path);

Promise.all seems to be required when reading the files as pipeP expects a value or a promise, but is receiving an array of promises to read the files. What puzzles me is why I had to make a function return the Promise.all instead of inlining it.

Your usage of task/fork is intriguing because error handling is built in. Would like pipeP to have a catch block, because without it need to inject maybe, which is hard for a beginner like me to get that right.

Upvotes: 0

Scott Christopher
Scott Christopher

Reputation: 6516

As David has suggested, commute is useful for converting a list of some applicatives (such as Task) into a single applicative containing the list of values.

var app = compose(chain(map(readFile)), readDirectories);

We never get into fs.readfile(), but 'file' is the actual file name.

The closely related commuteMap can help here too as it will take care of the separate map step, meaning the code above should also be able to be represented as:

var app = compose(chain(commuteMap(readFile, Task.of)), readDirectories);

Upvotes: 4

Related Questions