What is MojiScript
MojiScript is an async-first, opinionated, and functional language designed to have 100% compatibility with JavaScript engines.
Because MojiScript is written async-first, asynchronous tasks not only become trivial, but become a pleasure to use. Read this article for more on async in MojiScript: Why async code is so damn confusing (and a how to make it easy).
MojiScript is also JavaScript engine compatible, meaning it runs in node.js and browsers without the need to transpile!
Even though it runs in any JavaScript engine, you will notice significant differences between MojiScript and JavaScript.
Significant differences you say?
Well, JavaScript as you know it will not run. But other than that...
It's best to forget everything you know about JavaScript when learning MojiScript.
But don't worry, there are easy ways to interop with JavaScript. We won't be convering JavaScript iterop in this article though.
FizzBuzz
You should already be familiar with FizzBuzz. If not, the game is simple:
Write a program that prints the numbers from 1 to 100. But for multiples of three print "Fizz" instead of the number and for the multiples of five print "Buzz". For numbers which are multiples of both three and five print "FizzBuzz". -- FizzBuzz Test
Start
We'll be writing a node application, so I am assuming you already have node and git installed.
Make sure you have node v10.9.0
+
Install the mojiscript-starter-app
git clone https://github.com/joelnet/mojiscript-starter-app.git
cd mojiscript-starter-app
Make sure it builds and runs.
npm ci
npm run build
npm start --silent
If everything went well you should see:
Hello World
Format on Save
Visual Studio Code is highly recommended as your editor. It adds some nice features like Format on Save.
If there's another IDE you love that doesn't Format on Save, then you can just run this command:
npm run watch
This is important because your app will not build
if your formatting is off.
You can also run this command to fix your formatting.
npm run build -- --fix
It's little things like this that make MojiScript such a joy to code in!
Files
There are two files that are important:
src/index.mjs
- Load dependencies and start app.
src/main.mjs
- Your app without dependencies makes it easy to test.
note: We are using the .mjs
file extension so that we can use node --experimental-modules
which gives us the ability to import
and export
without transpiling.
Open up src/main.mjs
That is where we will start.
It should look like this:
import pipe from 'mojiscript/core/pipe'
const main = ({ log }) => pipe ([
'Hello World',
log
])
export default main
Let's write some code!
First let's create a loop from 1 to 100.
Import these two functions:
-
range
- Creates an Iterable fromstart
toend
. -
map
- Maps over an Iterable.
import range from 'mojiscript/list/range'
import map from 'mojiscript/list/map'
Modify your main to look like this:
const main = ({ log }) => pipe ([
() => range (1) (101),
map (log)
])
run your app and you should see the console output numbers 1 to 100.
Next, I'd like to get rid of those "magic numbers" 1
and 100
. You shouldn't be hard-coding values directly into your source, at least not in src/main.mjs
. You can however put those values in src/index.mjs
since it's responsibility is to load and inject dependencies and add configuration.
So open up src/index.mjs
add those numbers to a new value state
.
const state = {
start: 1,
end: 100
}
Add the state to the run
command
run ({ dependencies, state, main })
Now src/index.mjs
should look like this:
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 })
Go back to src/main.mjs
and modify main
to use start
and end
.
const main = ({ log }) => pipe ([
({ start, end }) => range (start) (end + 1),
map (log)
])
run npm start
again to make sure it works.
Let's break to talk about Pipes
I would like to talk a little bit about pipe
and how it works. Imagine data moving through the pipe
and being transformed (or Morphed) at each step of the way.
With this pipe, if we were to pass a 4
through it, it will be morphed into a 9
, then an 18
. log
performs a side effect and doesn't morph the value at all so the 18
would be returned from the pipe.
const main = pipe ([
// |
// | 4
// ▼
/*-------------------*/
/**/ x => x + 5, /**/
/*-------------------*/
// |
// | 9
// ▼
/*-------------------*/
/**/ x => x * 2, /**/
/*-------------------*/
// |
// | 18
// ▼
/*-------------------*/
/**/ log, /**/
/*-------------------*/
// |
// | 18
// ▼
])
So in our FizzBuzz example, we start with { start: 1, end: 100 }
morph that into an Iterable
of 1
to 100
and then log
each value. The pipe would return an array of 1
to 100
.
Back to FizzBuzz
So far all we have done is create an array. We still have to calculate the Fizziness of each number.
import allPass from 'mojiscript/logic/allPass'
import cond from 'mojiscript/logic/cond'
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 ]
])
isFizzBuzz
is true
if both isFizz
and isBuzz
are true.
cond
is similar to JavaScript's switch
. It compares either a function or a value and then will execute a function or a value. The last condition [ () => true, x => x ]
will always return true
and then will return the value passed into fizziness
. This is the default case.
Finally, add the fizziness
morphism to your main
const main = ({ log }) => pipe ([
({ start, end }) => range (start) (end + 1),
map (fizziness),
map (log)
])
Function Compostion
You may have noticed map
being called twice. We are looping through 1
to 100
twice. It's not a big deal here because 100 iterations is microscopic. But other applications this might be important.
We can compose fizziness
and log
together using a pipe
and modify our main
to use our new logFizziness
function.
// logFizziness :: Function -> Number -> Number
const logFizziness = log => pipe ([
fizziness,
log
])
const main = ({ log }) => pipe ([
({ start, end }) => range (start) (end + 1),
map (logFizziness (log))
])
Now we are iterating through the iterator only once.
Our final src/main.mjs
should look like this:
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
Part 2
In part 2 I'm going to go over asynchronous mapping, Infinity
, reduce
, unit testing and more! This is where MojiScript really starts to get fun!
Follow me here, or on Twitter @joelnet so you do not miss Part 2!
End
Did you notice you just learned currying, partial application, function composition, functors, and category theory? Gasp! Of course not. That's because we were having too much fun!
If you thought MojiScript was fun, give it a star https://github.com/joelnet/MojiScript! If you have questions, put em in the comments!
Do not miss Part 2 where I reveal the mysteries of life!
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 (7)
Where/how exactly do you let sleep not require an async/await function? Your sleep is identical to mine but mine requires you to await it, never found a way around that. Neither mojiscript/threading/sleep or mojiscript/core/pipe exports anything async.
Secondly, do you deliberately not comment your exports or is it lazyness? I can help you with that. Most looks pretty straightforward.
And last, maybe you should define a main property in your package.json so that mojiscript can be required the conventional way.
I also want to suggest you stop calling this a language but that is just my opinion and really up to you.
MojiScript's
pipe
,map
,filter
, andreduce
work with both async and sync code. They do not need toawait
anything.You could look at
call
, which will execute with a function after testing if thevalue
is or is not aPromise
. github.com/joelnet/MojiScript/blob...Post your code, I'll figure out the difference.
I'm still looking for a solution for auto-generated documentation. So I am holding off on putting docs directly into the code because the format may differ based on the tool I select. I have posted about the problems I am having here: Let's talk about the state of auto-generated documentation tools for JavaScript
There are full docs for MojiScript here: github.com/joelnet/MojiScript/blob...
I thought about this, but decided not to. I don't want the entire library being imported into your project. Only the functions you need will be imported to keep the size down.
This has been discussed in the Discord chat. More on the reasons why will be written shortly.
Joel, I would like to point out that a main.js does not prevent you from importing only the functions you need. It doesn't matter.
First of all, import statements will still work.
Secondly you can also require selectively only the stuff you need, exactly like import.
The design decision to create every function as it's own import was made to remove a footgun.
The footgun removed is the ability to import the entire library when you only need a few functions.
This way, the output bundle sir is only as big as the functions you import.
So MojiScript will have a microscopic footprint in your final output bundle because it is no longer possible to import the entire bundle.
I made a script called mojifier that converts your module into a single object with everything included / proper namespaces by iterating the directories/files.
Amazing!
Just one question, does it has a better performance than normal JavaScript for Node.js?
Or it is just for doing functional coding style?
Thanks, but you are asking the wrong question!
It functions better than JavaScript.
Look at this JavaScript code. It is complicated to understand the output. This code outputs all give 6's after 1 second.
Now look at the MojiScript alternative. This program will output 1 through 5 with 1 second delay between each output. (imports have been excluded for brevity)
Did you notice that? Did we just create an asynchronous
map
? Shhh. articles coming out on Asynchronousmap
,filter
, andreduce
shortly. Stay tuned!