DEV Community

loading...

Getting started with MojiScript: Async, Infinity, Testing (part 2).

JavaScript Joel
Cofounded Host Collective (DiscountASP.net). Cofounded Player Axis (Social Gaming). Computer Scientist and Technology Evangelist with 20+ years of experience with JavaScript!
Updated on ・6 min read

Infinity Loop

This is part 2 to Getting started with MojiScript: FizzBuzz (part 1). In Part 1 we created a basic FizzBuzz application using MojiScript.

Skipped Part 1?

It is recommended to start with Part 1, but if you don't want to, this is how to catch up:

# download mojiscript-starter-app
git clone https://github.com/joelnet/mojiscript-starter-app.git
cd mojiscript-starter-app

# install, build and run
npm ci
npm run build
npm start --silent
Enter fullscreen mode Exit fullscreen mode

Copy this into src/index.mjs

import log from 'mojiscript/console/log'
import run from 'mojiscript/core/run'
import main from './main'

const dependencies = {
  log
}

const state = {
  start: 1,
  end: 100
}

run ({ dependencies, state, main })
Enter fullscreen mode Exit fullscreen mode

Copy this into src/main.mjs

import cond from 'mojiscript/logic/cond'
import pipe from 'mojiscript/core/pipe'
import map from 'mojiscript/list/map'
import range from 'mojiscript/list/range'
import allPass from 'mojiscript/logic/allPass'

const isFizz = num => num % 3 === 0
const isBuzz = num => num % 5 === 0
const isFizzBuzz = allPass ([ isFizz, isBuzz ])

const fizziness = cond ([
  [ isFizzBuzz, 'FizzBuzz' ],
  [ isFizz, 'Fizz' ],
  [ isBuzz, 'Buzz' ],
  [ () => true, x => x ]
])

const logFizziness = log => pipe ([
  fizziness,
  log
])

const main = ({ log }) => pipe ([
  ({ start, end }) => range (start) (end + 1),
  map (logFizziness (log))
])

export default main
Enter fullscreen mode Exit fullscreen mode

Run npm start --silent to make sure it still works.

Let the fun begin!

This is where all the fun stuff happens.

What if I want FizzBuzz to go to Infinity? If I ran the code with Infinity my console would go insane with logs, CPU would be at 100%. I don't know if I can even stop it, but I don't want to find out.

So the first thing I want to add is a delay between each log. This will save my sanity so I can just CTRL-C if I get impatient waiting for Infinity to come.

Like I said in Part 1, asynchronous tasks not only become trivial, but become a pleasure to use.

Add an import at the top.

import sleep from 'mojiscript/threading/sleep'
Enter fullscreen mode Exit fullscreen mode

Then just slip the sleep command into logFizziness.

const logFizziness = log => pipe ([
  sleep (1000),
  fizziness,
  log
])
Enter fullscreen mode Exit fullscreen mode

That's it. Seriously. Just add a sleep command. Try to imagine how much more complicated it would have been to do with JavaScript.

Run the app again and watch the fizziness stream out 1 second at a time.

Now that I have no worries about exploding my console, if I want to count to Infinity all I have to do is change...

// change this:
const state = {
  start: 1,
  end: 100
}

// to this:
const state = {
  start: 1,
  end: Infinity
}
Enter fullscreen mode Exit fullscreen mode

You see we can do that because range is an Iterator and not an Array. So it will enumerator over the range one number at a time!

But... map will turn that Iterator into an Array. So eventually map will explode our memory. How can I run this to Infinity if I run out of memory?

Okay, so let's throw away the Array map is slowly creating.

This is where reduce comes in handy. reduce will let us control what the output value is.

// this is what map looks like
map (function) (iterable)

// this is what reduce looks like
reduce (function) (default) (iterable)
Enter fullscreen mode Exit fullscreen mode

That's not the only difference, because reduce's function also takes 1 additional argument. Let's compare a function for map with a function for reduce.

const mapper = x => Object
const reducer = x => y => Object
Enter fullscreen mode Exit fullscreen mode

Since the first argument is reduce's accumulator and I don't care about it, I can just ignore it.

// instead of this:
logFizziness (log)

// I would write this:
() => logFizziness (log)
Enter fullscreen mode Exit fullscreen mode

I just need to put this guy at the top.

import reduce from 'mojiscript/list/reduce'
Enter fullscreen mode Exit fullscreen mode

I also need to throw in a default value of (0) and then I can convert main to this:

const main = ({ log }) => pipe ([
  ({ start, end }) => range (start) (end + 1),
  reduce (() => logFizziness (log)) (0)
])
Enter fullscreen mode Exit fullscreen mode

We no longer have any memory issues because no Array is being created!

The final src/main.mjs should look like this:

import cond from 'mojiscript/logic/cond'
import pipe from 'mojiscript/core/pipe'
import range from 'mojiscript/list/range'
import reduce from 'mojiscript/list/reduce'
import allPass from 'mojiscript/logic/allPass'
import sleep from 'mojiscript/threading/sleep'

const isFizz = num => num % 3 === 0
const isBuzz = num => num % 5 === 0
const isFizzBuzz = allPass ([ isFizz, isBuzz ])

const fizziness = cond ([
  [ isFizzBuzz, 'FizzBuzz' ],
  [ isFizz, 'Fizz' ],
  [ isBuzz, 'Buzz' ],
  [ () => true, x => x ]
])

const logFizziness = log => pipe ([
  sleep (1000),
  fizziness,
  log
])

const main = ({ log }) => pipe ([
  ({ start, end }) => range (start) (end + 1),
  reduce (() => logFizziness (log)) (0)
])

export default main
Enter fullscreen mode Exit fullscreen mode

Unit Tests

It's probably good practice to move isFizz, isBuzz, isFizzBuzz and fizziness to src/fizziness.mjs. But for article brevity I'm not doing that here.

To unit test these bad boys, just add the export keyword to them.

export const isFizz = num => num % 3 === 0
export const isBuzz = num => num % 5 === 0
export const isFizzBuzz = allPass ([ isFizz, isBuzz ])

export const fizziness = cond ([
  [ isFizzBuzz, 'FizzBuzz' ],
  [ isFizz, 'Fizz' ],
  [ isBuzz, 'Buzz' ],
  [ () => true, x => x ]
])

export const logFizziness = log => pipe ([
  sleep (1000),
  fizziness,
  log
])

export const main = ({ log }) => pipe ([
  ({ start, end }) => range (start) (end + 1),
  reduce (() => logFizziness (log)) (0)
])

export default main
Enter fullscreen mode Exit fullscreen mode

Create src/__tests__/fizziness.test.mjs and write some tests:

import { isFizz } from '../main'

describe('fizziness', () => {
  describe('isFizz', () => {
    test('true when divisible by 5', () => {
      const expected = true
      const actual = isFizz(5)
      expect(actual).toBe(expected)
    })

    test('false when not divisible by 5', () => {
      const expected = false
      const actual = isFizz(6)
      expect(actual).toBe(expected)
    })
  })
})
Enter fullscreen mode Exit fullscreen mode

Now here I am using the Jest test framework. You can use whatever. Notice that I am writing the tests in JavaScript. I found it's best to just follow the format the test framework wants you to use. I don't think it's worth wrapping Jest so we can write tests in MojiScript.

Testing Main

Testing main complex. We have a sleep command in there. So if we test numbers 1-15, then it'll take 15 seconds.

Ain't nobody got time for that

Fortunately, it's easy to mock setTimeout.

// setup mocks
jest.spyOn(global, 'setTimeout').mockImplementation(func => func())

// take down mocks
global.setTimeout.mockReset()
Enter fullscreen mode Exit fullscreen mode

Now our test should take about 7ms to run, not 15 seconds!

import I from 'mojiscript/combinators/I'
import main from '../main'

describe('main', () => {
  const log = jest.fn(I)

  beforeEach(() => jest.spyOn(global, 'setTimeout').mockImplementation(func => func()))
  afterEach(() => global.setTimeout.mockReset())

  test('main', async () => {
    const expected = [[1], [2], ["Buzz"], [4], ["Fizz"], ["Buzz"], [7], [8], ["Buzz"], ["Fizz"], [11], ["Buzz"], [13], [14], ["FizzBuzz"]]
    expect.assertions(1)
    await main ({ log }) ({ start: 1, end: 15 })
    const actual = log.mock.calls
    expect(actual).toMatchObject(expected)
  })
})
Enter fullscreen mode Exit fullscreen mode

Summary

  • We learned how trivial adding asynchronous code can be.
  • We learned how separating dependencies from main into index can make testing easier.
  • We learned how to asynchronously map. Wait... did I just say async map? You might have missed this because it was so easy, but map, filter, and reduce can be asynchronous. This is a big deal and I will be writing an entire article on this in the near future.

Oh ya, in Part 1 I said I would "reveal the mysteries of life!". Well, I don't want to disappoint, so the mystery of life is... LIFE. It's recursion, so loop on that.

Follow me here, or on Twitter @joelnet!

If you thought MojiScript was fun, give it a star https://github.com/joelnet/MojiScript! Share your opinions with me in the comments!

Read my other articles:

Why async code is so damn confusing (and a how to make it easy)

How I rediscovered my love for JavaScript after throwing 90% of it in the trash

Cheers!

Discussion (0)