Awesome
MojiScript
MojiScript is an Async First, opinionated, and functional library and language designed to have 100% compatibility with JavaScript engines. This will allow full access to JavaScript modules (NPM) and all tooling already available to JavaScript. This means that MojiScript language features can run in any JavaScript application and vice-versa.
MojiScript's design is derived from Functional Programming concepts such as Currying, Partial Application, Function Composition, Category Theory, and Atomic Design.
NEW: Join the discussion in the MojiScript Discord chat!
Environment Support
This library supports ES6. If you need to support ES5, you will need to transpile it with Babel.
Table of Contents
- Philosophy
- Benefits
- Quickstart
- Examples
- Star Wars Console - Check this out!
- Express Hello World - Express "Hello World" web server.
- Express Static File Server - Express static file server.
- Async Simple
- Hello World
- Conditionals
- map/filter/reduce
- FizzBuzz
- Recursion
- API Documentation
- Style Guide
- Complementary Libraries
- Variables
- Objects
- String Templates
- Expressions
- Pipes
- Conditionals
- Error Handling
- Morphisms
- Application Layout
- Unit Tests
- Previous Art
- Contributors
Philosophy
The MojiScript philosophy is to provide a functional-style application framework, making asynchronous tasks intuitive and easy.
MojiScript is heavily opinionated and prevents code considered to be async-unfriendly like for
loops and statement blocks.
Benefits
- The Asynchronous-first design greatly simplifies writing and reasoning about Asynchronous code. Worry less about callbacks, promises, async, await, etc.
- Atomic Design, function composition, and Pipes encourages maximum code re-use, testability and the ability to compose smaller functions into larger ones.
- Compatibility with ECMAScript gives our applications full access to the JavaScript ecosystem. It also allows us to import elements from MojiScript into existing JavaScript applications.
- A modular design allows for features to be imported on an as needed basis, keeping packages small.
- Plays well with functional libraries. Check out the Complementary Libraries section for libraries that can benefit your MojiScript applications.
Quickstart
Clone the 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
If everything works, you should see:
Hello World
If your editor does not format on save, you can run the following command:
npm run watch
Style Guide
All values must be declared with const
.
// BAD
let value = {}
// GOOD
const value = {}
Variables should be named in lower camel case.
// BAD
const AddNumbers = x => y => x + y
// GOOD
const addNumbers = x => y => x + y
Expressions or Pipes and their arguments should be separated with a space. Arguments should be surrounded with parentheses. Further discussed at #438.
// BAD
add(1)(2)
// GOOD
add (1) (2)
Prefer String Templates
// BAD
const func = x => `Value: ${x}`
// GOOD
const func = $`Value: ${0}`
// BAD
const func = ({ prop }) => `Prop: ${prop}`
// GOOD
const func = $`Prop: ${'prop'}`
Following Atomic Design principles, code should be broken down into Atoms. This maximizes reusability, testability, composability, and readability.
// BAD
const getOrdersText = ifElse (({ length }) => length > 0) ($`${0} orders`) ($`No Orders`)
// GOOD
const hasOrders = ({ length }) => length > 0
const getOrdersText = ifElse (hasOrders) ($`${0} orders`) ($`No Orders`)
// GOOD
const hasOrders = ({ length }) => length > 0
const ifHasOrders = ifElse (hasOrders)
const getOrdersText = ifHasOrders ($`${0} orders`) ($`No Orders`)
ifElse
and the condition should be on the same line. Longer statements can be broken out into multiple lines. If it is long, consider breaking it down further.
// BAD
ifElse
(lessThan0)
(Math.abs)
(Math.sqrt)
// GOOD
ifElse (lessThan0) (Math.abs) (Math.sqrt)
// GOOD
ifElse (lessThan0)
(Math.abs)
(Math.sqrt)
Pipes must be multi-line.
// BAD
const main = pipe ([ add ])
// GOOD
const main = pipe ([
add
])
Arrays must have a space after the opening bracket and before the closing bracket.
// BAD
const array = [1, 2, 3]
// GOOD
const array = [ 1, 2, 3 ]
No semi-colons.
// BAD
const value = 888;
// GOOD
const value = 888
Complementary Libraries
- Sanctuary - Recommended collection of useful functions.
- Ramda - Another recommended collection of useful functions.
- List - π An immutable list with unmatched performance and a comprehensive functional API. (use
list/curried
)
Variables
Variables are constant.
// constant
const path = './data'
path = './hello'
//=> Error("Path is read-only")
Objects declared with const
are now also immutable!
const state = {
// mutable
count: 0
}
state.count = 1
//=> error Unallowed reassignment fp/no-mutation
A variable can be a value (Number, String, Object), an Expression, or a Pipe.
Objects
Objects are plain data objects.
const cat = {
name: 'mojo',
dob: Date.parse('July 14, 2009'),
weight: 14
}
note: Objects may contain functions, but those functions will not have a reference to the object itself. Behavior and data should be decoupled.
String Templates
String Templates make strings a joy to work with.
import $ from 'mojiscript/string/template'
const searchTemplate = $`Searching for: "${0}"`
const nameTemplate = $`${'first'} ${'last'}`
searchTemplate ('Skywalker') //=> 'Searching for: "Skywalker"'
nameTemplate ({ first: 'Luke', last: 'Skywalker' }) //=> 'Luke Skywalker'
Have a look at the Axios Example for more on how String Templates can improve your code.
Expressions
Expressions can be compared to a synchronous function that takes 1 argument and returns 1 argument.
const increase = x => x + 1
Multiple Arguments
Multiple arguments can be simulated a couple different ways.
Currying and Closures
const add = x => y => x + y
add (3) (4) //=> 7
Objects
const add = ({ x, y }) => x + y
add ({ x: 3, y: 4 }) //=> 7
Arrays
const add = ([x, y]) => x + y
add ([3, 4]) //=> 7
Compound Expressions
Compound expressions combine multiple expressions. The last expression will return the value of the Compound Expression. This is typically done to handle side effects.
const tap = func => value => (
func(value),
value
)
Pipes
Pipes can be compared to an asynchronous function that takes 1 argument and returns 1 argument.
Each pipe can contain multiple Pipes or Expressions. A Pipe will return the result of the final Pipe or Expression.
// increase :: Number -> Number
const increase = pipe ([
x => x + 1
])
increase (1) //=> 2
Pipes are a stream of data
A Pipe should be viewed as a stream of data, that performs Morphisms (or transformation between Categories) along each step.
import pipe from 'mojiscript/core/pipe'
import run from 'mojiscript/core/run'
import log from 'mojiscript/console/log'
const state = 4
const main = pipe ([
// |
// | 4
// βΌ
/*-------------------*/
/**/ x => x + 5, /**/
/*-------------------*/
// |
// | 9
// βΌ
/*-------------------*/
/**/ x => x * 2, /**/
/*-------------------*/
// |
// | 18
// βΌ
/*-------------------*/
/**/ log, /**/
/*-------------------*/
// |
// | 18
// βΌ
])
run ({ state, main }) //=> 18
Multiple arguments
Multiple arguments can be simulated a couple different ways.
Currying and Closures
// add :: Number -> Number -> Number
const add = x => pipe ([
y => x + y
])
add (3) (4) //=> 7
Objects
const add = pipe ([
({ x, y }) => x + y
])
add ({ x: 3, y: 4 }) //=> 7
Arrays
const add = pipe ([
([x, y]) => x + y
])
add ([3, 4]) //=> 7
Partial Application
Because Multiple Argument Pipes are curried, it is easy to create new functions with Partial Application.
// add :: Number -> Number -> Number
const add = x => pipe ([
y => x + y
])
// increase :: Number -> Number
const increase = add (1)
increase (4) //=> 5
Composing Pipes
Multiple Pipes can be Composed (combined) to create a new Pipe.
// increase :: Number -> Number
const increase = pipe ([
x => x + 1
])
// double :: Number -> Number
const double = pipe ([
x => x * 2
])
// increaseThenDouble :: Number -> Number
const increaseThenDouble = pipe ([
increase,
double
])
Pipes are Asynchronous
Pipes are Asynchronous. The elimination of synchronous statements greatly simplifies the code. No need for Promise
, async
, or await
!
import log from 'mojiscript/console/log'
import pipe from 'mojiscript/core/pipe'
import run from 'mojiscript/core/run'
import sleep from 'mojiscript/threading/sleep'
const state = 4
// increase :: Number -> Number
const increase = x => x + 1
// double :: Number -> Number
const double = x => x * 2
const main = pipe ([
log,
sleep (1000),
increase,
double,
log
])
run ({ state, main })
//=> 4
//=> (pause for 1 second)
//=> 10
Note: There are not any problems with synchronous or asynchronous code. Though there are complexities when you mix asynchronous code with synchronous code.
Conditionals
Example 1: if/else conditional
import log from 'mojiscript/console/log'
import ifElse from 'mojiscript/logic/ifElse'
import pipe from 'mojiscript/core/pipe'
import run from 'mojiscript/core/run'
import $ from 'mojiscript/string/template'
const dependencies = {
log
}
const state = 7
// isEven :: Number -> Boolean
const isEven = x => x % 2 === 0
// yesOrNo :: Boolean -> String
const yesIfEven = ifElse (isEven) ($`Yes, ${0} is even.`) ($`NO, ${0} is not even.`)
// main :: Number -> String
const main = ({ log }) => pipe ([
yesIfEven,
log
])
run({ dependencies, state, main }) //=> 'NO, 7 is not even.'
Example 2: switch case
import logF from 'mojiscript/console/logF'
import cond from 'mojiscript/logic/cond'
import pipe from 'mojiscript/core/pipe'
import run from 'mojiscript/core/run'
const dependencies = {
logF
}
const state = new Date().getDay()
const dayName = cond ([
[ 0, 'Sunday' ],
[ 1, 'Monday' ],
[ 2, 'Tuesday' ],
[ 3, 'Wednesday' ],
[ 4, 'Thursday' ],
[ 5, 'Friday' ],
[ 6, 'Saturday' ]
])
const main = ({ logF }) => pipe ([
dayName,
logF(day => `Today is ${day}.`)
])
run({ dependencies, state, main }) //=> 'Friday'
Example 3: if/else/elseif
import log from 'mojiscript/console/log'
import cond from 'mojiscript/logic/cond'
import pipe from 'mojiscript/core/pipe'
import run from 'mojiscript/core/run'
import $ from 'mojiscript/string/template'
const dependencies = {
log
}
const state = 100
// getTempInfo :: Number -> String
const getTempInfo = cond ([
[ 0, 'water freezes at 0Β°C' ],
[ 100, 'water boils at 100Β°C' ],
[ () => true, $`nothing special happens at ${0}Β°C` ]
])
const main = ({ log }) => pipe ([
getTempInfo,
log
])
run({ dependencies, state, main }) //=> 'water boils at 100Β°C'
Error Handling
import ifError from 'mojiscript/logic/ifError'
Synchronous Error Handling
const fail = err => ({ err })
const pass = value => ({ value })
const func = ifError (maybeThrowError) (fail) (pass)
func ('fail') //=> { err: [Error: Oops!] }
func (1) //=> { value: 1 }
Asynchronous Error Handling
Asynchronous error handling is no different than Synchronous error handling.
const func = ifError (maybeThrowErrorAsync) (fail) (pass)
func ('fail') //=> then { err: [Error: Oops!] }
func (1) //=> then { value: 1 }
Error Handling with Sanctuary
Sanctuary makes error handling easy with the Either type.
const func = ifError (maybeThrowError) (S.Left) (S.Right)
func ('fail') //=> Left (new Error ("Oops!"))
func (1) //=> Right (1)
Morphisms
Morphisms allow us to transform an object from one Category to another. The power of Morphisms can be seen when they are combined to create more complex Morphisms.
Here's a high level example of a Morphism that take an input as a String
and returns data from an API endpoint.
// urlToJson :: Url -> Json
const urlToJson = pipe ([
urlToAjaxResponse,
ajaxResponseToJson,
])
// queryToCustomer :: Query -> Customer
const queryToCustomer = pipe ([
queryToUrl,
urlToJson,
jsonToCustomer
])
Application Layout
app/
βββ dist/
βββ src/
βββ __tests__/
β βββ main.test.js
βββ index.js
βββ main.js
dist/
- Bundled or Babeled output folder.src/
- It is recommended to create ansrc/
folder when your source requires transpiling with something like Webpack or Babel.__tests__/
- Testing is not optional. Keep tests close to your code.index.js
- Entrypoint to your application. This will load all dependencies as well as executemain
. Can be excluded from tests.main.js
- Decoupling from dependecies and start allows for easy testing.
Unit Tests
Check out the map/filter/reduce example for an example on how to write unit tests.
Previous Art
Many of these concepts are not new. These are some of the projects that have previously existed and influenced MojiScript.
-
Ramda - A practical functional library for JavaScript programmers.
-
Sanctuary - Refuge from unsafe JavaScript.
-
Fantasy Land - Specification for interoperability of common algebraic structures in JavaScript
-
Folktale - A standard library for functional programming in JavaScript
Contributors
Thanks goes to these wonderful people (emoji key):
<!-- ALL-CONTRIBUTORS-LIST:START - Do not remove or modify this section --> <!-- prettier-ignore -->This project follows the all-contributors specification. Contributions of any kind welcome!