by ckknight
http://ckknight.github.io/gorillascript
This presentation: http://ckknight.github.io/gorillascript-presentation
Dictionary definition: a language designed for programming computers
My definition: a mechanism for humans to convey intent to a machine
I argue that nearly all problems that occur when writing computer programs are due to these two attributes, often working together to befuddle us.
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.
You might ask yourself what makes code beautiful, and I like to borrow from the 13th century priest and philosopher Thomas Aquinas.
A building has integrity just like a man. And just as seldom. ~Ayn Rand
Clarity is the counterbalance of profound thoughts. ~Luc de Clapiers
It is my ambition to say in ten sentences what others say in a whole book. ~Friedrich Nietzsche
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
+
and +=
operatorsWhat 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"
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"
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-infunction 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.
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.
if (test = 1) {
// well, test is definitely 1 now
} else {
// this will never be hit
}
GorillaScript solves this by two ways:
:=
vs. =
If you think :=
looks ugly, good. You should be avoiding mutability whenever possible.
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.
// 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;
});
}
}
// 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;
});
}
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:
while
(and until
): Works like a normal while, but you can also specify an increment so it acts like a for
loop.for-in
: Iterate over an array-like object, can easily loop backwards or with a custom step or through only a segment.for-of
: Iterate over an object's keys and values, checking for ownershipfor-ofall
: Iterate over an object's keys and values, not checking for ownershipfor-from
: Iterate over a generator.while test()
do-something()
while test(), i += 1
do-something()
until test()
do-something()
until test(), i += 1
do-something()
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 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 value from gimme-numbers()
do-something value
for value, index from gimme-numbers()
do-something value
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.
const DEBUG = true
if DEBUG
do-some-expensive-check()
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()
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
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
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"
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
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
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)
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
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"
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
}
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"
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.
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!"
for item in array
do-something item
else
no-items()
let has-good = for some item in array
item.is-good()
let all-good = for every item in array
item.is-good()
let total = for reduce item in array, current = 0
current + item.value
let good-ones = for filter item in array
item.is-good()
let found = for first item in array
if item.is-good()
item
else
null
let array = [\a, \b, \c, \d]
let middle = array[1 til -1]
let reversed-middle = array[-2 til 0 by -1]
let array = [\a, \b, \c, \d]
let last = array[* - 1]
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!"
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 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"
switch
case is-good()
"good"
case is-bad()
"bad"
default
"neutral"
try
something-dangerous()
catch e as SpecificError
handle-error(e)
catch e
uh-oh()
else
whew()
finally
cleanup()
let fib()*
let mutable a = 0
let mutable b = 1
while true
yield b
let tmp = a
a := b
b += tmp
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)
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
let promise = fulfilled! value
let bad-promise = rejected! reason
let take-a-while = promise! #*
for i in 0 til 10
calculate(i)
yield delay! 100_ms
let read-file-or-timeout(filename)
some-promise! [
read-file filename
delay! 1000_ms
]
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
let loop = promisefor(3) filename in filenames
let text = yield read-file filename
return text.to-upper-case()
loop.then on-success, on-error
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()
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"
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
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
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
gorilla -jco lib/code.js src/one.gs src/two.gs src/three.gs --sourcemap
grunt.initConfig({
gorilla: {
dist: {
options: {
sourceMap: true
}
files: {
"lib/code.js": ["src/one.gs", "src/two.gs", "src/three.gs"]
}
}
}
});
Hopefully there's enough time to show a quick demo.
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.
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())
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)
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()
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
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()