In talking to a lot of Javascript developers over the course of the last few months, I learnt that most hated the limits placed on them by javascript. I found myself cursing every time I needed to do some very basic tasks. This project is an attempt to find out what we could be doing instead of fighting hard against javascript.
The source and the changelog can be found at https://github.com/rahulsom/jsalt/
The problems
-
Javascript pretends to be a functional language
-
Javascript pretends to be an object oriented language
-
Javascript was introduced in 1995, and has never broken backwards compatibility.
-
Javascript has optional no typing.
-
Newer versions of the spec aren’t fully supported by browsers.
The use cases
-
Calling utility methods that accept functions as arguments
-
Creating "class"es and accessing static and instance methods
-
Dealing with some javascript eccentricities
-
Managing types
The format
You’ll see code that is picked from tested examples over here. There will be callouts that point out interesting things. Some are positive, some are negative, while others are just plain interesting.
The Contenders
Classic Javascript (ES 5)
'use strict'; (1)
/**
* A factory that offers factorial methods
*/
var TaylorClassic = (function() { (2)
function TaylorClassic() {
} (3)
/**
* Computes the p-th power of num
* @param {number} num the number
* @param {int} p the number
* @return {number} the power
*/
TaylorClassic.prototype.pow = function(num, p) { (4)
if (p === 0) {
return 1;
} else if (p > 0) {
return num * this.pow(num, p - 1);
} else {
return 1 / num * this.pow(num, p + 1);
}
};
/**
* Computes the factorial of a number
* @param {int} x the number (5)
* @return {int} the factorial
*/
TaylorClassic.prototype.fact = function fact(x) {
return x <= 0 ? 1 : x * this.fact(x - 1);
};
/**
* Computes the sine of an angle
* @param {number} x the angle
* @return {number} the sine
*/
TaylorClassic.prototype.sine = function(x) {
var _this = this; (6)
return _.reduce(_.range(0, 10), function(sum, i) { (7) (8)
return sum + _this.pow(-1, i) * _this.pow(x, i * 2 + 1) / _this.fact(i * 2 + 1); (9)
}, 0.0);
};
/**
* Returns a singleton of the TaylorClassic
* @return {TaylorClassic}
*/
TaylorClassic.getInstance = function getInstance() { (10)
if (!this._instance) { (11)
this._instance = new TaylorClassic();
}
return this._instance;
};
return TaylorClassic; (12)
})();
1 | This is a tip of the hat to the weird history of javascript and the reluctance to do away with bad implementations. As we’ll later see, this is not as strict as we want it to be. |
2 | This is a secret handshake between the developer and the browser that this is a class . |
3 | This is the constructor, and it does nothing, but it can be modified in several steps below. |
4 | The prototype feels like you’re mucking with a meta object. Indeed, that’s how javascript wants you to deal with
classes. |
5 | As you’ve probably noticed by now, there are no types to parameters or return values. Javascript does not impose any restrictions inspite of having classes we’ve seen earlier. The solution to getting type information in the IDE is use of JSDoc. You might be tempted to think there is Haskell style type inference. Do not fall for that trap. Things can go from bad to worse pretty soon once a method is called incorrectly. |
6 | There is no sane definition of this since classes are just functions.
So what this means depends on where it is used. In this case, it’s an instance of FactorialUtilClassic .
An experienced javascript developer would know this by instinct, but still fall for it’s traps. |
7 | Here we’re using lodash or underscore or some library that offers a functional style of programming. |
8 | This is the boilerplate required to call reduce |
9 | If you forget the return keyword, the function is assumed to return void . There is no warning. |
10 | This is a static function. The clue is, there’s no prototype . |
11 | this means the class over here. |
12 | We return the function from <3> over here. That is the instance returned to the call new FactorialUtilClassic . |
describe('Taylor Classic Spec', function() {
return it('calls, values and instances match', function() {
var taylor = TaylorClassic.getInstance();
var PI = 3.14159265359;
expect(taylor.pow(2, -2)).toBe(0.25);
expect(taylor.pow(2, -1)).toBe(0.5);
expect(taylor.pow(2, 0)).toBe(1);
expect(taylor.pow(2, 1)).toBe(2);
expect(taylor.pow(2, 2)).toBe(4);
expect(taylor.fact(0)).toBe(1);
expect(taylor.fact(1)).toBe(1);
expect(taylor.fact(2)).toBe(2);
expect(taylor.fact(3)).toBe(6);
expect(taylor.sine(0)).toBeCloseTo(0.0, 3);
expect(taylor.sine(PI / 4)).toBeCloseTo(0.707, 3);
expect(taylor.sine(PI / 3)).toBeCloseTo(0.866, 3);
expect(taylor.sine(PI / 2)).toBeCloseTo(1.0, 3);
expect(taylor.sine(PI)).toBeCloseTo(0.0, 3);
});
});
Modern Javascript (ES 6)
/**
* A factory that offers factorial methods
*/
class TaylorModern { (1)
/**
* Computes the p-th power of num
* @param {number} num the number
* @param {int} p the number
* @return {number} the power
*/
pow(num, p) {
if (p == 0) return 1;
else if (p > 0) return num * this.pow(num, p - 1);
else return 1 / num * this.pow(num, p + 1);
}
/**
* Computes the factorial of a number
* @param {int} x the number
* @return {int} the factorial
*/
fact(x) {
return x <= 0 ? 1 : x * this.fact(x - 1);
}
/**
* Computes the sine of an angle
* @param {number} x the angle
* @return {number} the sine
*/
sine(x) {
return _
.chain(_.range(0, 10))
.reduce((sum, i) => sum + this.pow(-1, i) * this.pow(x, i * 2 + 1) / this.fact(i * 2 + 1), 0.0); (2)
}
constructor() {
this._instance = null; (3)
}
/**
* Returns a singleton of the TaylorModern
* @return {TaylorModern}
*/
static getInstance() {
if (!this._instance) { (4)
this._instance = new TaylorModern();
}
return this._instance;
}
}
1 | There is native support for classes. |
2 | Arrow functions look a lot like Lambda Calculus, and have a saner implementation of this |
3 | There is support for static members in classes. |
4 | You still use this inside static members. |
describe('Taylor Modern Spec', function() {
return it('calls, values and instances match', function() {
var taylor = TaylorModern.getInstance();
var PI = 3.14159265359;
expect(taylor.pow(2, -2)).toBe(0.25);
expect(taylor.pow(2, -1)).toBe(0.5);
expect(taylor.pow(2, 0)).toBe(1);
expect(taylor.pow(2, 1)).toBe(2);
expect(taylor.pow(2, 2)).toBe(4);
expect(taylor.fact(0)).toBe(1);
expect(taylor.fact(1)).toBe(1);
expect(taylor.fact(2)).toBe(2);
expect(taylor.fact(3)).toBe(6);
expect(taylor.sine(0)).toBeCloseTo(0.0, 3);
expect(taylor.sine(PI / 4)).toBeCloseTo(0.707, 3);
expect(taylor.sine(PI / 3)).toBeCloseTo(0.866, 3);
expect(taylor.sine(PI / 2)).toBeCloseTo(1.0, 3);
expect(taylor.sine(PI)).toBeCloseTo(0.0, 3);
});
});
Coffeescript
'use strict' (1)
###* (2)
# A factory that offers factorial methods
###
class TaylorCoffee (3)
###*
# Computes the p-th power of num
# @param num {number} the number
# @param p {int} the number
# @return {number} the power
###
pow: (num, p) ->
if p == 0 then 1
else if p > 0 then num * @pow(num, p - 1)
else 1/num * @pow(num, p+1)
###*
# Computes the factorial of a number
# @param x {int} the number
# @return {int} the factorial
###
fact: (x) -> (4)
if x <= 0 then 1 else x * @fact(x - 1) (5)
###*
# Computes the sine of an angle
# @param x {number} the angle
# @return {number} the sine
###
sine: (x) ->
_this = @ (6)
reducer = (sum, i) ->
sum +
_this.pow(-1, i) * _this.pow(x, i * 2 + 1) / _this.fact(i * 2 + 1) (7)
_.reduce(_.range(0, 10), reducer, 0.0) (8)
###*
# Returns a singleton of the FactorialUtil
# @return {FactorialUtil}
###
@getInstance = ->
if !@_instance
@_instance = new TaylorCoffee
@_instance (9)
window.TaylorCoffee = TaylorCoffee (9)
1 | This is based on ES5, so you still need to enforce strict mode |
2 | It is based on Ruby and Python so indents matter and comments are # based |
3 | You have native classes |
4 | This is a function defined as a property, which in case of class is a public method |
5 | It is not immediately clear in this case, but if is not a statement, but an expression in coffeescript. |
6 | @ is used to mapped to this or this. in javascript. Makes for concise code more than you would think. |
7 | There is still a lambda-esque function here, but coffeescript functions always remind you of lambdas. |
8 | The last statement is always a return |
9 | Depending on the implementation of Coffeescript, the whole function could be inside of an anonymous function or not. So if you want to export the class, you better explicitly do that. |
describe 'Taylor Coffee Spec', ->
it 'calls, values and instances match', ->
taylor = TaylorCoffee.getInstance()
PI = 3.14159265359
expect(taylor.pow(2, -2)).toBe 0.25
expect(taylor.pow(2, -1)).toBe 0.5
expect(taylor.pow(2, 0)).toBe 1
expect(taylor.pow(2, 1)).toBe 2
expect(taylor.pow(2, 2)).toBe 4
expect(taylor.fact(0)).toBe 1
expect(taylor.fact(1)).toBe 1
expect(taylor.fact(2)).toBe 2
expect(taylor.fact(3)).toBe 6
expect(taylor.sine(0)).toBe 0.0
expect(taylor.sine(PI/4)).toBeCloseTo 0.707, 3
expect(taylor.sine(PI/3)).toBeCloseTo 0.866, 3
expect(taylor.sine(PI/2)).toBeCloseTo 1.0, 3
expect(taylor.sine(PI)).toBeCloseTo 0.0, 3
The test is perhaps where coffeescript excels in demonstrating it’s conciseness.
Grooscript
package jsalt.groovy
import groovy.transform.CompileStatic
import groovy.transform.Memoized
import groovy.transform.TypeChecked
/**
* Utility to work with factorials
*/
@CompileStatic (1)
@TypeChecked
class TaylorGroovy {
double pow(double num, int p) { (3)
if (p == 0) 1
else if (p > 0) num * pow(num, p - 1) (4)
else 1 / num * pow(num, p + 1)
}
long fact(long x) {
x > 0 ? x * fact(x - 1) : 1
}
double sine(double x) {
(0..10).inject(0.0) { sum, i ->
sum + pow(-1, i) * pow(x, i * 2 + 1) / fact(i * 2 + 1)
} as double
}
static TaylorGroovy instance = null (2)
/**
* A poor man's singleton. This is not thread safe.
* But the joke is on threads, because javascript has only one thread
*
* @return the singleton
*/
static TaylorGroovy getInstance() { (5)
if (!instance) {
instance = new TaylorGroovy()
}
instance
}
}
1 | Within Groovy code, you can use Static compilation and type checking |
2 | You can have typed variables |
3 | Typed functions |
4 | There is no this scattered in a confusing way. But the implementation of this is as sane as groovy . |
5 | Some ASTs like @Singleton are not allowed |
package jsalt.groovy
import spock.lang.Specification
class TaylorGroovySpec extends Specification { (1)
public static final BigDecimal PI = 3.14159265359
def "calls, values and instances match"() {
def taylor = TaylorGroovy.instance
expect:
taylor.pow(2, -2) == 0.25 (2)
taylor.pow(2, -1) == 0.5
taylor.pow(2, 0) == 1
taylor.pow(2, 1) == 2
taylor.pow(2, 2) == 4
taylor.fact(0) == 1
taylor.fact(1) == 1
taylor.fact(2) == 2
taylor.fact(3) == 6
taylor.sine(0) == 0.0
taylor.sine(PI / 4) > 0.707 && taylor.sine(PI / 4) < 0.708 (3)
taylor.sine(PI / 3) > 0.866 && taylor.sine(PI / 3) < 0.867
taylor.sine(PI / 2) > 0.999 && taylor.sine(PI / 2) < 1.001
taylor.sine(PI) > -0.001 && taylor.sine(PI) < 0.001
}
}
1 | We are using Spock to test the code |
2 | The assertions are much nicer than jasmine |
3 | Jasmine’s toBeCloseTo is missed |
scala.js
import scala.scalajs.js.annotation.JSExport
class TaylorScala {
def fact(x: Long): Long = if (x > 0) x * fact(x - 1) else 1 (1) (2) (3)
def pow(num: Double, p: Int): Double = { (4)
if (p == 0) 1
else if (p > 0) num * pow(num, p - 1)
else 1 / num * pow(num, p + 1)
}
def sine(x: Double) = Range(0, 10) (5)
.foldLeft(0.0)(
(sum, i) => sum + pow(-1, i) * pow(x, i * 2 + 1) / fact(i * 2 + 1)
)
}
@JSExport (6)
object TaylorScala {
private lazy val instance = new TaylorScala
def getInstance = instance (7)
}
1 | if is an expression, not a statement. |
2 | Also function bodies without {} are common. |
3 | Since we’re using recursion, we need to be very clear about the signature inspite of advanced type inference. |
4 | This is a method body with {} . |
5 | Also Scala infers the return of this method. It is not Object . |
6 | ScalaJs is very aggressive in optimizing generated javascript. We need to tell it what we will be using from
outside unless we have a JSApp with a main method. |
7 | More demo of advanced type inference. |
import org.junit.runner.RunWith
import org.scalatest._
import org.scalatest.junit.JUnitRunner
// import scala.collection.immutable.List (1)
@RunWith(classOf[JUnitRunner]) (2)
class TaylorScalaSpec extends FunSuite {
final val PI = 3.14159265359
test("calls, values and instances match") {
val taylor = TaylorScala.getInstance
assert(taylor.pow(2, -2) == 0.25) (3)
assert(taylor.pow(2, -1) == 0.5)
assert(taylor.pow(2, 0) == 1)
assert(taylor.pow(2, 1) == 2)
assert(taylor.pow(2, 2) == 4)
assert(taylor.fact(0) == 1)
assert(taylor.fact(1) == 1)
assert(taylor.fact(2) == 2)
assert(taylor.fact(3) == 6)
assert(taylor.sine(0) == 0.0)
assert(taylor.sine(PI / 4) > 0.707)
assert(taylor.sine(PI / 4) < 0.708)
assert(taylor.sine(PI / 3) > 0.866)
assert(taylor.sine(PI / 3) < 0.867)
assert(taylor.sine(PI / 2) > 0.999)
assert(taylor.sine(PI / 2) < 1.001)
assert(taylor.sine(PI) > -0.001)
assert(taylor.sine(PI) < 0.001)
}
}
1 | This is commented because it is implicit, but I’ve put it there to show by default scala collections are immutable. |
2 | We need this because we’re trying to play well with the other test written in Spock. |
3 | Assertions are almost as elegant as Spock. |
Purescript
module TaylorPurescript where
import Prelude
import Data.Foldable (foldl)
import Data.Array ((..))
import Data.Int (toNumber)
import Control.Monad.Eff.Console (log)
--powImpl :: Number -> Int -> Number -> Number (5)
--powImpl num p accum
-- | p == 0 = accum
-- | p > 0 = powImpl num (p-1) (accum * num)
-- | otherwise = powImpl num (p+1) (accum / num)
--
---- pow :: Number -> Int -> Number
--pow num p = powImpl num p 1.0
--
--factImpl :: Int -> Number -> Number
--factImpl n accum
-- | n == 0 = accum
-- | n > 0 = factImpl (n-1) (accum * toNumber n)
--
---- fact :: Int -> Number
--fact n = factImpl n 1.0
--
-- pow :: Number -> Int -> Number (1)
pow num p (2)
| p == 0 = 1.0
| p > 0 = num * pow num (p-1)
| otherwise = 1.0/num * pow num (p+1)
-- fact :: Int -> Number
fact 0 = 1.0 (3)
fact n = toNumber (n) * fact(n-1)
-- sine :: Number -> Number
sine x = foldl (+) 0.0 $ map f (0 .. 10) where
f i = pow (-1.0) i * pow x (i*2+1) / fact (i*2+1)
main = log "Hello" (4)
1 | This line is commented out, but conventionally it would be there. It demonstrates Purescript’s type inference. Purely on the basis of the operations performed inside of it, it will guess exactly the same type as I had intended. |
2 | This is a function defined using guard values |
3 | This is a function defined using pattern matching |
4 | This appears necessary for the module to be taken seriously by the compiler |
5 | The commented block is better performing code. It uses tail call optimization. If you need it, you should use it. TCO is almost a must-have if you want to use quickcheck |
module Test.Main where
import TaylorPurescript
import Math (abs)
import Prelude
import Control.Monad.Eff
import Control.Monad.Eff.Console
import Data.Int (toNumber, round)
import Test.Unit (test, runTest)
import Test.Unit.QuickCheck (quickCheck)
import Test.QuickCheck (Result(), (===))
import Test.Unit.Assert as Assert
-- pi :: Number
pi = 3.14159265359
--powersCascade :: Number -> Int -> Result (1)
--powersCascade a b = (pow a (b+1)) === ((pow a b)*a)
--
--factsCascade :: Int -> Result
--factsCascade a = (fact (b+1)) === (fact b * toNumber(b+1)) where
-- b = round $ abs $ toNumber a
--
main = runTest do
-- test "quickCheck pow function" do
-- quickCheck powersCascade (2)
--
-- test "quickCheck fact function" do
-- quickCheck factsCascade
--
test "the pow function" do
Assert.equal (pow 2.0 (-2)) 0.25 (3)
Assert.equal (pow 2.0 (-1)) 0.5
Assert.equal (pow 2.0 0) 1.0
Assert.equal (pow 2.0 1) 2.0
Assert.equal (pow 2.0 2) 4.0
test "the fact function" do
Assert.equal (fact 0) 1.0
Assert.equal (fact 1) 1.0
Assert.equal (fact 2) 2.0
Assert.equal (fact 3) 6.0
test "the sine function" do
Assert.equal (sine 0.0) 0.0
Assert.assert "sin pi/4 ~= 0.707" $ (sine (pi/4.0)) > 0.707
Assert.assert "sin pi/4 ~= 0.707" $ (sine (pi/4.0)) < 0.708
Assert.assert "sin pi/3 ~= 0.866" $ (sine (pi/3.0)) > 0.866
Assert.assert "sin pi/3 ~= 0.866" $ (sine (pi/3.0)) < 0.869
Assert.assert "sin pi/2 ~= 0.707" $ (sine (pi/2.0)) > 0.999
Assert.assert "sin pi/2 ~= 0.707" $ (sine (pi/2.0)) < 1.001
1 | This defines a property for quickCheck |
2 | This is quickCheck - it runs a hundred tests with random numbers to see if the property holds true |
3 | You need ot put negative numbers into parens. It’s a haskell thing. |
However purescript doesn’t generate JUnit style xml output. So we’ve gone ahead and put in some jasmine tests as well
describe('Taylor Purescript Spec', function () {
return it('calls, values and instances match', function () {
var taylor = PS.TaylorPurescript; (1)
var PI = 3.14159265359;
expect(taylor.pow(2)(-2)).toBe(0.25); (2)
expect(taylor.pow(2)(-1)).toBe(0.5);
expect(taylor.pow(2)(0)).toBe(1);
expect(taylor.pow(2)(1)).toBe(2);
expect(taylor.pow(2)(2)).toBe(4);
expect(taylor.fact(0)).toBe(1);
expect(taylor.fact(1)).toBe(1);
expect(taylor.fact(2)).toBe(2);
expect(taylor.fact(3)).toBe(6);
expect(taylor.sine(0)).toBeCloseTo(0.0, 3);
expect(taylor.sine(PI / 4)).toBeCloseTo(0.707, 3);
expect(taylor.sine(PI / 3)).toBeCloseTo(0.866, 3);
expect(taylor.sine(PI / 2)).toBeCloseTo(1.0, 3);
expect(taylor.sine(PI)).toBeCloseTo(0.0, 3);
});
});
1 | This is how you pull in Purescript into native javascript. There is the option to use CommonJS or whatever else is your favorite module system for JS, but we’re keeping it simple. |
2 | This is where you blame your favorite mathematician - Gottlob Frege, Moses Schönfinkel, or Haskell Curry. It is what makes sense to mathematics. And it’s ugly when we take a programming language where Currying is not a first class capability. |
Clojurescript
(ns jsalt.core)
(defn ^:export fact [x] ; (1)
(if (<= x 1) 1 ; (2)
(* x (fact (- x 1)))))
(defn ^:export pow [x n]
(if (zero? n) 1
(if (< n 0)
(/ (pow x (inc n)) x)
(* (pow x (dec n)) x)
)))
(defn term [x i]
(/
(* (pow -1 i) (pow x (+ 1 (* i 2))))
(fact (+ 1 (* i 2)))))
(defn ^:export sine [x]
(reduce +
(map (partial term x) (range 10)))); (3)
1 | The ^:export is required to ensure the name remains the same |
2 | Everything is in prefix notation. No infix. |
3 | Clojure has partial functions. Not currying. |
Typescript
/// <reference path="../typings/lodash/lodash.d.ts" /> (1)
/**
* A factory that offers factorial methods
*/
class TaylorTypescript {
pow(num:number, p:number):number { (2)
if (p == 0) return 1;
else if (p > 0) return num * this.pow(num, p - 1);
else return 1 / num * this.pow(num, p + 1);
}
fact(x:number):number {
return x <= 0 ? 1 : x * this.fact(x - 1);
}
sine(x:number):number {
/*
return _.chain(_.range(0, 10))
.reduce((sum, i) => sum + this.pow(-1, i) *
this.pow(x, i * 2 + 1) / this.fact(i * 2 + 1), 0.0)
.value(); (3)
*/
return _.reduce(
_.range(0, 10),
(sum, i) => sum + this.pow(-1, i) *
this.pow(x, i * 2 + 1) / this.fact(i * 2 + 1),
0.0
);
}
static _instance:TaylorTypescript;
static getInstance() {
if (!this._instance) { (4)
this._instance = new TaylorTypescript();
}
return this._instance;
}
}
1 | This is how we tell typescript where to import things |
2 | Typing is optional. There is no int, so everything is a number. |
3 | Lodash chaining does not work. It’s a flaw in the d.ts for it. |
4 | The insane littering of this doesn’t go away. |
/// <reference path="TaylorTypescript.ts" />
/// <reference path="../typings/jasmine/jasmine.d.ts" />
describe('Taylor Typescript Spec', function() {
return it('calls, values and instances match', function() {
var taylor = TaylorTypescript.getInstance();
var PI = 3.14159265359;
expect(taylor.pow(2, -2)).toBe(0.25);
expect(taylor.pow(2, -1)).toBe(0.5);
expect(taylor.pow(2, 0)).toBe(1);
expect(taylor.pow(2, 1)).toBe(2);
expect(taylor.pow(2, 2)).toBe(4);
expect(taylor.fact(0)).toBe(1);
expect(taylor.fact(1)).toBe(1);
expect(taylor.fact(2)).toBe(2);
expect(taylor.fact(3)).toBe(6);
expect(taylor.sine(0)).toBeCloseTo(0.0, 3);
expect(taylor.sine(PI / 4)).toBeCloseTo(0.707, 3);
expect(taylor.sine(PI / 3)).toBeCloseTo(0.866, 3);
expect(taylor.sine(PI / 2)).toBeCloseTo(1.0, 3);
expect(taylor.sine(PI)).toBeCloseTo(0.0, 3);
});
});