Why Squiggle?
You might be wondering why you should use Squiggle. After all, JavaScript is better than ever, and there are many new contenders for "cool compile-to- JavaScript" language right now.
Obviously, if I was satisfied with the selection of compile-to-JS languages, I wouldn't have made Squiggle. So first I'll lay out some of the core things I value in Squiggle, and then compare Squiggle to other languages.
What's good?
Simple syntax
Squiggle does not use semicolons, and indentation does not matter. Arrays, objects, strings, and numbers are all written like JS, so it's easy to pick up.
Arity checking
Squiggle functions have their arity (parameter count) checked. This means that the function fn(x, y) x + y
will throw an error if called with any number of arguments besides 2. This may not seem like a big deal if you're used to JavaScript, but this behavior leads to errors traveling through your program to later points, undetected, and makes refactoring code extremely hard, as changing the arity of a function generally leads to misbehaving code rather than flat out errors where the incorrect call occurs.
No implicit this
The JavaScript keyword this
is really a pain. Just look at all the Stack Overflow results. It's such a common pain point that there's a pattern of var self = this to work around it nesting and callback issues. It's also hard to tell up front of a function uses this
at all. You could even accidentally use when copy/pasting code. Because of this, Squiggle makes JavaScript's this
into a named parameter like any other:
let lorenzo = {
name: "Lorenzo",
getName: fn(@this) this.name
}
lorenzo.getName()
This also means that callbacks are safer:
let obj = {
delay: 300,
title: "Squiggle is Cool",
showLater: fn(@this) do
setTimeout(fn() global.alert(this.title), this.delay)
end
}
Explicit variadic parameters
Many JavaScript functions are variadic (can accept many different numbers of arguments), but JavaScript makes it hard to write these functions, relying on the arguments
pseudo-array. Squiggle uses the ES6 rest notation for functions like this: def printf(format, ...args)
.
No hoisting
JavaScript is filled with hoisting. This means you can use variables before initialization, which leads to subtle bugs when reordering your code, or when you accidentally try to use values in a circular fashion. In Squiggle it's an error to use a variable before initialization, and impossible to reassign a variable.
Frozen array/object literals
ES5 introduced Object.freeze
, a function which makes a JavaScript object impossible to update with new values. However, JS never got a syntax for frozen object literals. In Squiggle, all array and object literals are frozen by default, making it easy to safely pass off your data to another function, knowing it cannot be modified.
Easy update operators
JS arrays have the awkward .concat
method to put two arrays together, but have no built-in operators or methods or functions for merging objects. Squiggle adds ++
for array concatenation and ~
for object concatenation, allowing programmers to easily compose data.
Destructuring and pattern matching
Squiggle also offers destructuring assignment and pattern matching, making it easy to grab nested properties from objects, import multiple values into scope, or dispatch behavior based on object shapes.
No type coercion
Squiggle does not perform type coercion on its operators, so most of them only work on numbers, producing runtime errors on other values. For example 1 + "2"
is an error. Use 1 .. "2"
.
Dot operator saftey
Squiggle throws errors on bad property access. For example, foo.bar
will throw unless foo has "bar"
is true (meaning foo
is not undefined/null, and has a property "bar" with a value not equal to undefined). This makes it easier to avoid grabbing nonexistent values from arrays and objects.
Better equality
Squiggle has two separate equality operators: is
and ==
. The operator is
is like the ES6 Object.is
, and ==
is like JavaScript ===
but throws on objects/arrays.
Versus ClojureScript
ClojureScript is a very powerful and expressive language with a great community, but it has some rough points compared to Squiggle.
Including libraries written in JS with your ClojureScript bundle is a bit awkward. ClojureScript using Google Closure compiler with advanced optimizations on, meaning you have to actively fight against name mangling to use JS with your program.
ClojureScript uses nil
(their version of null
) pervasively to represent values that cannot exist and other errors. The idea is that nil punning will let nil
flow through the system with the desired behavior. Squiggle embraces fail fast design and throws as many errors as possible, right at the site of the problem, for increased debuggability of code.
The "Quick Start" guide is 11 printed pages and requires installing Java and either downloading a JAR or adding a slow Java-based build tool.
Versus Elm
Elm is a very neat language, but it's really an entirely different world from JS.
Elm code cannot call JS code, and vice versa. This means that to communicate, everything is done through a message passing system with communication ports. This means that gradually porting a JS application to Elm could be quite difficult.
Elm has some great guarantees about statically enforced semver with its package manager, but nonetheless requires a separate package manager and build tool from existing JS tools.
Elm uses a Haskell-style import system, which encourages polluting your local scope like:
import Html exposing (..)
import Style exposing (..)
type alias Styles = List ( String, String )
centeredLayout : Styles
centeredLayout =
[ display flex'
, justifyContent center
, alignItems center
]
view : Html
view =
div [ style centeredLayout ] [ text "Hello, world!" ]
So it's not clear by looking at import statements where a particular function is even coming from.
Versus PureScript
PureScript is fairly similar to Elm, though it does actually provide a way to call JS code from PS and vice versa. Although, it mentions that code guarantees can be violated this way.
Also, PureScript requires manual wrapping of JS functions to make them curried, and JS must manually apply PS functions in a curried fashion. So for each JS file you want to use with PS, you have to write out type annotations and curried wrappers around all of its behavior, a non-trivial task.
Versus CoffeeScript
CoffeeScript made a huge impact on the JS world, influencing multiple new features in ES6. CoffeeScript aims to be expression-oriented and compile very closely to JS, using the same tooling, much like Squiggle.
CoffeeScript contains many synonyms, making consistent code even harder to develop:
yes = on = true
no = off = false
not = !
and = &&
or = ||
is = ===
isnt = !==
@ = this
@foo = this.foo
Additionally, the flexible syntax can make programs parse in surprising ways:
This looks like a function that returns its only argument, but…
f = x -> x
# looks like:
f = (x) -> x
# but works like:
f = x(() -> x)
This looks like a function that logs 4, but accidentally omitted whitespace changes the meaning entirely, instead of producing an error.
f = ->
console.log(4)
# looks like:
f = () ->
console.log(4)
# works like:
f = () -> undefined
console.log(4)
CoffeeScript's syntax is at its most egregious with regards to functions passed to other functions, or braceless based objects.
foo ->
console.log("yo")
,
a: 1
b: 2
f: -> ->
# looks like... complete gibberish
# works like:
foo(() -> console.log("yo"), {
a: 1,
b: 2,
f: () -> () -> undefined
})
Lastly, CoffeeScript does not provide any useful ways to compose arrays or objects together, making its expression-oriented nature not as powerful as it could be.
Versus ES6 / ES2015
ES6 (now known as ES2015 officially) is the latest JavaScript standard. It was designed to be backwards compatible, so that means it inherits all of JavaScripts bad parts, merely adding more things to JS to know and use, not reducing complexity in any way.
Browser support is getting better every day, but the compatibility table is filled with a lot of red still. Many things cannot be polyfilled 100% correctly, so there's also a risk of writing code that depends on polyfilled fake-ES6 but won't run in a real ES6 engine, such as generators, symbols, weakmaps, and more.
Versus TypeScript
TypeScript is a superset of ES5 with support for some ES6 features, as well as a static type system.
The type system is pretty good, especially for being optional and layered on top of such a loose language, but it can provide very little assurance without explicit type annotations on every single function argument, and many variable declarations as well, meaning it's more work to gain value from TS. This also means that using JS libraries with TypeScript nearly requires the use of community maintained type definition files, of varying quality.
Also, per TypeScript's design goals, it does not perform any runtime checks on code. This is good for performance, but it makes TypeScript not terribly useful for writing libraries, or for being used in a mixed JS/TS project, as the type system is ignored in that case.
Otherwise, TypeScript essentially shares the same faults as plain JS or ES6.