9

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

SpaceBeers
  • 13,617
  • 6
  • 47
  • 61

3 Answers3

12
'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.

davidchambers
  • 23,918
  • 16
  • 76
  • 105
  • 1
    Thanks. That's actually really interesting. It's such a shift in thinking I've been strugging with it. Back to the Dr Boolean book. – SpaceBeers Nov 05 '15 at 09:41
  • I can't find commute in the ramda documentation. What am I missing? – akaphenom Jul 07 '16 at 14:03
  • also (since I can't find the documentation) how is commute different than "control.monads" sequence function? – akaphenom Jul 07 '16 at 14:06
  • `R.commute` was renamed [`R.sequence`](http://ramdajs.com/docs/#sequence) in [ramda/ramda#1467](https://github.com/ramda/ramda/pull/1467). I'll update my answer. – davidchambers Jul 07 '16 at 17:30
4

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);
Community
  • 1
  • 1
Scott Christopher
  • 6,458
  • 23
  • 26
0

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.

Brian
  • 967
  • 7
  • 12