Operators
An operator is something that takes one or more values (or expressions) and yields another value (so that the construction itself becomes an expression)
Operators can be grouped according to the number of values they take. Unary operators take only one value, for example ! (the logical not operator). Binary operators take two values, such as the familiar arithmetical operators + (plus) and - (minus). The majority of Ghost's operators fall into this category.
Precedence and Associativity
The precedence of an operator specifies how "tightly" it binds two expressions together. For example, in the expression 1 + 5 * 3, the answer is 16 and not 18 because the multiplication (*) operator has a higher precedence than the addition (+) operator. Parentheses may be used to force precedence, if necessary. For example, (1 + 5) * 3 evaluates to 18.
When operators have equal precedence their associativity decides how the operators are grouped. - is left-associative, so 1 - 2 - 3 is grouped as (1 - 2) - 3 and evaluates to -4.
Use of parentheses, even when not strictly necessary, can often increase readability of the code by making grouping explicit rather than relying on the implicit operator precedence and associativity.
The following table summarizes the operator precedence in Ghost, from highest to lowest. Operators in the same box have the same precedence.
| Precedence | Operator | Description | Associates |
|---|---|---|---|
| 1 | . [] | Property access, Subscript | Left |
| 2 | () | Call | Left |
| 3 | - ! | Negate, Logical not | Right |
| 4 | % | Modulo | Left |
| 5 | * / *= /= | Multiply, Divide | Left |
| 6 | + - += -= | Add, Subtract | Left |
| 7 | < <= > >= | Comparison | Left |
| 8 | == != | Equals, Not equal | Left |
| 9 | .. | Range | Left |
| 10 | ? : | Ternary | — |
| 11 | and | Logical and | Left |
| 12 | or | Logical or | Left |
% binds tighter than * and /, so 2 * 3 % 4 is 2 * (3 % 4) and evaluates to 6. And .. binds looser than the comparisons, so a range built from compared values needs parentheses. When in doubt, parenthesize.Assignment (=) is not in the table: it is a statement rather than an expression, so a = b = c is not valid Ghost.
Arithmetic Operators
Remember basic arithmetic from school? These work just like those.
| Example | Name | Result |
|---|---|---|
-a | Negation | Opposite of a. |
a + b | Addition | Sum of a and b. |
a - b | Subtraction | Difference of a and b. |
a * b | Multiplication | Product of a and b. |
a / b | Division | Quotient of a and b. |
a % b | Modulo | Remainder of a divided by b. |
Division always produces a decimal, even when both operands are whole: 10 / 4 is 2.5, not 2. Use math.floor() if you want the whole part.
The result of the modulo operator (%) has the same sign as the dividend - that is, the result of a % b will have the same sign as a. For example:
console.log(5 % 3) // >> 2
console.log(5 % -3) // >> 2
console.log(-5 % 3) // >> -2
console.log(-5 % -3) // >> -2
+ also concatenates two strings:
console.log("Hello, " + "world!") // >> Hello, world!
On lists every arithmetic operator is elementwise, and broadcasts: a number spreads across the list, and a shorter shape stretches across a longer one.
console.log([1, 2, 3] * 2) // >> [2, 4, 6]
console.log([1, 2, 3] + [10, 20, 30]) // >> [11, 22, 33]
console.log([[1, 2], [3, 4]] + [10, 20]) // >> [[11, 22], [13, 24]]
Note that + does not join two lists the way it joins two strings — the arithmetic reading is the one that keeps all five operators agreeing with each other. Use concat() to join.
Both operands have to be the same type. Adding a number to a string is a type error — use a template literal, or call toString() on the number first.
Assignment Operator
The assignment operator is =. This declares and assigns the value of the expression on the right.
message = "Hello, world!"
console.log(message) // >> Hello, world!
Compound Assignment
The compound operators apply an arithmetic operation to a variable and assign the result back to it.
| Example | Equivalent to |
|---|---|
a += b | a = a + b |
a -= b | a = a - b |
a *= b | a = a * b |
a /= b | a = a / b |
There is no %=; write a = a % b.
total = 10
total += 5
console.log(total) // >> 15
Increment and Decrement
++ and -- are postfix operators that add or subtract one. There is no prefix form — ++x is not valid Ghost. They work on a variable, a property, and an index alike:
counts = { hits: 0 }
list = [1, 2]
counts.hits++
list[0]++
count = 0
count++
count++
count--
console.log(count) // >> 1
They are most at home in the increment clause of a for loop.
Comparison Operators
Comparison operators, as their name implies, allow you to compare two values.
| Example | Name | Result |
|---|---|---|
a == b | Equal | true if a is equal to b. |
a != b | Not equal | true if a is not equal to b. |
a < b | Less than | true if a is less than b. |
a > b | Greater than | true if a is greater than b. |
a <= b | Less than or equal to | true if a is less than or equal to b. |
a >= b | Greater than or equal to | true if a is greater than or equal to b. |
Numbers, strings, and booleans compare by value. Comparing two values of different types is a type error rather than false — except against null, which any value may be compared to, and which is how you test whether something is set:
if (result == null) {
// nothing came back
}
Every type answers == and !=. Which answer you get depends on the kind of value, and the split is between values defined by their contents and values defined by which one they are:
| Both sides are | == compares |
|---|---|
| number, string, boolean | value |
list | contents, to any depth |
map | contents, to any depth — same keys, each with an equal value |
date | the instant, whatever time zone each is attached to |
duration | its six components |
instance | identity |
| function, class, trait, module | identity |
Lists and maps compare by contents, so one you built compares equal to one you wrote out:
console.log([1, 2] + [1, 2] == [2, 4]) // >> true
console.log([[1], [2]] == [[1], [2]]) // >> true
console.log({name: "Ghost"} == {name: "Ghost"}) // >> true
Class instances compare by identity: two instances of the same class holding equal fields are still two different objects, and == reports false. Functions, classes, and traits work the same way — a value is equal to itself and to nothing else.
class Point {
constructor(x) { this.x = x }
}
console.log(new Point(1) == new Point(1)) // >> false
p = new Point(1)
console.log(p == p) // >> true
Ordering two lists with < or > is not supported: neither an elementwise nor a lexicographic reading is obviously the right one, so Ghost refuses rather than picking. Two durations are the same — "which span is longer" has no answer without a reference date, since a month is a different number of days depending on which month it starts from.
Dates compare as instants, with ==, !=, and all four ordering operators. See Date.
Logical Operators
| Example | Name | Result |
|---|---|---|
!a | Logical not | true if a is false, and vice versa. |
a and b | Logical and | true if both a and b are true. |
a or b | Logical or | true if either a or b is true. |
and and or do not short-circuit. Both sides are evaluated before the operator is applied, so a test on the left cannot guard the expression on the right. See Control Flow.Range Operator
.. builds a list of the whole numbers from its left operand up to and including its right one.
console.log(1 .. 5) // >> [1, 2, 3, 4, 5]
It is mostly used to drive a for ... in loop:
for (i in 1 .. 3) {
console.log(i)
}
If the start is greater than the end, the result is an empty list — ranges do not count backwards.
Ternary Operator
condition ? ifTrue : ifFalse evaluates to one of two expressions depending on a condition.
label = count == 1 ? "item" : "items"
Ternary expressions cannot be nested, in either branch, and parentheses do not change that — width > 100 ? (width > 500 ? "huge" : "large") : "small" is a syntax error. Use an if statement, or an intermediate variable:
if (width > 500) {
size = "huge"
} else if (width > 100) {
size = "large"
} else {
size = "small"
}
Subscript and Property Access
[] reads an element from a list, a character from a string, or a value from a map by key. . reads a property of a map or an instance, or calls a method.
list = [1, 2, 3]
map = { name: "Ghost" }
console.log(list[0]) // >> 1
console.log("Ghost"[0]) // >> G
console.log(map["name"]) // >> Ghost
console.log(map.name) // >> Ghost
An index that is out of range, and a map key that is not present, both read as null rather than raising an error.