Control Flow and Functions

If expressions

In Hush, conditionals expect a condition of type bool. Attempt to use values of other types, such as nil, will result in a panic.

If expressions may assume three forms, with and without the else or elseif fragment:

let condition = true

if condition then
	# ...
end

if condition then
	# ...
else
	# ...
end

if condition then
	# ...
elseif condition then
	# ...
end

As they are expressions, they will evaluate to whatever is the value resulting in the last statement of the executed block. If the else/elseif block is omitted and the condition evaluates to false, the expression will result in nil.

let condition = false

let x = if condition then
	1
else
	2
end

std.assert(x == 2)

x = if condition then
	3
end

std.assert(x == nil)

Functions

Functions are first class citizens, which means they are values like any other. The following are equivalent:

let fun = function ()
	# ...
end

function fun()
	# ...
end

They must declare how many arguments they expect, which is enforced when calling a function. Calling a function with less or more arguments than expected will result in a panic.

function takes_one(x)
	# ...
end

function takes_two(x, y)
	# ...
end

takes_one(1)
takes_two("a", 2)

Contrary to Lua, functions in Hush always return a single value, which is the result of the last statement in their body. They also may return early with the return keyword. The following are equivalent:

function fun(x)
	if x >= 2 then
		return # implicitly returns `nil`
	else
		return "lower than 2"
	end
end

function fun(x)
	if x < 2 then
		"lower than 2"
	end
end

Hush implements lexical scoping, which means variables are enclosed in the body in which they are declared, just like in Python and Lua. It also supports closures, which are functions that capture variables from the enclosing scope:

function adder(x)
	let y = x + 1

	return function (z) # `return` may be ommited here
		y + z # captures `y` from the parent scope.
	end
end

std.assert(adder(1)(2) == 4)

Closures may even mutate the captured variables:

let x = 0

function increment()
	x = x + 1
end

increment()
increment()
increment()

std.assert(x == 3)

Functions can also be recursive. As they are values, recursive functions are actually closures on themselves (they capture the variable to which they are assigned).

function factorial(n)
	if n == 0 then
		1
	else
		n * factorial(n - 1)
	end
end

std.assert(factorial(5) == 120)

Self

Hush provides one further facility for functions: the self keyword. When calling a function inside a dictionary using the dot operator, self will be an alias to that dictionary. If the function is called through other means, self will be nil. This is frequently used in object oriented code.

let dict = @[
	value: 5,

	method: function()
		# `self` is a reference to the dictionary which contains the function, if any.
		std.print(self)
	end
]

dict.method() # @[ "value": 5, "method": function<...> ]

# Isolate the method from the object, which will cause `self` to be `nil`:
let method = dict.method
method() # nil

# But we can bind it back to the object using `std.bind(obj, method)`:
method = std.bind(dict, dict.method)
method() # @[ "value": 5, "method": function<...> ]

While loops

While loops are statements, and therefore cannot be used as expressions.

let condition = true
let i = 0

while condition do
	condition = false
	i = i + 1
end

std.assert(i == 1)

For loops

For loops are also statements, but opposed to While loops, they do not expect a boolean condition. First, they expect a variable name, which will be scoped to the loop's body. Second, they expect an iterator function.

An iterator function is a function that may be called repeatedly without arguments, and always returns a dictionary with at least one field:

  • finished: a boolean indicating whether the loop should stop.
  • value: the value to be assigned to the loop variable. May be omitted if finished is true.
# A function to generate an iterator to the given array.
function iter(array)
	let i = -1
	let len = std.len(array)

	function ()
		i = i + 1 # captures `i`, incrementing it on every call.

		if i == len then # check if we reached the captured `len`.
			@[ finished: true ]
		else
			@[ finished: false, value: array[i] ]
		end
	end
end

let array = [1, 2, 3]
let sum = 0

for item in iter(array) do
	sum = sum + item
end

std.assert(sum == 6)

Fortunately, the iter function defined above is present in the standard library, as std.iter(collection). For numeric iterations, the standard library also supplies the std.range(from, to, step) function, which returns an iterator:

let sum = 0

for i in std.range(1, 4, 1) do
	sum = sum + i
end

std.assert(sum == 6)

Break statement

One may also interrupt loops using the break statement:

while true do # this will not run forever.
	if 1 + 2 < 4 then
		break
	end
end

Wrapping up

With these constructs, you should be able to write basic programs in Hush. Next, we'll learn how to implement proper error handling, as robustness is one of the core values of the language.