Is a dynamic scripting language that compiles into JavaScript and is fully compatible with it. JSON is also a subset of VaderScript.
VaderScript builds on top of the dynamism of JavaScript, enhancing its functional nature. Just like JavaScript at it's core, the primitives of the language are simple and powerful, but VaderScript keeps the consistency on every level, avoiding surprises and unexpected behavior whenever possible.
The big difference is that VaderScript is more strict. Undefined access and invalid types will throw errors, unless explicitly allowed. This way, rebel properties are shut down before they have the chance to cause any trouble.
// Variables (lexically scoped)
number = 1
darkSide = true
// Constants (begin with a capital letter)
Pi = 3.14159265
Pi = 44 // TypeError: Assignment to constant variable
// Immutable Constants (all capital letters or underscores)
TWO_PLANETS = ["Mars", "Jupiter"] // made inmutable with Object.freeze
TWO_PLANETS.push("Pluto") // TypeError: Can't add property 2, object is not extensible.
// Arrays
list = [1, 2, 3, 4, 5,] // trailing comma is optional
list[0] // 1
list[0..2] // [1, 2, 3]
list[0..2] = [99] // replace segment
list // [99, 4, 5]
// Objects
systems = { shields: "ready", motor: "on", } // trailing comma is optional
troops = {
joe: {
weapon: "pistol" // no commas needed when using line breaks
level: 8
}
}
// Functions
add = (x, y) -> { x + y }
add(3, 5) // returns 8
add(3) // TypeError: argument 'y' can not be undefined
// Default parameters (param=val)
initials = (fullName, separator=".") -> { fullName.split(" ").map((s) -> s.charAt(0)).join(separator) }
initials("Hommer J Simpson") // returns "H.J.S"
initials("Hommer J Simpson", "--") // returns "H--J--S"
// Named parameters ({params})
obviousTotal = ({value, tax, discount}) -> value + tax - discount
obviousTotal(value: 100, tax: 10, discount: 5) // returns 105
// Parameter Types (Contracts)
mul = (x:number, y:number = 1) -> x * y
mul("foo") // TypeError: argument x:number can not have value "foo"
// Regular, Typed, Named and Default parameters combined
total = (value:number, {tax:number = value*0.1, discount:number = 0}) -> subtotal + tax - discount
total(100) // returns 110
total(100, tax: 12, discount: 5) // returns 107
// Dynamic Type Checks (Contracts)
x = 4
x is :number // true (:number is a VaderScript default type)
x is :string // false
x as :string // "4"
x is :fjck // TypeError: fjck type or constructor not known
new Number(4) is :Number // true (instanceof constructor)
4 is :Number // true (primitive types work as expected)
user is :User // true if user is an instance of the User constructor
// Custom Types (any object with methods isType and asType)
json = {
isType: (val=undefined) -> val is :string && (!!JSON.parse(val) catch false)
asType: (val:object|array) -> JSON.stringify(val)
}
"randomstuff" is :json // false
"randomstuff" as :json // TypeError: can not cast value to type :json
{foo: "var"} as :json // "{\"foo\": \"var\"}"
// Lexical scope of this (@)
dragster = {
distance: 0
move: (n=1) -> { @.distance += n }
}
setInterval(dragster.move, 10) // works as expected
// Pattern Matching
fib = ~> {
(n:number) when n < 0 -> throw new ArgumentError("negative number")
(0) -> 0
(1) -> 1
(n:number) -> fib(n-1) + fib(n-2)
}
fib(5) // returns 8
fib(null) // ArgumentError: fib function does not match argument null
// Handling null and undefined (?)
obj = {}
obj.foo // Error: property foo is undefined
obj.foo? // returns undefined
obj.foo?.var?.stuff? // undefined
val = obj.foo ? "default" // obj.foo or "default" if foo is null or undefined
obj.foo ?= "default" // assigns only if null or undefined (set default)
obj.foo // returns "default"
obj.foo ?= "something else" // not assigned, already had value "default"
obj.foo // returns "default" (was not modified)
Funtions accept some parameters and returns the result of the last expression of their body.
empty = () -> {}
id = (x) -> { x }
add = (x, y) -> { x + y }
The return
statement can be used for extra clarity, or to return early if needed:
check = (subject) -> {
return false if subject is :nil
// ... more code ...
return true
}
check(null) // false
check("helmet") // true
If the function body has only one expression, the braces can be omitted:
empty = () -> {}
id = (x) -> x
add = (x, y) -> x + y
To call a function, use parentheses with arguments:
none = empty() // undefined
one = id(1)
five = add(2, 3)
Parentheses can be omited only if the last argument is also a function (callback), for example:
// Regular iterators
list.forEach (element) -> {
console.log(element)
}
// Or even shorter (using anonymous arguments)
list.forEach -> console.log($0)
// Spec DSL
describe "A suite", -> {
it "contains spec with an expectation", -> {
expect(true).toBe(true)
}
}
// jQuery ajax calls
$.get "ajax/test.html", (data) -> {
$(".result").html(data)
}
If the last argument is an object, the object braces can be omitted:
$.get("test.cgi", name: "John", time: "2pm")
// is equivalent to
$.get("test.cgi", {name: "John", time: "2pm"})
Which is useful for Named Parameters.
When the argument is undefined, the given default value is used.
fill = (container, liquid="coffee") -> "Fill ${container} with ${liquid}"
fill("glass") // returns "Fill glass with coffee"
Equivalent to using an options object as last argument, but without the boilerplate code to extract the given properties.
fill = ({container, liquid = "coffee"}) -> "Fill ${container} with ${liquid}"
// is equivalent to
fill = (opts:object) -> {
container = opts.container
liquid = opts.liquid ? "coffee"
"Fill ${container} with ${liquid}"
}
fill("glass") // TypeError: expected an options object
fill(container: "glass") // "Fill glass with coffee"
fill(container: "glass", liquid: "tea") // "Fill glass with tea"
fill(liquid: "tea", container: "glass") // "Fill glass with tea"
Reducing boilerplate code also reduces the opportunity for typos and bugs. It also makes clear what parameters are being used when calling the function. Since Named Parameters are just a destructured options object, the function can be easily called from JavaScript.
VaderScript has native support for dinamyc type checks (Contracts), which also applies to function parameters:
add = (x:number, y:number) -> x + y
// is equivalent to
add = (x, y) -> {
if x isnot :number {
throw new TypeError("argument x:number can not be ${x}")
}
if y isnot :number {
throw new TypeError("argument x:number can not be ${x}")
}
x + y
}
// Calling with invalid data throws a TypeError
add(1, null) // TypeError, argument y:number can not be null
Unlike JavaScript, missing function arguments are not allowed by default. If no type is speciifed, :!undefined
is assumed, which means they accept anything except undefined.
add = (x, y) -> x + y
// is equivalent to
add = (x:!undefined, y:!undefined) -> x + y
// Which does not allow undefined values
add(1) // Exception Undefined: add expects 2 arguments
add(1, undefined) // Exception Undefined: add expects 2 arguments
add(1, null) // returns 1, because 1 + null is 1
To allow undefined properties (like in JavaScript), simply set the default value to undefined:
add = (x=undefined, y=undefined) -> x + y
add() // returns NaN, because undefined + undefined is NaN
add(undefined, undefined) // returns NaN
add(1) // returns NaN, because 1 + undefined is NaN
Parameters with no default values go first, then arguments with defaults, and named arguments go last.
update = (user, at:number = Date.now(), {name, age:number, reason:string = "needed fresh look"}) -> { /*...*/ }
update() // TypeError: argument user can not be undefined
update(joe, name: "Cappuccino") // TypeError: named argument age:number can not be undefined
update(joe, name: "Cappuccino", reason: "Thesaurus said so", age:22)
update(joe, yesterday, age: 22)
Anonymous functions with no parameter declaration can use Anonymous Arguments: $0, $1, $2, are implicit for first, second and third arguments respectively.
users.map(-> $0.name)
// is equivalent to
users.map(($0) -> $0.name)
Anonymous arguments reduce boilerplate on common high order functions like Array's map
, filter
, find
and forEach
.
For example, get names of users that start with "A", ordered by age:
users.filter(-> $0.name[0] is "A").sort(-> $0.age - $1.age).map(-> $0.name)
Note that Anonymous Arguments can also obscure argument values. As a general rule, use implicit arguments for common short lambda expressions where their value is clear, and don't use them on functions that span over multiple lines.
Translate directly to ES6 generator functions.
perfectSquares = *-> {
num = 0
loop {
num += 1
yield num * num
}
}
iterator = perfectSquares()
iterator.next().value // 1
iterator.next().value // 4
iterator.next().value // 9
VaderScript is strict by default, throwing ReferenceError if a variable or object property is not defined:
obj = {foo: "var"}
obj // {foo: "var"}
obj.foo // "var"
obj.duh // ReferenceError, key duh not defined in object
duh // ReferenceError, var duh not defined
Use "allow any" (?) postfix to return undefined instead of throwing an exception
obj? // {foo: "var"}
obj.foo? // "var"
duh? // undefined
obj.duh? // undefined
Use "allow any" navigation (?.) operator to avoid exceptions when accessing inner properties. It returns undefined on the first nested key that is null or undefined:
obj?.foo // "var"
obj?.duh?.foo // undefined
duh?.duh?.duh // undefined
Default assignment (?=) assigns only if it was null or undefined:
obj = {foo: "bar"}
obj.foo ?= "other" // not assigned, obj.foo was defined and not null
obj.miz ?= "other" // assigned, obj.miz was undefined
obj.foo // "bar"
obj.miz // "other"
obj.hhh = null
obj.hhh ?= "other" // assigned, obj.hhh was defined but null
obj.hhh // "other"
Default assignment (??=) assigns only if it was undefined, which is how default parameters are implemeneted:
math.foo = null
math.foo ??= "other" // not assigned
math.foo // null
Default truthy assignment (||=), for JavaScript compatibility, assigns only if it was falsy (undefined, null, false, '', 0, NaN)
math.foo = 0 // 0 is falsy
math.foo ||= "other" // assigned, because math.foo was falsy
math.foo // "other"
In general, (||) for falsy values, (?) for null and undefined, (??) only for undefined. In most cases, you will prefer to use (?) to tell appart both null and undefined from other falsy values.
Check with value is :type
(returns true or false)
str = "text"
console.log("a string") if str is :string
console.log("not a number") if str isnot :number
Assert function parameters with param:type
(throws an exception)
add = (x:number, y:number) -> x + y
add(null, 3) // TypeError: argument x:number can not have value null
Prototype constuctors can also be used as types to check instances:
joe = new Player()
ash = new ProPlayer()
joe is :ProPlayer // false
ash is :ProPlayer // true
Some types and aliases included by default on VaderScript:
:string
:number, :float
:int
:array
:object
:function
:bool
:null
:undefined
:nil // null or undefined
:any // anything, including undefined
n = 11.11
s = n as :string // "11.11"
i = n as :int // 11
b = n as :bool // true
x = n as :object // TypeCastError: can not cast 11.11 to type :obj
Note that VaderScript types are just runtime contracts, that don't have to be exclusive.
To check if a value is of any type use the union (:type1|type2|type3)
11 is :int|bool // true
false is :int|bool // true
To check if a value is of all types use the intersection (:type1&type2&type3)
11 is :int&bool // false
The check if a value is not of a type, use the negation (:!type)
11 is :!bool // true
11 isnot :bool // true (equivalent in this case)
Note that union, intersection and negation can be arbitrarily combined:
foo is :!undefined|(!mycupoftea&number)
Multiple types can also be used on function arguments
// Function that accepts both numbers and strings, where strings are interpreted as numbers:
add = (x:number|string, y:number|string) -> { x as :number + y as :number }
... TODO (examples :array<number>
, :object<string|null>
, :mytype<number>
)
Any object with methods isType
and asType
can be used as a :type.
This means you can define expressive interfaces and data validation tools, very handy to verify data input and function parameters.
For example, make a contract to check if a number is prime:
primeNumber = {
isType: (n:number) -> {
return false if n < 2
loop i from 2 to Math.sqrt(n) {
return false if n%i is 0
}
return true
}
// asType: is optional and only needed to cast values
}
3 is :primeNumber // true
"foo" is :primeNumber // false (primeNumber.isType only accepts numbers)
// Functions can use custom types
encrypt = (something, key:primeNumber) -> { /* ... */ }
Used to compare values, is equivalent to the ===
comparison in JavaScript (Note that JavaScript ==
operator has no equivalent in VaderScript).
if x is 5 {
console.log("high five!")
}
The opposite operator is isnot
if x inot 5 {
console.log("missed!")
}
When the right side of the comparison is a :type (starting with a semicolon), the othe sentence turns into a dynamic type check.
if x is :number {
console.log("maybe a five?")
}
The is :type
operator will lookup and check for a :type in this order:
- VaderScript default types such as :string, :number, :nil, ...
- Custom Type in the current score, an object with method
isType
, accepts type if isType(val) returns true and raises no exception. - Constructor function: accepts type if instanceof Constructor (with extra checks for native constructors like String and Number).
VaderScript JavaScript
is ===
isnot !==
@ this
a ** b Math.pow(a, b)
a in list check if a is included in list
a is :type check if a is of that :type
a as :type cast a to that :type
a? do not raise exception if a is undefined
a ?= b assign a = b only if a is null or undefined
a ??= b assign a = b only if a is undefined
Bitwise operators are prefixed by #
VaderScript JavaScript
#& &
#| |
#^ ^
#~ ~
#<< <<
#>> >>
#>>> >>>
Other operators are the same as in Javascript: &&, ||, !, ||=, ++, --, ...
If statements:
if truthy { doStuff() }
if truthy { doStuff() } else { doOtherStuff() }
if truthy { doStuff() } else if thruthy2 { doOtherStuff2() } else { doOtherStuff() }
Inline conditionals, sometimes are more readable:
doStuff() if truthy
doStuff() unless truthy
Note that if statements are also expressions, returning the last expression in their body:
x = if true { 1 } // x is 1
x = if false { 1 } // x is undefined
x = if false { 1 } else { 0 } // x is 0
x = 1 if true // x is 1
Note that, in order to avoid confusion with the "allow any" operator (?), the "Elvis" terniary operator (?:) is not supported.
Iterator functions should be used whenever possible:
[1..4].forEach -> console.log($0) // prints "1", "2", "3" on each line
[1, 2].map(-> 2 * $0).join(', ') // returns "1, 2"
Object.keys(obj).forEach (key) -> obj[key] // iterate object properties
When low level is neccessary, use the loop
operator. By itself is just an infinite loop:
loop {
console.log('Print for ever') // infinite loop
}
Using break
to stop the loop:
loop {
console.log("One more time")
break if Math.random() > 0.9
}
With a control variable:
loop i {
// i = 0, 1, 2 ...
}
loop i from 10 {
// i = 10, 11, 12 ...
}
loop i from 0 to 10 {
// i = 0 .. 10 (inclusive)
}
loop i from -10 to 10 step 2 {
// i = -10, -8, -6 ... 8, 10 (inclusive)
}
loop i in [1..10] {
// iterate on arrays or ranges, although Array.prototype.forEach is usually preferred
}
… in progress …
I would like to give it a more Prototypal and Functional approach, but maybe for compatibility it’s best to keep same semantics as ES6 classes ...
class Animal {
constructor: (@.name) -> {}
move: (meters) ->
alert("${@.name} moved ${meters}m.")
}
class Snake extends Animal {
move: -> {
alert("Slithering...")
super.move(5)
}
}
class Horse extends Animal
move: -> {
alert("Galloping...")
super.move(45)
}
}
sam = new Snake("Sammy the Python")
tom = new Horse("Tommy the Palomino")
sam.move()
tom.move()
// TODO: meta-programming to compose classes ?
class User extends Sequel.Model {
has_many('products') // could this be possible ?
}
Person = (age) -> {
@.age = age
setInterval(-> {
@.age++ // @ properly refers to the person object
}, 1000)
}
p = new Person()
// TODO: do not allow this @ on regular functions. // Only on explicit constructors or on objects/classes
Person = (age) -> {
@.age = 0 // Exception, self reference (@) is only allowed on object methods or constructor functions
}
TODO: Constructor functions ?
Person = @(age) -> {
@.age = age
}
Person.prototype.grow: -> { @.age += 1 }
TODO: Constructors with prototypes ?
Animal = class (name) -> {
@.name = name
} static {
class.moves = 0
} prototype {
@.move = (meters) -> {
class.moves += 1
alert("${@.name} moved ${meters}m.")
}
}
General syntax:
match var { pattern1 -> {...}; pattern2 -> {...}; ... }
Example:
str = "b"
is_vowel = match str {
'a' | 'e' | 'i' | 'o' | 'u' -> true
_ -> false
} // false because str is "b"
Note that, with no static analysis, the pattern-matching can not be exhaustive. If no pattern matches, a runtime error will be thrown, which is still better than returning undefined.
fib = ~> {
(0) | (1) -> 1
(n) -> fib(n-1) + fib(n-2)
}
// equivalent to:
fib = -> {
match arguments {
(0) | (1) -> 1
(n) -> fib(n-1) + fib(n-2)
}
}
Example: boolean evaluator
eval = ~> {
("T") -> true
("F") -> false
(["Not", expr]) -> not eval(expr)
(["And", leftExpr, rightExpr]) -> eval(leftExpr) and eval(rightExpr)
(["Or", leftExpr, rightExpr]) -> eval(leftExpr) or eval(rightExpr)
}
e1 = "T"
e2 = ["And", "T", "F"]
e3 = ["Or", e1, e2]
e4 = ["And", ["Not", e2], e1]
[e1, e2, e3, e4].forEach (expr) -> {
console.log("${eval(expr)} <= ${expr}")
}
Example: list sum
sum = ~> {
() -> 0
(first, ...tail) -> first + sum(tail)
}
sum(1, 4, 5) // 10
Example: function to check if two rational numbers are equal:
eqRat = ~> {
([_,0], [_,0]) -> true
([_,0], _) -> false
(_, [_,0]) -> false
([n1, 1], [n2, 1]) when n1 is n2 -> true
([n1, d1], [n2, d2]) when ((n1 * d2) is (n2 * d1)) -> true
_ -> false
}
eqRat([22, 0], [33, 0]) // returns true
List of common patterns:
match var {
// Type checks
x -> "match anything, cature in variable x"
n:number -> "a number"
n:nil -> "null or undefined"
str:string|nil -> "a string, null or undefined"
f:function -> "a function"
a:array -> "an array"
o:myType -> "checks on myType"
o:MyClass -> "an instance of MyClass"
// Using guards
x:int when x > 0 -> "a positive integer"
// Literals
"lit" -> "is lit string"
0 -> "is 0"
null -> "is null"
undefined -> "is undefined"
// Arrays
[] -> "empty array"
[_] -> "array with one element"
[x] -> "array with one element, capture that element in variable x"
[_, x] -> "array with two elements, capture second element in variable x"
[head, ...tail] -> "array with at least one element, assign head to first element, tail to the rest of the array (may be empty)"
[head, second, ...tail] -> "array with at least two elements, assing head and second to the first two, tail to the rest of the array"
[head:number, second:number, ...tail] -> "array with at least two elements, where the first and the second are numbers"
[_:number, _:number] -> "array of two elements that are numbers"
a:array -> "an array (may be empty)"
a:array<string> -> "an array (may be empty) where all elements are strings" // dynamic parametric types is still experimental
// Objects
{} -> "empty object"
{x} -> "object that has property x, capture value on variable x"
{x, y:number} -> "object that has properties x and y, where y is a number"
// Combined, nested patterns
[{x}, y] -> "array with two elements where the first element is an object that has property x"
[{x, x2}, y, ...tail] where _.isArray(x) and tail.length >= 3 -> "..."
[{x:MyClass}, y:MyClass]] -> "..."
// Function arguments
(x) -> "arguments object with one argument"
(x, y) -> "arguments object with two argsuments"
(x, ...others) -> "arguments object with at least one arg, assign x to the first argument, others to the rest (may be empty)"
(x:number, {y:number}) when x > 21 -> "arguments object, the first argument is a number bigger than 21, the second one is named parameter; an object with a property y that is also a number"
(...args) when 2 < args.length && args.length < 5 -> "arguments object with 3 or 4 args"
// Wildcard
_ -> "anything (wildcard) always matches but does not assign. Often used as last option for completeness"
}
This is fantastic and quite exciting!! I think many of these concepts are really rich for exploration and implementation!!
I really love these ideas and how clearly they are expressed! I think defiantly start to work on this implementation! Many of these Ideas could be built!
I wonder if Vader could be written as a Babel plugin? https://babeljs.io/docs/plugins/