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 two forms, with and without the else fragment:

  1. let condition = true
  2. if condition then
  3. # ...
  4. end
  5. if condition then
  6. # ...
  7. else
  8. # ...
  9. 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 block is omitted and the condition evaluates to false, the expression will result in nil.

  1. let condition = false
  2. let x = if condition then
  3. 1
  4. else
  5. 2
  6. end
  7. std.assert(x == 2)
  8. x = if condition then
  9. 3
  10. end
  11. std.assert(x == nil)

Functions

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

  1. let fun = function ()
  2. # ...
  3. end
  4. function fun()
  5. # ...
  6. 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.

  1. function takes_one(x)
  2. # ...
  3. end
  4. function takes_two(x, y)
  5. # ...
  6. end
  7. takes_one(1)
  8. 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:

  1. function fun(x)
  2. if x >= 2 then
  3. return # implicitly returns `nil`
  4. else
  5. return "lower than 2"
  6. end
  7. end
  8. function fun(x)
  9. if x < 2 then
  10. "lower than 2"
  11. end
  12. end

Hush implements lexical scoping#Lexical_scope_vs._dynamic_scope_2), 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:

  1. function adder(x)
  2. let y = x + 1
  3. return function (z) # `return` may be ommited here
  4. y + z # captures `y` from the parent scope.
  5. end
  6. end
  7. std.assert(adder(1)(2) == 4)

Closures may even mutate the captured variables:

  1. let x = 0
  2. function increment()
  3. x = x + 1
  4. end
  5. increment()
  6. increment()
  7. increment()
  8. 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).

  1. function factorial(n)
  2. if n == 0 then
  3. 1
  4. else
  5. n * factorial(n - 1)
  6. end
  7. end
  8. 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.

  1. let dict = @[
  2. value: 5,
  3. method: function()
  4. # `self` is a reference to the dictionary which contains the function, if any.
  5. std.print(self)
  6. end
  7. ]
  8. dict.method() # @[ "value": 5, "method": function<...> ]
  9. # Isolate the method from the object, which will cause `self` to be `nil`:
  10. let method = dict.method
  11. method() # nil
  12. # But we can bind it back to the object using `std.bind(obj, method)`:
  13. method = std.bind(dict, dict.method)
  14. method() # @[ "value": 5, "method": function<...> ]

While loops

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

  1. let condition = true
  2. let i = 0
  3. while condition do
  4. condition = false
  5. i = i + 1
  6. end
  7. 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.
  1. # A function to generate an iterator to the given array.
  2. function iter(array)
  3. let i = -1
  4. let len = std.len(array)
  5. function ()
  6. i = i + 1 # captures `i`, incrementing it on every call.
  7. if i == len then # check if we reached the captured `len`.
  8. @[ finished: true ]
  9. else
  10. @[ finished: false, value: array[i] ]
  11. end
  12. end
  13. end
  14. let array = [1, 2, 3]
  15. let sum = 0
  16. for item in iter(array) do
  17. sum = sum + item
  18. end
  19. 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:

  1. let sum = 0
  2. for i in std.range(1, 4, 1) do
  3. sum = sum + i
  4. end
  5. std.assert(sum == 6)

Break statement

One may also interrupt loops using the break statement:

  1. while true do # this will not run forever.
  2. if 1 + 2 < 4 then
  3. break
  4. end
  5. 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.