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
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 })
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
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'
Then just slip the sleep
command into logFizziness
.
const logFizziness = log => pipe ([
sleep (1000),
fizziness,
log
])
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
}
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)
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
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)
I just need to put this guy at the top.
import reduce from 'mojiscript/list/reduce'
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)
])
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
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
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)
})
})
})
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.
Fortunately, it's easy to mock setTimeout
.
// setup mocks
jest.spyOn(global, 'setTimeout').mockImplementation(func => func())
// take down mocks
global.setTimeout.mockReset()
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)
})
})
Summary
- We learned how trivial adding asynchronous code can be.
- We learned how separating dependencies from
main
intoindex
can make testing easier. - We learned how to asynchronously
map
. Wait... did I just sayasync
map
? You might have missed this because it was so easy, butmap
,filter
, andreduce
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
Top comments (0)