Literals

Abra has all the literal types you'd expect: integer and floating-point numbers, booleans, strings, and arrays.

val int = 24
val float = 1.618
val boolean = true
val string = "Hello"
val array = [1, 2, 3]

Variables

You probably noticed that the variable declarations above have no type indicator. Abra is a statically-typed language, but the typechecker will infer a variable's type whenever it can. This saves you from cluttering your code with redundant type annotations.

Of course you can always explicitly specify the type of a variable, either for the sake of clarity or if Abra is having a tough time figuring it out. If a type annotation conflicts with the actual type of the variable's assignment, then a type error will be raised at compile-time.

val favoriteNumber: Int = 24
val pairs: Int[][] = [[0, 1], [2, 3], [4, 5]]

val incorrectType: Int = "Not an integer!" // This would result in a type error 

Mutability

Variables also have a built-in notion of mutability, which is expressed via the keywords val and var. Variables declared with a var can be reassigned to with no issues, but a compile error will be raised when trying to reassign to a variable declared with val. This is very helpful because it allows you to declare intent of a variable; when a variable is declared with var it means, "hey, keep an eye out for this, it may change in the future", versus a val which can be safely assumed to never change its value.

val favoriteNumber = 24
favoriteNumber = 37 // This would result in a compilation error

var favoriteColor = "blue"
favoriteColor = "purple" // This works just fine

Types

Abra has some built-in types, representing the standard literal kinds but beginning with a capital letter. In Abra, types must always begin with a capital letter To declare an Array of a certain type, you use the square brackets ([]) suffix.

Int     // eg. 24
Float   // eg. 3.14
String  // eg. "Hello"
Bool    // eg. false

Int[]   // eg. [1, 2, 3]
Int[][] // eg. [[1, 2], [3, 4]]

Option Type

There's another special type in Abra called the Option type, which is denoted with the question-mark (?) suffix. This represents a value that may contain a value of that type, or it may not. A good example of this arises when indexing into an array:

val myArray = ["a", "b", "c"]
val firstLetter: String = myArray[0] // This will result in a type error

val letterC: String? = myArray[2] // This works just fine

It's important to understand the purpose of an Optional type in this case; when extracting a value out of an array of strings, there's no way of knowing whether an item will exist at that index. Some languages use a sentinel value (like undefined or null) to denote when no value existed at such an index, and some languages throw an exception and give up.

Abra takes the middle road: an array indexing operation will always succeed and will always return a "box", which may or may not contain a value. One way to "unbox" this value is to use the coalescing operator (?:), also called the "Elvis operator" in some languages. This operator will either unbox the Optional and return what's inside, or will provide a given fallback.

val letters = ["a", "b", "c"]
val letter1: String = letters[0] ?: "d" // This will unbox the Optional; letter1 = "a"
val letter2: String = letters[15] ?: "z" // This will use the provided default "z"

letters[2] ?: 12 // Type error, since the default does not match the type of the Optional

"hello" ?: "world" // Type error, since the left-hand side "hello" is not an Optional

There's another operator that makes it convenient to work with Optional types: the ?. operator. This allows for an Optional-safe way of accessing properties of variables without manually "unwrapping" the value. For example, if we wanted to get the length of a string contained within an array:

val names = ["Brian", "Ken", "Meg"]

val brian: String? = names[0] // The type of brian is an Optional String
val wrongLength = brian.length // Type error, since the property "length" does not exist on String?
val rightLength = brian?.length // This is correct

The rightLength variable will be of type Int?. Essentially, what happens here is this: since the code can't be sure if the variable brian holds a value or not, we can use theOptional-safe operator (?.) to access its length property. If the variable holds a value, it will get the length property off of the variable; if it does not hold a value, it will just short-circuit and produce the value of None.

Functions

Abra has standalone, top-level functions (functions not tied to any object or instance), and also (eventually) methods (functions attached to a given type instance). Functions have names, any number of arguments and a body. Arguments must have type annotations describing the type of that argument; a return type annotation for the function is optional (Abra will infer this if not specified).

A function's body can be a block which consists of many statements/expressions, the last of which will be the return value for that function. If a function's body consists of only 1 expression, you don't need a block.

func greaterThan(num1: Int, num2: Int): Bool {
  val gt = num1 > num2
  gt
}

// The above function could also be written like this, omitting the return 
// type annotation and condensing the function body into a single expression
func greaterThan(num1: Int, num2: Int) = num1 > num2

Functions can also be declared with default argument values. This makes those parameters optional when calling that function; if no value for that parameter is passed, the default value will be provided to the function body. Arguments with default values will have their type be inferred from the default value, if no type annotation is present. Note that all optional (aka default-valued) parameters must come at the end of the argument list; there can be no required (aka non-default-valued) parameters among the optional ones.

func add(a: Int, b: Int = 2, c = 3) = a + b + c

add(1)  // 6
add(1, 10)  // 14
add(1, 10, 100)  // 111

// Note that here ▾ there is no type annotation; it's inferred from the default value 2
func add2(a: Int, b = 2, c: Int) = a + b + c
// This is an error here ^ since required params 
// cannot come after optional ones

Calling Functions

Functions are called by using parentheses to pass in arguments, much like you'd expect. In Abra, you may take a named-arguments approach to provide additional clarity. These named arguments may be passed in any order, but you cannot mix named and unnamed parameters (it's all or nothing). It's especially helpful to use named-arguments when passing in a literal value (as opposed to a variable, which can have some intent ascribed to it via its name).

Parameters with default values will be set to those values upon calling the function. The default value will be provided if not enough positional arguments were passed (if calling with unnamed arguments), or if no value is provided as a named argument for that parameter.

func getFullName(firstName: String, lastName: String) = firstName + " " + lastName

getFullName("Turanga", "Leela")

// To help reduce ambiguity ("did the firstName parameter come first, or the 
// lastName?"), you can use named-arguments
getFullName(firstName: "Turanga", lastName: "Leela")

Lambda Functions

There's also a syntax for anonymous lambda functions in Abra, which follows after Typescript's arrow function syntax. Arguments are in parentheses followed by their type annotations, then an arrow (=>) and then either a single expression or a block.

val add = (a: Int, b: Int) => a + b
val mult = (a: Int, b: Int) => {
  var product = a
  for i in range(0, b) {
    product = product + a
  }
  product
}

Functions in Abra are first-class citizens, so they can be passed as arguments to other functions. When passing lambdas as arguments, the type annotations for arguments are optional.

It's also important to note that regular named functions can also be passed as parameters!

func call(fn: (Int) => Int, number: Int) = fn(number)

call(x => x + 1, 23) // Returns 24

func increment(value: Int) = value + 1
call(increment, 23) // Also returns 24

A function will satisfy a function type signature if all of its required arguments match; any optional arguments are not typechecked against the type signature:

func call(fn: (Int) => Int, number: Int) = fn(number)

call((x, y = 1) => x + y + 1, 22) // Returns 24
//       ^ The argument y will always be set to the default value

Recursive Functions

Abra has support for recursive functions, but the function must have an explicit return type annotation. For example:

func fib(n: Int): Int {
//                ^ Without this, an error would be raised
  if (n == 0) {
    0
  } else if (n == 1) {
    1
  } else {
    fib(n - 2) + fib(n - 1)
  }
}

Creating Types

You can create your own type in Abra by using the type keyword, giving it a name, and defining its shape. Here we can see a type representing a person, with 2 fields, firstName andlastName, each of type String.

type Person {
  firstName: String
  lastName: String
}

When a type is declared in this way, there will also be a function declared with the same name as the type, which has arguments that match that type's fields. This is like a constructor in other languages, and is used to create new instances of that type. Much like how regular function work (see the section above on Functions), these parameters can be passed in any order.

val leela = Person(firstName: "Turanga", lastName: "Leela")

You can also specify default values for a type's fields. These will become optional arguments in the constructor function for that type.

type Person {
  firstName: String = "Turanga"
  lastName: String
}

val leela = Person(lastName: "Leela")
println(leela.firstName) // Prints "Turanga", the default value

Fields on an instance of a type can be accessed using the dot operator, much like you may be used to from other languages. You can also use the same idea to update an instance's fields.

val leela = Person(firstName: "turanga", lastName: "leela")
val fullName = leela.firstName + " " + leela.lastName

leela.firstName = "Turanga"
leela.lastName = "Leela"

Methods on Types

Methods can also be declared within a type declaration. Functions with self as the first parameter will be instance methods, and functions without self will bestatic methods.

type Person {
  name: String
  
  func yellName(self) = self.name.toUpper() + "!"
  
  func isLeela(name: String) = name == "Leela" || name == "Turanga Leela"
}

val leela = Person(name: "Leela")
println(leela.yellName()) // Prints "LEELA!"

Person.isLeela(leela.name) // true

Control Flow

If/Else

Conditional branching logic works in Abra as you might expect, similarly to standard C-like languages: a boolean expression is evaluated for truth or falseness, and then the applicable branch is run. Abra does not have implicitly "truthy" or "falsey" values; a strict boolean or Optional type must be passed (more on the use of Optional values in if-conditions later on).

val value = 24

if (value > 30) {
  // Do something
} else if (value > 20) {
  // Do something else
} else {
  // Fallback option
}

Where Abra differs from some other languages is in its inclusion of if/else expressions. In other words, an if/else-block can be used anywhere an expression can be used, and the last expression in the block will be its final value (this is similar to functions' return values).

When an if/else block is treated as an expression, all branches must result in values of the same type. Branches having different types will be raised in a type error.

val tempCelsius = 24
val desc = if (tempCelsius > 30) {
  val emoji = "🥵"
  "Way too hot! " + emoji
} else if (tempCelsius < 5) {
  val emoji = "🥶"
  "Too cold! " + emoji
} else {
  "This is perfect!"
}
desc  // "This is perfect!"

val errorExample = if (tempCelsius > 30) {
  "Too hot!"
} else {
  tempCelsius + 1  // This will result in an error, since both branches have different types
}

It's also allowed to have an if expression without an else clause. This will produce a value whose type is wrapped in an Optional (see Types for more detail), since we cannot be certain a value is indeed present.

val tempCelsius = 24
val desc: String? = if (tempCelsius > 5 && tempCelsius < 30) {
  "Pretty good"
}

Loops

While Loops

Abra has while-loops, which encapsulate a block of logic that is meant to run until a given condition is no longer true. This is identical to how while-loops function in other C-like languages. The break keyword can be used to exit a loop early, even if the loop condition may still be true.

var a = 0
while a < 10 {
  a = a + 1
}
a  // 10

var b = 0
while b < 10 {
  b = b + 1
  if b == 3 {
    break
  }
}
b  // 3

For Loops

Abra has for-loops, which encapsulate a block of logic that is meant to run for each item in a specified collection. The collection can either be specified statically, or as any other expression. The range builtin function comes in handy here:

var sum = 0
for num in [1, 2, 3, 4] {
  sum = sum + num
}
sum  // 10

var product = 0
for num in range(1, 5) {
  product = product * num
}
product  // 24

In addition to specifying the iteratee variable (num in the above example), you can also specify an index binding. The index binding will be a number equal to the number of times the loop has repeated (zero-indexed).

/*
  This loop will print the following:
  Number: 5, index: 0
  Number: 6, index: 1
  Number: 7, index: 2
*/
for num, i in range(5, 8) {
  println("Number: " + num + ", index: " + i)
}

Much like the while-loop, the break keyword can be used to exit a loop early, even if the collection hasn't been fully iterated through.

Convenience variables

Oftentimes it's useful in control structures to have a convenient way to access the evaluated condition. For cases where the condition is a boolean it might not be apparent, but it is useful when the condition is an Optional.

val arr = [1, 2, 3]
if arr[4] |number| {  // `number` will equal arr[4] if it exists
  println("The number was " + number)
} else {
  println("There was no number at index 4")
}

If this is done for a boolean condition, the variable is guaranteed to always be true, so while it is possible to write code such as the following, it's not very useful:

if a > 4 |res| {
  println("Res will always be true, otherwise we couldn't have gotten here")
}

This construct can be used in while loops as well!

val arr = [1, 2, 3]
var idx = 0
while arr[idx] |number| {
  println("Got number: " + number)
  idx += 1
}