GorillaScript

by ckknight

http://ckknight.github.io/gorillascript

This presentation: http://ckknight.github.io/gorillascript-presentation

What is a programming language?

Dictionary definition: a language designed for programming computers

My definition: a mechanism for humans to convey intent to a machine

Problems

  • Computers are terrible at comprehending intent.
  • Humans are terrible at conveying intent. (Primarily because we don't know what we want most of the time.)

I argue that nearly all problems that occur when writing computer programs are due to these two attributes, often working together to befuddle us.

Design philosophy

One of the big philosophies behind GorillaScript is that if something is tedious, then there should be a way to do things "the right way" without having to expend too much brainpower on it.

An good historical example of this is Garbage Collection. Before its wide-spread adoption, it was necessary to spend at least half of one's brainpower simply making sure that you've free'd or delete'd all your references. Once Garbage Collection comes into play, all the thought that would have gone into manual cleanup can be spent on producing a product.

GorillaScript was written with the Sapir-Whorf hypothesis in-mind, in that mental capabilities are limited by the ability or inability to articulate within the bounds of one's linguistic capabilities.

Humans have a finite number of thoughts that run through our minds per day. Thus, one of GorillaScript's main goals is to optimize human thought by relieving as much stress and ceremony of coding as possible while simultaneously enabling the writing of beautiful code.

Beautiful code

You might ask yourself what makes code beautiful, and I like to borrow from the 13th century priest and philosopher Thomas Aquinas.

  • Integrity: does my code do what I expect it to (and nothing I don't expect)?
    A building has integrity just like a man. And just as seldom. ~Ayn Rand
  • Clarity: is the code obvious in conveying its intent?
    Clarity is the counterbalance of profound thoughts. ~Luc de Clapiers
  • Proportionality: given the task I expect the code to do, is it proportional in size to what it's accomplishing?
    It is my ambition to say in ten sentences what others say in a whole book. ~Friedrich Nietzsche

Problems specific to JavaScript as-is

(and how GorillaScript tries to fix it)

Unstrict equality by default

1 == "1"
0 != ""
3 == "03"
[] == ""
[] == 0
[] == ![]
[] == false
[""] == 0
["0"] == 0
{toString: function() { return ""; }} == 0
null != false
null == undefined

In JavaScript, the solution is to always use ===. The only valid time to use == is when comparing against null

1 != "1"
3 != "03"
[] != ""
// etc

// check against null or undefined
null? // false
undefined? // false
0? // true
false? // true
true? // true

The + and += operators

What does the following code do?

a = b + c;

It depends, which is terrible.

1 + 2 === 3 // as expected
"hello, " + "world" === "hello, world" // as expected
"hello, " + 123 === "hello, 123" // sure, I can accept this
"1" + 2 === "12"
1 + "2" === "12"

// and for some oddities
false + false === 0
false + true === 1
true + true === 2
null + null === 0
isNaN(undefined + undefined)
[] + [] === ""
{} + {} === "[object Object][object Object]"
true + [] === "true"
new Date() + 1 === "Tue Jan 29 2013 20:25:58 GMT-0800 (PST)1" // or something like it
new Date() - 1 === 1359519958072 // or some other number
var foo = {
  toString: function () { return 5; }
  valueOf: function () { return "foo"; }
};
foo.toString() + 1 === 6
foo + 1 === "foo1"

Addition and String Concatenation as separate operators

1 + 2 == 3
"hello, " + "world" // TypeError
"hello, " + 123 === "hello, 123" // TypeError
"1" + 2 // TypeError
1 + "2" // TypeError

"hello, " & "world" == "hello, world"
"hello, " & 123 == "hello, 123"
"1" & 2 == "12"
1 & "2" == "12"
1 & 2 == "12" // even this one!

// or, an often easier way:

"hello, $(123)" == "hello, 123"
"1$(2)" == "12"
"$(1)2" == "12"
"$(1)$(2)" == "12"

Laissez-faire type coercion

Pretty much all JavaScript operators perform type coercion. The only ones that don't are the ones which simply evaluate to one of their operands, e.g. &&, ||, ? :

If you've ever seen text like "Total: undefined USD", you know this can be an issue.

GorillaScript solves this in a way pioneered by "use restrict", by performing type-checking on its operators

1 + {} // TypeError
1 * undefined // TypeError
[1, 2, 3] < [1, 2, 4] // TypeError

The idea is to catch simple type errors such as these as early as possible rather than having them bite you later.

"use strict" opt-in

function Point(x, y) {
  this.x = x;
  this.y = y;
}

var point = Point(1, 2); // congratulations, you've now polluted the global scope.

GorillaScript code is always in strict-mode.

Mutability as the default

At least until ECMAScript 6 reaches broad support (here's hoping for a bright future in the year 2020), all bindings are declared with var or function, both of which allow their bindings to be redefined without any hint of a warning. Even if you use const in Chrome, Safari, or Opera, it works identically to var.

GorillaScript takes the approach of immutable-by-default, with opt-in mutability if desired.

let x = 1
let mutable y = 2
x += 1 // compile-time error, `x` is immutable.
y += 1 // perfectly fine
z += 1 // compile-time error, `z` was never declared.

Easy-to-miss assignment

if (test = 1) {
  // well, test is definitely 1 now
} else {
  // this will never be hit
}

GorillaScript solves this by two ways:

  1. All assignments must be in the statement position, or wrapped in parentheses
  2. Assignment and declaration have different tokens. := vs. =

If you think := looks ugly, good. You should be avoiding mutability whenever possible.

For loops are easy to get wrong

JavaScript has two kinds of for loops, one for object iteration (for (k in obj) {}), and one C-style (for (;;) {}).

There is no built-in way to iterate over arrays, and it's easy to do it inefficiently if you code by-hand.

Even for the object iteration loop, one still needs to wrap the body in a hasOwnProperty check to get at the keys you actually want.

Even if you use the new functional methods of iteration like Array.prototype.forEach, there are still issues such as the loss of break, continue, and return as well as the unmitigatable context switching.

ECMAScript 6 proposes the for-of loop, and if we ever enter a world where we don't need to support old browsers, it will be amazing (probably not).

Also, due to JavaScript's lack of block scoping (all bindings are scoped to the function), it is easy to misuse a mutated variable within a closure.

Examples of bad looping, for-in

// didn't declare var k, global pollution
for (k in obj) {}
for (var k in obj) {
  // oops, getting this on all keys, not just owned keys
  doSomething(k);
}
for (var k in obj) {
  // what if obj.hasOwnProperty isn't Object.prototype.hasOwnProperty?
  if (obj.hasOwnProperty(k)) {
    doSomething(k);
  }
}
// looping over an array with for-in is wrong, but often typical
for (var item in array) { 
  doSomething(item);
}
for (var k in obj) {
  if (Object.prototype.hasOwnProperty.call(obj, k)) {
    doSomething(function () {
      // unless doSomething calls this synchronously, `k` will be the last key of `obj`
      return k;
    });
  }
}

Examples of bad looping over an array

// array.length is accessed every loop
for (var i = 0; i < array.length; ++i) {
  var item = array[i];
  doSomething(item);
}
for (var i = 0, len = array.length; i < len; ++i) {
  var item = array[i];
  doSomething(function () {
    // both `i` and `item` will be wrong unless doSomething calls this synchronously
    return item;
  });
}

GorillaScript loops

All loops in GorillaScript provide block-scoping, so closing over bindings is something you don't even need to worry about.

GorillaScript has five kinds of normal loops:

  1. while (and until): Works like a normal while, but you can also specify an increment so it acts like a for loop.
  2. for-in: Iterate over an array-like object, can easily loop backwards or with a custom step or through only a segment.
  3. for-of: Iterate over an object's keys and values, checking for ownership
  4. for-ofall: Iterate over an object's keys and values, not checking for ownership
  5. for-from: Iterate over a generator.

While/until

while test()
  do-something()
while test(), i += 1
  do-something()
until test()
  do-something()
until test(), i += 1
  do-something()

for-in (array-like iteration)

for item in get-array()
  do-something item
for item, index, length in get-array()
  do-something item
// let's go backwards
for item in get-array() by -1
  do-something item
// let's only get every other one
for item in get-array() by 2
  do-something item
// only some middle parts
for item in get-array()[5 to -5]
  do-something item
for item in get-array()
  // sure, pass a function in, `item` will point to the correct object.
  do-something #
    item

for-of (object iteration)

for key, value of get-object()
  do-something key, value
for key, value, index of get-object()
  do-something key, value
for key, value ofall get-object()
  do-something key, value

for-from (generator iteration)

for value from gimme-numbers()
  do-something value
for value, index from gimme-numbers()
  do-something value

Callback hell

When dealing with highly asynchronous back-and-forth IO, it is easy to get into a state affectionately known as "callback hell".

db.open(function (err, conn) {
  if (err) {
    handleError(err);
  } else {
    conn.send(someQuery, function (err, result) {
      if (err) {
        handleError(err);
      } else {
        conn.send(someOtherQuery, function (err, otherResult) {
          // etc.
        });
      }
    });
  }
});

Thankfully, this is mitigatable with libraries such as caolan/async, but it's even nicer when handled as first-class syntax.

GorillaScript has two ways of handling asynchronous workflows, usable based on your taste.

This code is equivalent to that on the previous slide:

async! handle-error, conn <- db.open()
async! handle-error, result <- conn.send some-query
async! handle-error, other-result <- conn.send some-other-query
// etc.

GorillaScript also has first-class Promises/A+ support, as well as tools to convert between node.js-style callbacks into nice Promises.

Ideally, this syntax does work much nicer if everything exposes its asynchronous results as Promises rather than simple callbacks.

One major benefit is that your asynchronous code looks practically identical to synchronous code, except that the yield expression ends up waiting on another Promise, allowing for extremely easy composability.

promise!
let conn = yield to-promise! db.open()
let result = yield to-promise! conn.send some-query
let other-result = yield to-promise! conn.send some-other-query
// etc.

Or say we want to do those last two queries at the same time:

promise!
let conn = yield to-promise! db.open()
let [result, other-result] = yield every-promise! [
  to-promise! conn.send some-query
  to-promise! conn.send some-other-query
]
// etc.

Shiny features

Compile-time constants

const DEBUG = true

if DEBUG
  do-some-expensive-check()

Indentation-based code blocks

Note: this is actually optional. You can turn it off and require end at the end of all your blocks, but I don't recommend it, as consistent indentation improves code clarity.

let x =
  something()
  if check
    for item in array
      item.value
  else
    something-else()

Everything is an expression (pretty much)

In pretty much all cases (except for explicit statements like break and continue), all nodes within GorillaScript can be used as an expression, even non-traditional ones such as debugger, throw, or any of the loops.

let x = something() or throw Error "oh noes!"
// `y` will be an array
let y = for item in array
  item.value

Number syntax

let time = 10_000_ms // commented numbers
let hex = 0x1234_5678 // hex numbers
let octal = 0o070 // octals use 0o instead of just 0
let binary = 0b1010010101 // binary numbers
let radix = 36rNFfdH45 // custom radixes from 2-36
let float = 123_456.789_012
let hex-float = 0x1234.5678 // all numbers can have fractions

String syntax

let normal = "hello"
let single = 'world'
let interpolation = "Hello, $single"
let multi-line = """
  $interpolation.
  
  I hope you're well today.
  """
let backslash = \hello-world // same as "helloWorld"

RegExp syntax

r"l".test "apple"
let regex = r"""
  This is a large regex, $name
  And all the space is ignored # and this is ignored, too!
  """gim

Function syntax

let single-line() "hello"
let multi-line(name)
  let upper-name = name.to-upper-case()
  "Hello, $upper-name"
f #(x) // anonymous function
  x + 1
let with-spread(start, ...middle, end)
  [start, ...middle, end]

let self = this
let bound-this()@
  this == self

let hello(name as String) // this will error if passed a non-String
  "Hello, $name"

let side-effects(name)! // prevent automatic return
  log(name)
  // returns undefined

Call syntax

f(0)
f 1
f ...array
f start, ...middle, end, ...as-many-of-these-as-you-want

f@ context, first-arg // same as f.call(context, first-arg)
f@ context, ...array // same as f.apply(context, array)

new f
new f item
new f(item)

Binding access

let obj = {
  f: # this
}

let bound = obj@.f
assert bound() == obj // same as doing obj.f.call(obj)
let unbound = obj.f
assert unbound() == window

Array syntax

let list = [1, 2, 3]
let other-list = [...list, 4, 5, 6] // now contains [1, 2, 3, 4, 5, 6]

// another way to specify an array
let items =
  * "Apples"
  * "Bananas"
  * "Cherries"

Object syntax

let obj = {
  list // same as list: list
  sum: 6
  f()
    "result" // same as f: # "result"
  +good // same as good: true
  -bad // same as bad: false
}

let great-apes =
  bonobos:
    awesomeness: "pretty cool"
    population: 40_000
  humans:
    awesomeness: "let's not say anything bad about these guys"
    population: 7_000_000_000
  gorillas:
    awesomeness: "clearly the best"
    population: 100_000

let special = {
  [1 + 2]: "three"
  "key$i": "interpolated key"
  class: "JavaScript would fail on the 'class' key."
}

let with-prototype = { extends special
  value: 1
}

Map syntax

let obj = {}
let other = {}
let map = %{
  [obj]: 1
  [other]: "hello"
}
assert map.get(obj) == 1
assert map.get(other) == "hello"
map.delete other
assert map.has obj
assert not map.has other
map.set other, "there"
assert map.has other
assert map.get(other) == "there"

Set syntax

let set = %[obj, other]
assert set.has(obj)
assert set.has(other)
set.delete(other)
assert not set.has(other)
set.add other
assert set.has(other)
set.add other // does nothing, already in the set.

Class syntax

Note: JavaScript does not have classes, so all class implementations are utter hacks. Here's GorillaScript's!

class Animal
  def constructor(@name) ->

  def eat() "$(@name) eats"

class GreatApe extends Animal
  // no constructor, Animal's is automatically called
  def eat(food="fruit") super.eat() & " a " & food

class Gorilla extends GreatApe
  def constructor(@name, @favorite-food)
    // important to call the super constructor.
    super(@name)

  def eat() super.eat(@favorite-food)

class Chimp extends GreatApe
  def eat() super.eat("banana")

let bobo = Chimp("Bobo") // new is not required on GorillaScript-made classes
assert bobo.eat() == "Bobo eats a banana"

let toko = Gorilla("Toko", "cherry")
assert toko.eat() == "Toko eats a cherry"

// set a method on the Gorilla prototype
Gorilla::barrel := # @name & " throws a barrel!"

assert toko.barrel() == "Toko throws a barrel!"

Loops with else

for item in array
  do-something item
else
  no-items()

For-some

let has-good = for some item in array
  item.is-good()

For-every

let all-good = for every item in array
  item.is-good()

For-reduce

let total = for reduce item in array, current = 0
  current + item.value

For-filter

let good-ones = for filter item in array
  item.is-good()

For-first

let found = for first item in array
  if item.is-good()
    item
else
  null

Array slicing

let array = [\a, \b, \c, \d]
let middle = array[1 til -1]
let reversed-middle = array[-2 til 0 by -1]

Array negative indexing

let array = [\a, \b, \c, \d]
let last = array[* - 1]

Cascades

let array = [\a, \b, \c, \d, \e]
  ..push \f
  ..reverse()
  ..sort()
document.query-selector \h1
  ..style
    ..color := \red
    ..font-size := "200%"
  ..inner-HTML := "Hello, world!"

Destructuring

let [x, y] = [1, 2]
assert x == 1
assert y == 2

let {a, b: c} = {a: 3, b: 4}
assert a == 3
assert c == 4

let [d, {e, f: [g]}] = get-data()

let [value, ...rest] = array

Switch

Switch is break-by-default, opt-in fallthrough, unlike JavaScript.

switch value
case 0, 1, 2
  "small"
case 3, 4, 5
  fallthrough // in the last position of the case, causes the case to fall through to the next case.
case 6, 7, 8
  "large"
default
  "unknown"

Topicless switch

switch
case is-good()
  "good"
case is-bad()
  "bad"
default
  "neutral"

Try

try
  something-dangerous()
catch e as SpecificError
  handle-error(e)
catch e
  uh-oh()
else
  whew()
finally
  cleanup()

Generators

let fib()*
  let mutable a = 0
  let mutable b = 1
  while true
    yield b
    let tmp = a
    a := b
    b += tmp

Promises

let make-promise = promise! #(filename)*
  // here, read-file returns a Promise
  let text = yield read-file(filename)
  text.to-upper-case()

let promise = make-promise()
  .then(on-success, on-failure)
let promise = promise!
  // here, read-file returns a Promise
  let text = yield read-file(filename)
  text.to-upper-case()

promise.then(on-success, on-failure)

Converting a promise to a node.js-style callback

let do-stuff(filename)
  let my-promise = promise!
    let p = to-promise! fs.read-file filename, "utf8"
    let text = yield p
    return text.to-upper-case()
  
  let node-func = from-promise! my-promise
  node-func #(err, value)
    if err?
      handle-error err
    else
      handle-success value

fulfilled! and rejected!

let promise = fulfilled! value
let bad-promise = rejected! reason

delay!

let take-a-while = promise! #*
  for i in 0 til 10
    calculate(i)
    yield delay! 100_ms

some-promise!

let read-file-or-timeout(filename)
  some-promise! [
    read-file filename
    delay! 1000_ms
  ]

every-promise!

let read-many-files(filenames)
  let file-promises = []
  for filename in filenames
    file-promises.push read-file filename
  every-promise! file-promises
let read-many-files(filenames)
  let file-promises = {}
  for filename in filenames
    file-promises[filename] := read-file filename
  every-promise! file-promises

promisefor

let loop = promisefor(3) filename in filenames
  let text = yield read-file filename
  return text.to-upper-case()

loop.then on-success, on-error

Optional typing

let increment(x as Number) x + 1
let greet(x as String|Number) "Hello, $x"
let run(x as -> Number) x()
let get-number() as Number -> num
let join(x as [String]) x.join ", "
let use-object(o as {x: Number, y: Number}) o.x + o.y

let x as Number = f()

Operators as functions

let add = (+) // same as #(x, y) x + y
assert add(5, 6) == 11

let square = (^ 2) // same as #(x) x ^ 2
assert square(10) == 100

let double = (2 *) // same as #(x) 2 * x
assert double(5) == 10

let invert = (not) // same as #(x) not x
assert invert(true) == false
assert invert(false) == true

assert 10 == [1, 2, 3, 4].reduce (+)

let get-length = (.length) // same as #(x) x.length
assert get-length("hello") == 5

let to-hex = (.to-string(16)) // same as #(x) x.to-string(16)
assert to-hex(255) == "ff"

Getters and setters and custom properties, oh my!

let obj =
  _x: 0
  get x: # @_x
  set x: #(value)! @_x := value

  _y: 0
  property y:
    get: # @_y
    set: #(value)! @_y := value
    configurable: true
    enumerable: true

Curried functions

let add(a, b, c)^
  a + b + c

assert add(1, 2, 3) == 6 // same as before
let add-one = add 1
assert add-one(2, 3) == 6
let add-two = add 2
assert add-two(1, 3) == 6
let add-one-and-two = add-one 2 // or add 1, 2
assert add-one-and-two(3) == 6
let sort-by(key, array)^
  array.sort #(a, b) a[key] <=> b[key]

let sort-by-id = sort-by \id
let sort-by-name = sort-by \name

let items =
  * id: 0
    name: "Dog"
  * id: 1
    name: "Car"
  * id: 2
    name: "Robot"
  * id: 3
    name: "Guitar"

let items-by-name = sort-by-name items
let items-by-id = sort-by-id items

Generics

func<String>("hello")
// turns into
func.generic(String)("hello")
class MyClass<T>
  def constructor(@value as T) ->
  
  def get-value() as T
    @value

let wrapped-string = MyClass<String>("hello")
let wrapped-number = MyClass<Number>(1234)
let wrapped-any = MyClass({})
let func<T>(value as T) value

assert func<String>("hello") == "hello"
assert func<Number>(1234) == 1234
assert func(true) == true // no type specified, so any type is allowed
assert func(null) == null

Build support

Command-line:

gorilla -jco lib/code.js src/one.gs src/two.gs src/three.gs --sourcemap

Grunt:

grunt.initConfig({
  gorilla: {
    dist: {
      options: {
        sourceMap: true
      }
      files: {
        "lib/code.js": ["src/one.gs", "src/two.gs", "src/three.gs"]
      }
    }
  }
});

Built-in coverage support

Hopefully there's enough time to show a quick demo.

Browser support

Assuming you don't use getters or setters, GorillaScript compiles to working ES3 code. If you do, then it will fail at runtime (rather than a JavaScript syntax error).

Also, GorillaScript has full support for Source Maps, so you'll be able to debug in the language you wrote in rather than the compiled JavaScript.

Macros

Unary operators

macro operator unary +?
  // the following idents are available:
  //   op (which will always be "+?" in this case)
  //   node (which will be the node to the right of this unary prefix macro)
  
  ASTE $node > 0

assert(+?500)
assert(not +?(-500))
assert(+?f())

Binary operators

macro operator binary union, ∪
  // the following idents are available:
  //   op (which will either be "union" or "∪", i.e. "\u222a")
  //   left (which will be the node to the left of the binary operator)
  //   right (which will be the node to the right of the binary operator)

  ASTE $left.union($right)

assert(alpha union bravo union charlie)
assert(alpha ∪ bravo ∪ charlie ∪ delta)

Assign operators

macro operator assign union=, ∪=
  // the following idents are available:
  //   op (which will either be "union=" or "∪=", i.e. "\u222a=")
  //   left (which will be the node to the left of the assign operator)
  //   right (which will be the node to the right of the assign operator)

  // here we need to potentially cache parts of the left-hand-side, since we're referencing it twice.
  // this will convert a()[b()] to tmp1[tmp2].
  @maybe-cache-access left, #(set-left, left)
    ASTE $set-left := $left.union($right)

let mutable alpha = some-set()
alpha union= bravo
alpha ∪= charlie
get-obj()[key()] union= delta()

Callable macros

macro get-or-add(cache, key, value)
  @maybe-cache cache, #(set-cache, cache)
    @maybe-cache key, #(set-key, key)
      let tmp = @tmp() // we need a temporary variable to store things into.
      AST
        let mutable $tmp = $set-cache[$set-key]
        if $tmp == void
          $cache[$key] := $tmp := $value
        $tmp

let some-cache = {}
let x = get-or-add some-cache, key, something-expensive()
let y = get-or-add some-cache, key, something-expensive() // something-expensive() is never executed

Custom-syntax macros

macro when
  syntax test as Logic, "then", body as Expression
    // the following idents are available:
    //   macro-name (which will always be "when" for this macro)
    //   test
    //   body
    ASTE if $test then $body

  syntax test as Logic, body as Body
    // same idents as before, since they are named the same.
    AST
      if $test
        $body

when is-hungry() or is-bored() then eat()

when is-angry()
  calm-down()

Q & A