Quickstart: Alternative solution

Based on the contents of the Quickstart tutorial, this shows an alternative solution to the same domain, almost entirely with the help of Type and UnionType from the adt package. This time, the resulting looks like chained calls (a.k.a. fluent api) on the outside, but is completely pure on the inside. Also note the similarity between the end result and the comparison solution using arrays.

Noteworthy

This section contains snippets of the complete code which might demand some explanation, be that because of their syntax, or because of new concepts. Feel free to skip them if you like.

Usage of UnionType

We create a new "data" type Sentence, which can either be empty ("") or at least contain a single word. To represent a Sentence build from the empty string, we define a sub-type EmptyString without any fields and a Line sub-type which holds an Array of values. Any Sentence can only ever be either of both sub-types. You can think of it as a special form of if statement at the type level.

// data Sentence a = Line (Array a)
//                   | EmptyString
const Sentence = UnionType('Sentence', {Line: ['ls'], EmptyString: []});

Pattern matching

Also note, that we can use pattern matching against the types created by UnionType (to a certain extend). This way, we can handle every sub-type separately which gives great power.

// map :: Sentence S => S a ~> (a -> b) -> S b
Sentence.fn.map = function (f) {
    return this.caseOf({
        Line: ls => Line(ls.map(f)), // <-- do this if the instance is a "Line"
        EmptyString: () => this      // <-- do this for "EmptyString" instances 
    });
}

Complete code

You can see the complete code below.

// file: utils/parsers/sentences.js
// ================================

const { adt: {Type, UnionType},
        monoid: {Fn},
        lambda: {curry, pipe},
        operation: {map, prop, foldMap} } = require('futils');



/********** DATA TYPES **********/

// data Matrix = Matrix [String]
const Matrix = Type('Matrix', ['value']);

// map :: Matrix M => M [String] ~> (String -> String) -> M [String]
Matrix.fn.map = function (f) {
    return Matrix(this.value.map(f));
}

// reduce :: Matrix M => M [String] ~> (a -> String -> a) -> a -> a
Matrix.fn.reduce = function (f, a) {
    return this.value.reduce(f, a);
}



// data Sentence a = Line (Array a)
//                   | EmptyString
const Sentence = UnionType('Sentence', {Line: ['ls'], EmptyString: []});
const {Line, EmptyString} = Sentence;

// #fromString :: Sentence S => String -> S a
Sentence.fromString = function (string) {
    return string.trim() !== '' ? Sentence.of(string.split(/\s+/g)) : EmptyString();
}

// map :: Sentence S => S a ~> (a -> b) -> S b
Sentence.fn.map = function (f) {
    return this.caseOf({
        Line: ls => Line(ls.map(f)),
        EmptyString: () => this
    });
}

// reduce  :: Sentence S => S a ~> ((b, a) -> b) -> a -> b
Sentence.fn.reduce = function (f, a) {
    return this.caseOf({
        Line: ls => ls.reduce(f, a),
        EmptyString: () => a
    });
}

// join  :: Sentence S => S a ~> String -> String
Sentence.fn.join = function (sep) {
    return this.reduce((acc, wrd) => !acc ? wrd : `${acc}${sep}${wrd}`, '');
}



// Parser :: { String : () -> Fn (String -> String) }
const Parser = {
    '^': _ => Fn(s => s[0].toUpperCase() + s.slice(1)),
    '!': _ => Fn(s => '-.+/=*'.includes(s) ? s : s + '!'),
    '<': _ => Fn(s => s.length < 2 ? s : s.split('').reverse().join('')),
    '"': _ => Fn(s => s.trim())
}

// interpreter :: (Matrix, Parser) -> (Number -> Number)
const interpreter = (m, p) => foldMap(x => p[x](x), m).value;



module.exports = {Matrix, Parser, Sentence, interpreter}

Test

For a quick test, use this as a template:

// file: hello-world.js
// ================================
const {Matrix, Parser, Sentence, interpreter} = require('./utils/parsers/sentences');



// matrix :: Matrix
const matrix = Matrix(['^', '!', '<']);

// interpret :: String -> String
const interpret = interpreter(matrix, Parser);



// result :: Sentence a
const result = Sentence.fromString('hello world - how are you today?').map(interpret);

// empty :: Sentence a
const empty = Sentence.fromString('').map(interpret);

console.log(`Parsed: ${result.join(' ')}`);
console.log(`Empty: ${empty.join(' ')}`);

Array based solution

As a comparison, here is a solution based on Array instead of Sentence. The core parts are equal, but the Sentence based solution conveys more clarity by abstracting the underlying types away into more descriptive ones.

// file: hello-world-array.js
// ================================
const {Matrix, Parser, interpreter} = require('./utils/parsers/sentences');



// matrix :: Matrix
const matrix = Matrix(['^', '!', '<']);

// interpret :: String -> String
const interpret = interpreter(matrix, Parser);

// convert :: String -> [String]
const convert = s => {
    if (s.trim() !== '') {      // <-- the if-statement removed by UnionType
        return s.split(/\s+/g);
    }
    return [];
}



// result :: [String]
const result = convert('hello world - how are you today?').map(interpret);

// empty :: [String]
const empty = convert('').map(interpret);

console.log(`Parsed: ${result.join(' ')}`);
console.log(`Empty: ${empty.join(' ')}`);