[006.4] Building our app with Mori.js

Implementing our tournament app with Mori.js

Subscribe now

Building our app with Mori.js [01.02.2018]


Hello and welcome back to this functional programming tutorial with Javascript and Mori.js. Yesterday we planned out how we would architect our application, now we're going to actually build it.

Project Requirements

This time, our project will require very little to run. All you need will be

  1. Node 8.x.x or above
  2. nodemon - and this is only optional but helpful

Getting Started

Ok, to get started, let's open up our terminal and handle some basic setup.

cd /your/project/directory
npm init
npm install --save bluebird mori colors prompt

Good, now let's open our package.json and make a quick change to our "scripts" key.

"scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon app.js"

Alright, now let's open up app.js and write some basic code to get this project off the ground.

const Promise = require('bluebird')
const prompt = Promise.promisifyAll(require('prompt'))
const _ = require('mori')
const colors = require('colors/safe')

const testData = _.vector('test', 'test1', 'test2', 'test3', 'test4', 'test5', 'test6', 'test7')
const testDataOdd = _.vector('test', 'test1', 'test2', 'test3', 'test4', 'test5', 'test6', 'test7', 'test8')

So first, we imported our libraries we'll be using for this. They cover a range of things such as handling Promises, user input, and text colors. Also, of course we imported Mori. Below, we have 2 different sets of test data to work with as we test our functions that we write.

Now let's write a couple of small utility functions to help us on our way.

const log = (...args) => console.log(...args.map(_.toJs))

function logVector (list) {
  _.each(i => log(i))

function changePrompt (color, newPrompt) {
  prompt.message = colors.green(newPrompt)
  switch(color) {
    case 'green':
      prompt.message = colors.green(newPrompt)
    case 'red':
      prompt.message = colors.red(newPrompt)
    case 'gray':
      prompt.message = colors.gray(newPrompt)
    case 'blue': 
      prompt.message = colors.blue(newPrompt)
    case 'cyan':
      prompt.message = colors.cyan(newPrompt)

Due to the fact that Mori uses Clojure data structures, it can be a bit irritating when we need to console log our data for testing or display so we have the log and logVector functions written to handle this issue for us. Next we wrote a changePrompt function which will be helpful when dealing with setting up our user prompts.

Now we need to get into the meat of our actual app system functions. First, let's cover the function that tests whether or not our brackets are even.

function isTournamentBracketEven (list) {
  const listEven = _.isEven(_.count(list))
  if (listEven) {
    return list
  } else if (!listEven) {
    log(colors.red('Your tournament needs an even number of contestants.'))
    log(colors.red('Do not worry though, we have removed the final contestant to make things even.'))
    return _.pop(list)

Great, this does exactly what we talked about yesterday. If the bracket isn't even, drop the last item and notify the user what is going on. This isn't rocket science. If you would like to test this function, just call it below and pass it either of our testData lists to see what happens. You should be able to do this with any of the functions we write from here on to make sure they work as needed.

Now we need a function to be able to print our list to the user in a way that helps them see the brackets being formed. This shouldn't be too hard. Let's write it out

function printBracket (bracket) {
  const x = _.toJs(bracket)
  x.map((item, index) => {
    if (_.isEven(index)) {
      log((index + 1) + '. ' + item)
    } else {
      log((index + 1) + '. ' + item)

Great, now you may be thinking wait a minute... why are we converting this to a Javascript array?. The reason is because it provides easy access to the standard JS map function which gives access to the index of each item in the list. After all, why make things more complicated than they need to be?

Now, we're going to move forward and write our runMatch function. Remember that algorithm we worked on yesterday to cycle through each bracket and return the winners? Now we're implementing it for real. Here we go.

async function runMatch (incrementer, list, newVector) {
  const firstItem = _.nth(list, incrementer)
  const secondItem = _.nth(list, (incrementer + 1))
  log(colors.green('match: ' + firstItem + ' vs. ' + secondItem))
  log(colors.blue("Enter the winner's name."))
  const winner = await prompt.getAsync(['winner'])
  switch(winner.winner) {
    case firstItem:
      if (incrementer >= (_.count(list) - 2)) {
        return _.conj(newVector, firstItem)
      } else {
        return runMatch(incrementer + 2, list, _.conj(newVector, firstItem))
    case secondItem:
      if (incrementer >= (_.count(list) - 2)) {
        return _.conj(newVector, secondItem)
      } else {
        return runMatch(incrementer + 2, list, _.conj(newVector, secondItem))
      return runMatch(incrementer, list, newVector)

Awesome, as you may notice, this looks pretty similar to what we worked out yesterday, but now we have added in our user prompts and interaction. If you would like to take a minute to test this function out, go for it.

Now we need a mechanism for adding a contestant name to our initial list. Remember that after all, when the user first starts the app, they will be adding names from the command line into an array. So let's build the mechanism for putting that data into our vector.

function addContestant (list, name) {
  const newList = _.conj(list, name.name)
  _.each(newList, i => log(i))
  return newList

Great, as you can see there is no voodoo here. We generate a new list with the contestant name added to the end then log the new list of contestants to the user before returning the new list.

Now we need a function for actually running our tournament. In other words, we have a means of running the matches for a full tournament bracket with runMatch, now we need a way of transitioning from bracket to bracket until we get to that final champion. So let's write it out.

async function runTournament (list) {
  if (_.count(list) === 1) {
    return list
  } else {
    const winList = await runMatch(0, list, _.vector())

There we go, that should do the trick. If the list length is 1, return the champion. Otherwise, print the new bracket to the user then run the matches for that bracket before calling this function again recursively.

Believe it or not, we're almost done, now we need a way of starting the tournament after the user has decided they are done constructing their list of contestants. Let's write it out.

function startTournament (list) {
  const bracket = isTournamentBracketEven(list)

Pretty simple, we run our isTournamentBracketEven function, print the bracket to the user, then begin running the tournament. While in this case, it may not be a literal pipeline function, you can see we are now to that stage in our app where we are just piping together our previous functions to get the product we want.

Now we have one more function to write and it will be a simple start function. This will be the stage where we are using user input to build that initial list.

async function start (list) {
  const name = await prompt.getAsync(['name'])
  if (!name) {
    throw new Error('name error')
  } else if (name.name === 'done') {
  } else {
    start(addContestant(list, name))

There you go, and once again, you can see that while this isn't a literal pipeline, we are indeed just stringing together our previous functions to achieve the result that we wanted.

Now it's time to write the last little bit of code to initialize this app and we'll be ready to fire it up!.

let entryList = _.vector()

log(colors.red("Hello! Let's create our tournament bracket!"))
log(colors.red("Enter the name of the contestants in your tournament."))
changePrompt('blue', 'contestant name')


And presto... we're done! Open it up in your terminal and give it a try! The app should be doing exactly what we were previewing yesterday. Let's be honest, you've come along way in a short time. We started out with some theory on functional programming and immutable data structures, we took a quick dive into Mori.js itself, and now we have built this nifty little tournament app which illustrates many of our points about how you would set up a program in a functional manner.

Remember this isn't intended to denigrate imperative or object oriented methods of programming. These paradigms are just means for getting a task done. You should always find what fits your situations the best and not try to bend everything into a particular paradigm be it functional or whatever. Thank you so much for working through this tutorial and I hope that this has been of value to you in your growth as a programmer. Best wishes.