Skip to content

Instantly share code, notes, and snippets.

@marioizquierdo
Last active June 7, 2016 03:27
Show Gist options
  • Save marioizquierdo/ecbe19b791a632c47816cf20e7ed909a to your computer and use it in GitHub Desktop.
Save marioizquierdo/ecbe19b791a632c47816cf20e7ed909a to your computer and use it in GitHub Desktop.
VaderScript syntax introduction

VaderScript

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.

Syntax Overview

// 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)

Functions

Function syntax: (params) -> {body}

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.

Default Parameters (param=val)

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"

Named Parameters ({params})

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.

Argument Type Checks (param:type)

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

Example with Combined Parameter Types

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 Arguments (-> $0)

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.

Generator Functions (*-> yield)

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

Handling undefined properties and null

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.

Dynamic Type Checks (Contracts)

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

Cast values to other types (as :type)

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

Multiple Types (union, intersection, negation)

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 }

Parametric Types (nested contracts)

... TODO (examples :array<number>, :object<string|null>, :mytype<number>)

Custom Types

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) -> { /* ... */ }

The (is) operator

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 operators

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: &&, ||, !, ||=, ++, --, ...

Loops and Conditionals

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.

Loops

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
}

Classes, Inheritance, and Super

… 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 ?
}

Lexical this (@)

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.")
  }
}

Pattern matching (match) (~>)

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.

Pattern matching on function arguments (~>)

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"
}
@curtislinden
Copy link

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/

@DavidYork
Copy link

I agree with Curtis, this is fantastic.

We went over this previously and I mentioned lots of things I like. Basically my default state for this entire language draft is to love everything except for what I have below. Most of my comments are critiques (or occasionally questions) but all in all this work is absolutely fantastic. I think you've got the best browser client-side language by far.

Some thoughts:

You create variables when you assign them (or so it appears). For example number = 42 instead of var number = 42. Is this intentional? I feel an implicit option strict is a really good idea, it avoids lots of bugs.

Why do you allow both &1, &2, &3, as well as &a, &b, &c? I don't think that adds to expressiveness, but rather gives arbitrary esoterica that will confuse. Also I don't like these at all - they remove the "you must have 2 parameters to this function" safety net you have when you don't have implicit arguments and make for slightly less readable code all in the name of a little bit of brevity.

For &a why was & chosen, is it because of another language that does this? I'm sure it's fine, but my C background makes by brain go screwy when I see the & symbol.

I notice you have num and integer - I recommend having both be 3 letters or both be full words (I prefer full words).

Can you use ; or : to separate multiple commands on the same line?

Regarding this line:
call = (name, at = "7pm", {for = "emergency"}) -> "Call ${name} at ${at} for ${for}"
I noticed named arguments mean arguments don't need to be positional, but why have both? It feels they would be somewhat arbitrary. Are named arguments automatically considered optional? Personally I prefer named arguments always but I suspect I'm in the minority. It seems optional arguments are the ones with defaults, so having both named and unnamed would not be a benefit.

Regarding prototypes I suspect mirroring the behavior of ES6 is totally the way to go, but I personally cannot stand the use of the term prototype and would like a different keyword :)

I find this syntax confusing:
[_, _]:arr[num] -> "array of two elements that are numbers" a:arr -> "an array (may be empty)" a:arr[str] -> "an array (may be empty) where all elements are strings"
Personally I would prefer this:
[num, num]
Although I do not understand why you have both [] and a:arr. Is one creating an instance, and the other describing a type? Also I really don't think you're gaining anything with the brevity, personally I much prefer array to arr.

I love the prefix # for bitwise operators!

Are you going to allow /* ... */ style comments, and if not why not?

Personally I don't like the use of // for comments though, I prefer # greatly. I feel // is a hangover from C++ where it was a variation of /* ... */. The extra character buys nothing other than the ability to have /* ... */ style comments as well, but if you# it for comments obviously you cannot use it for bitwise operators. Maybe ^? So like ^& and &|? You know, honestly I prefer the use of English for non-bitwise operators, and just using the operators for bitwise operations. For example:
if (x and y) or (not x and not y) then... which frees up |, !, &, etc for bitwise operations.

So regarding this:
If the last argument is an object, the object braces can be omitted, so they look like "named arguments":

$.get("test.cgi", name: "John", time: "2pm")
// equivalent to
$.get("test.cgi", {name: "John", time: "2pm"})

Does this create language ambiguity with named parameters, as they are different than creating an object with arbitrary members?

Not needing to use parentheses when calling a function if and only if the last param is a function makes no sense to me and is very confusing:
$.get "ajax/test.html", (data) -> { $(".result").html(data) }
I get that you are trying to avoid the })(); javascript garbage, but I don't think you have the solution yet.

One thing that Javascript is really lacking is language features designed for large software projects. C# has lots of this stuff. I think you may be solving today's problem (JS = garbage) brilliantly, but it may not be doing a great job solving some of tomorrow's problems. This is fine, doing things like advanced module management, information hiding, guarantees, etc is probably perfect for a later version of the language.

@marioizquierdo
Copy link
Author

@DavidYork this is fantastic feedback! Thank you so much.

I'll answer in reverse order...

One thing that Javascript is really lacking is language features designed for large software projects. C# has lots of this stuff ...

C# is a fantastic language! but TypeScript is already doing a great job at making JavaScript more like C#. It is a great alternative for large scale development, with IDE assistance and all that.

In one hand, having static analysis and better support for classes would get a little out of scope of what I'm trying to do. In the other hand, I don't really want to diverge from JavaScript, but to embrace what already makes JavaScript great. In a sense, I'm applying a minimalist principle: remove all the clutter to keep the most important parts. Then add a little more to empower the functional nature of the language.

Not needing to use parentheses when calling a function if and only if the last param is a function makes no sense to me and is very confusing ...

Good point. That seems to just add a random rule to save keystrokes. If it wasn't obvious enough, I was trying to have something like Ruby blocks, or Swift trailing closures, which allow to make clean DSLs. I am also trying to make a case for using functional iterators (forEach, map, select, filter, etc) instead of for loops. Examples that I thing would benefit from this:

// Most for loops should look like this one
list.forEach (element) -> { 
  console.log(element)
}

// or in a single line
list.forEach (element) -> console.log(element)

// or even shorter
list.forEach -> console.log(&a)

// Transform a list with multiple line statements
list.map (element) -> {
  multilinestatement
  transform(element)
}.filter (element) -> {
 isCool(element)
}

// DSL like libraries, like Jasmine.js
describe "A suite", -> {
  it "contains spec with an expectation", -> {
    expect(true).toBe(true)
  }
}

I tried other approaches, like making a different syntax for trailing functions ...

// regular syntax
funcWithCallback("blabla", (args) -> {
  // ...
})

// more like ruby ?
funcWithCallback("blabla")  do |args|
  // ...
end // looks good, but I prefer using curly brackets { } because any editor can easily find the matching one

// more like swift ?
funcWithCallback("blabla") { (args) ->
  // ...
}

none looks as "clean" and in line with the rest. Well, maybe I should just not do it and let every single list.forEach loop to be closed with })...

$.get("test.cgi", name: "John", time: "2pm")
// equivalent to
$.get("test.cgi", {name: "John", time: "2pm"})

Does this create language ambiguity with named parameters, as they are different than creating an object with arbitrary members?

No ambiguity. Actually, this is what makes "named arguments" to look like "named arguments". Note that when defining a function with named arguments (i.e. func = (x, {y, w}) -> { ... }), the "named arguments" must go last. This is implemented in JavaScript with a simple last argument that is an Object, and the function will have some basic checks to make sure that the object properties match the given arguments (and give them and/or check for types default if specified).

Note that all code written in Vader must be later perfectly usable from plain JavaScript. If you are calling a function defined in Vader with named arguments, just use an object as last argument. The function will even complain for invalid arguments when used from JavaScript!

Personally I don't like the use of // for comments though, I prefer # greatly.

Tell me more about that! I personally don't feel any real difference between them. I am trying to keep things that work well in JavaScript the way they are, makes it easier to transfer existing code. And yes, multiline comments /* ... */ will be there. I mean, comments will be just like JavaScript comments because I don't have any problem with that.

I love the prefix # for bitwise operators!

Hehe, actually I had issues confusing regular operators with bitwise before. I think most daily coding doesn't need them, so it's best to make them harder to type by accident.

I find this syntax confusing:
[_, _]:arr[num] -> "array of two elements that are numbers"

Personally I would prefer this:
[num, num]

Oh yes, that's an error. It actually makes no sense to check for :arr type on a pattern that uses the square brackets, because that can only be an array anyways.

I think I was playing with parametric types. Not only to implement something like Array<int> but also to be used by your custom types. The problem is that while Java or C++ do static analysis, here it's done at runtime, so to check a long array of int is O(N) on each pass. I may end up leaving parametric types behind, forcing the programmer to manually check, like for example:

list:arr when list.map(-> &a is :num) -> "array where all elements are numbers"

Which is not that bad, and shows potential performance issues more clearly.

The example you use [num, num] is close enough. You probably mean [x:num, y:num], or even better if you don't need the x and y variables in the matching body, using the wildcard [_:num, _:num]. Yes that's more readable.

Regarding this line:
call = (name, at = "7pm", {for = "emergency"}) -> "Call ${name} at ${at} for ${for}"
I noticed named arguments mean arguments don't need to be positional, but why have both? It feels they would be somewhat arbitrary. Are named arguments automatically considered optional? Personally I prefer named arguments always but I suspect I'm in the minority. It seems optional arguments are the ones with defaults, so having both named and unnamed would not be a benefit.

Well it's important to allow "regular" arguments in order to work well with JavaScript. I agree named arguments should be the "default" (clear intention), and maybe use special syntax to make unnamed arguments (less verbose). I personally find the right amount of verbosity to depend on the frequency of usage; if a given function is used only a few times, it's ok to give it a very long name that is very descriptive, but some other functions are used over and over again in your code, where is usually better to make them as short as possible.

In any case, I think the given example is probably not a good one. It may confuse "default values" with "named arguments". Let me give you another example:

add = (x, y = 1) -> x + y
add(1) // 2
add(1, 2) // 3

add = ({x, y = 1}) -> x + y
add(1) // TypeError, invalid argument, expected to be an object
add({x: 1}) // 2
add({x: 1, y: 2}) // 3
add(x: 1, y: 2) // 3

I understand Vader is actually not "simple" at all when it comes to function arguments. But I believe that is a very damn important part of any language, specially if it's as "loose" as JavaScript.

Can you use ; or : to separate multiple commands on the same line?

Yes of course. Use semicolons on the same line, just like Ruby or Coffeescript. In many years of coding Ruby I never had a problem with a semicolon, they are optional. Ugly if used as breaklines, but very necessary to make cool one-liners.

I notice you have num and integer - I recommend having both be 3 letters or both be full words (I prefer full words).

I am still not too happy with this. I want them to be as short as possible, specially to be used in function arguments:

makeUser = (name:str, age:num, favoriteMusinc:arr) -> { ... } 
makeUser = (name:string, age:number, favoriteMusinc:array) -> { ... } 

Maybe using full name is ok, even if that means typing :number and :string a million times. Using the shorter version may encourage programmers to give funky names to their own custom types.

Why do you allow both &1, &2, &3, as well as &a, &b, &c? I don't think that adds to expressiveness, but rather gives arbitrary esoterica that will confuse. Also I don't like these at all - they remove the "you must have 2 parameters to this function" safety net you have when you don't have implicit arguments and make for slightly less readable code all in the name of a little bit of brevity.

For &a why was & chosen, is it because of another language that does this? I'm sure it's fine, but my C background makes by brain go screwy when I see the & symbol.

I like &1, &2, &3 better, but reads weird when used in aritmetic operations (i.e. numbers.map(-> &1 + 1)), so I wanted to try with something else to see if it makes a difference. I also though about simply given an alias &val to the first argument (&1 or &val, &2, &3) for the common case of a callback with single argument (i.e. numbers.map(-> &val + 1)).

I also though about using $0, $1, $2 (looks less like a C reference), but unfortunately they are valid JavaScript variables ($ is actually commonly used for jQuery). It may be ok though, for most cases.

I really like this feature. It comes form this old feature proposal on CoffeeScript that I defended but was ultimately rejected: jashkenas/coffeescript#2952. This time around, is my own language so I'll do it :)

The reason I like it is because I believe it can be so commonly used that eventually becomes natural. The first time you see something like this:

[1, 2, 3].filter(-> &a < 10).map(-> &a * 2).reduce(-> &a + &b)

you may think "What the Heck", but it is really doing a lot of stuff with a minimal chance of errors.

The problem of removing the "safety net" is not really a problem, since JavaScript doesn't really have a safety net. VaderScript prefers to give the programmer the option to be as strict or as loose as she wants. It obviously doesn't make sense to use this syntax when defining object methods (would be the same as using arguments[0] in JavaScript). In practice, most people don't do this, and it's very easy to correct in a pull request when it happens. Think about the same line as before, with full security:

[1, 2, 3].filter((n:number) -> n < 10).map((n:number) -> n * 2).reduce((n1:number, n2:number) -> n1 + n2)
You create variables when you assign them (or so it appears). For example number = 42 instead of var number = 42. Is this intentional? I feel an implicit option strict is a really good idea, it avoids lots of bugs.

Yes, VaderScript compiles with "use strict";, just like CoffeeScript. Creating variables without var, let or cons, that are lexically scoped, makes it almost impossible to create globals by accident. That is one of the worst features of JavaScript (variables with no var prefix are globals by default, which you know, linters and experience help you avoid, but it's even better if you just don't have to think about it.

Most of my comments are critiques (or occasionally questions) but all in all this work is absolutely fantastic. I think you've got the best browser client-side language by far.

Nice! Thank you for that, but I don't think it's the "best browser client-side language" hehe. There's actually a good deal of other languages out there, take a look at this crazy list! https://github.com/jashkenas/coffeescript/wiki/list-of-languages-that-compile-to-js

Also, VaderScript is not really a "language", because it tries to stay close to JavaScript (just like CoffeeScript in this sense). I would say languages like elm are a lot better, they just lose on the "full compatibility" thing.

Well, that's all with the feedback -> feedback. Thank you so much for the helpful insights :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment