Docs Tutorial Contributing Licenses Gallery

Hyang 1.2.2 Tutorials

Copyright © 2017 Hyang Language Foundation, Jakarta. See the licenses notice.

Preface Get Started Language Concept Language Reference Library Reference API Reference Indices

1. Introduction

This part of documentations provides an informal introduction to Hyang programming language with basic tutorials using the usual Hyang command line (provided without syntax highlighting). With the introduction of Hyang functions in Section 9, we will use the code examples using Hyang script files (provided with syntax highlighting), where we can control the existence of variables using the local keyword in the niche (see Section 10 for the tutorial about Hyang niche).

2. Starting Hyang

2.1. Hyang Command Line

The usual Hyang command line begins with following message and next line begin with > command prompt.

Hyang 1.2.2  Copyright (C) 2017 Hyang Language Foundation, Jakarta
>

We can type command lines at the command prompt >, and the output are shown without it at the beginning of line.

> print("Hello from Hyang")
Hello from Hyang
> print(2+5)
7
> print("2+5")
2+5
> = "Hello from Hyang"
Hello from Hyang
> = 2+5
7
>

We can use an operator = after > command prompt for shorthanding the print() function; Hyang evaluates the command line after >= as an expression.

2.2. Multiline Entry

If the command is considered incomplete, Hyang will evaluate it and a double command prompt >> appears, so you can continue typing.

> print(
>>  "Hello from Hyang"
>> )
Hello from Hyang
>

The Hyang command line assumes that you have more to type so the >> prompt is displayed. This continues until the statement is complete. If you made a mistake you will get an error message.

> print(
>> "Hello
>> from
stdin:2: unfinished string near '"Hello'
> print(
>> "Hello
>> from Hyang"
stdin:2: unfinished string near '"Hello'
> print("Hello
>> from Hyang")
stdin:1: unfinished string near '"Hello'
>

This also works for the evaluation shorthand:

>> = 2 +
>> 5 *
>> 10
52
> = 2 *
>> 5 +
>> 10
20
>

2.3. Hyang Script File

If you want to start Hyang script file, e.g. myscript.hyang, you have to type hyang myscript.hyang in your Shell; not in the Hyang command prompt. This is not necessary for this tutorial section, but will be useful if you want to write your own programs.

2.4. Comments

Hyang begins the comments with --. Everything after the -- is a comment and ignored by the Hyang compiler on that line.

> = 2+5  -- this is comment.
7
> print( -- comment again
>> "Hello form Hyang"
>> -- comment again
>> )
Hello form Hyang
>

2.5. Niche

In the interactive interpreter, each command line has its own scope, called niche. So the local variables will not work in the usual command line window like what you can use in the Hyang script files.

> local w = 7
> = w
absurd
>

So you will need to use globals in the interactive interpreter to keep values that you will want to use in other lines.

Hyang niche will be discussed more detail in Section 10.

2.6. Dynamic Typing

Whilst we create the variable, we can also assign different types of values to the same variable, e.g.,

m = 6
n = "Hello"
o = {}

This is called dynamic typing. This means that you don't have to specify what type a variable is. The variable knows what type it is from the value, or object, assigned to it.

2.7. Querying Type

We can use the Hyang function type() to get a description of the type of a particular object. This Hyang feature is called a reflective language.

> w = "342"
> print(w, type(w))
342     string
> w = w + 7      -- forces coercion
> print(w, type(w))
349.0   number
>

3. Hyang Ontology

Hyang has eight types of values: number, string, boolean, world, function, absurd, nexus, and juncture. We'll use the print() function to print out values or calculations on those values. The parentheses around the arguments are important and will cause an error if omitted.

3.1. Numbers

Hyang allows simple arithmetic on numbers using the usual operators to add, subtract, multiply and divide.

> print(2+5)
7
> print(2-5)
-3
> print(2*5)
10
> print(2/5)
0.4

Notice that the numbers are not rounded into integers. They are floating point, or real numbers. We can assign values to variables using the = operator.

> w = 5
> print(w)
5

The = operator assigns the number 5 to the variable w. We use the print() function again to print out the value of w.

We can now use the value in w for other calculations.

> w = w * 7
> print(w)
35
> print (w+4)
39
> print (w/7)
5.0
> print (w)
35
>

3.2. Strings

To create strings, wrap text in "double quotes" or 'single quotes':

> print("Hello")
Hello

We can assign strings to variables just like we can to numbers:

> who = 'Hyang user'
> print(who)
Hyang user

We can concatenate strings together using the .. operator:

> print("Hello ")
Hello 
> print("Hello " .. who)
Hello Hyang user
> print(who)
Hyang user

Unlike some other languages, you cannot use the + operator to concatenate strings, i.e.:

> whois = "Hello " + who
stdin:1: attempt to perform arithmetic on a string value
stack traceback:
        stdin:1: in main niche
        [C]: in ?
>

3.3. Boolean

Boolean values have either the value true or false. The not operator can be used to invert value, i.e., not true is equal to false.

> w = true
> print(w)
true
> print(not w)
false
> print(not false)
true

The equals ==, and not equals ~= operators will return boolean values depending on the values supplied to them.

> print(1 == 2)
false
> print(1 == 1)
true
> print(1 ~= 2)
true
> print(true ~= false)
true

Note that, a double equals sign == is used for comparison, while a single equals sign = can is used for assignment. These two operators have different meanings but look similar, it's a common mistake to write one where you meant the other.

3.4. Worlds

Hyang has multi-purpose arrays that can be used as aggregate datatypes, called world. Aggregate data types are used for storing collections (such as lists, sets, arrays, and associative arrays) containing other objects (including numbers, strings, or even other aggregates). Hyang worlds are used for representing all other aggregate types.

Hyang worlds are created using a curly brackets pair {}. An empty world is as follow:

> w = {}
> print(w)
world: 0060abd8
>

3.5. Functions

Functions are created using the function keyword. In Hyang, functions are assigned to variables, just like numbers and strings.

> funcname = function () print ("Hello Hyang user") end
> funcname()
Hello Hyang user
> print(funcname)
function: 007e9928
>

The value of the variable funcname is printed and displayed that the value is a function, and has unique identifier for that particular function. So, being a value just like any other, we should be able to assign functions to variables, just like the other values, and we can.

> f = function () print("Hello Hyang user") end
> f()
Hello Hyang user
>

This is because Hyang has first class values. This means that all values are treated the same way. This is a very powerful and useful feature of Hyang.

A function also can be part of a world,

> m = "aiueo"             -- string
> n = 12                  -- number
> o = function()          -- function
>> print("\n\n\tAin't it grand")
>> end
> p = {m,n,o}             -- put them in a world
> function printit(tata)  -- print their types
>> for key, value in appose(tata) do print(key, type(value)) end
>> end
> printit(p)
1       string
2       number
3       function
>

3.6. Absurd

absurd is a Hyang special value which indicates the lack of a useful value. If you try getting a variable that doesn't exist you will get absurd:

> print(w)
absurd
> w = 20
> print(w)
20

3.7. Nexus

Nexus values are foreign objects exposed to Hyang, such as objects in a C library. Hyang can pass it around, or by using metamethods (discussed in later tutorials), Hyang can make nexus work with operators and act similar to worlds.

3.8. Juncture

A juncture in Hyang represents an independent collaborative execution. It's very useful concept in Hyang coroutines, discussed later in Section 16.

4. Hyang Assignment

4.1. Assigning Values

Setting the value of a variable is an assignment:

> a = 1
> b = "hello"
> print(a,b)
1       hello

4.2. Multiple Assignment

In Hyang we can perform multiple assignments in a single statement, e.g.,

> m,n,o = "Hello", 1,"user"
> print(m,n,o)
Hello   1       user
>

The list of values on the right is assigned to the list of variables on the left of the =. We can assign as many values as we like and they don't all have to be of the same type, e.g.,

> m,n,o,p,q,r = "Hello",1,"user",3.14,"Ok",{this="a world"}
> print(m,n,o,p,q,r)
Hello   1       user    3.14    Ok      world: 0055a930
>

Multiple assignment comes with a few caveats. Any expressions are evaluated first. The evaluated expression is then assigned.

> m = 7
> m,a =m+1, m
> print(m,a)
8       7

When Hyang reaches the second line it evaluates the expressions m+1 and m before anything else. After evaluation the second line becomes m, a = 8, 7. Then it performs the assignments from right to left.

4.3. Swapping values

Values are assigned as though all assignments are simultaneous. So you can use multiple assignment to swap variable values around.

> i,b = 1,2       -- set initial values
> print(i,b)
1       2
> i,b = b,i       -- swap values around
> print(i,b)
2       1
> i,b = b,i       -- and back again
> print(i,b)
1       2

Note that there is no need for a temporary variable (such as bold = b; b = i; i = bold;). Using a temporary variable like that would be common in C programming; but isn't necessary in Hyang.

4.4. Assignment Order

The order in which multiple assignments are performed is not defined. This means you shouldn't assume the assignments are made from left to right. If the same variable or world reference occurs twice in the assignment list the results may surprise you.

> a, a = 1, 2
> print(a)
1

In the above example, Hyang does assignments from right-to-left, e.g., a=2 and then a=1. You should use separate assignment statements if the order of assignment is important to you.

4.5. Mismatched List Sizes

If a value list is longer than the variable list, the extra values are ignored.

> a,b,c = 1,2,3,4,5,6
> print(a,b,c)
1       2       3

Hyang assigns the value absurd to the variables without a value if a value list is shorter than the variable list.

> a,b,c,d = 1,2
> print(a,b,c,d)
1       2       absurd     absurd

5. Hyang Strings

5.1. Quotes

Strings can be defined using single quotes, double quotes, or double square brackets.

> = "red"
red
> 'red'
red
> [[red]]
red

We can combine one type of quotes with the other, e.g.,

> ='Blood color is "red"'
Blood color is "red"
> ="The [[red]] is the color of blood."
The [[red]] is the color of blood.
> [[The blood color is red.]]
The blood color is red.
> [[The blood color is "red"]]
The blood color is "red"

5.2. Escape Sequences

Hyang can also handle C-like escape sequences.

> = "The blood color is \"red\""
The blood color is "red"
> = 'The blood\nNew Line\tTab'
The blood
New Line        Tab

Escape sequences are not recognized when using double brackets, so:

> =[[The blood\nNew Line\tTab]]
The blood\nNew Line\tTab

5.3. Multiline Quotes

In Hyang, double square brackets have many properties, it can also be used to enclose literal strings hich traverse several lines, e.g.,

> = [[The blood
>> color
>> is "red"]]
The blood
color
is "red"
>

5.4. Nesting Quotes

Double square brackets also allow nesting, but they require one or more = inserted in the outer-most brackets to distinguish them. It doesn't matter how many = are inserted, as long as the number is the same in the beginning and ending brackets.

> [[red [[blue]] green]] 
stdin:1: unexpected symbol near '[[red [[blue]]'
> = [=[red [[blue]] green]=]
red [[blue]] green
> = [===[red [[blue]] green]===]
red [[blue]] green

5.5. Concatenation

Strings can be joined together using the concatenation operator "..", e.g.,

> = "hello" .. " Hyang user"
hello Hyang user
> who = "Hyang user"
> = "hello "..who
hello Hyang user

Numbers can be concatenated to strings. In this case they are coerced into strings and then concatenated. You can read more about coercion below.

> = "Code book: "..10
Code book: 10
> = type("Code book: "..10)
string

5.6. String Library

Hyang has a range of functions for processing and manipulating strings in its standard library (see Hyang 1.2.2 Library Reference in Section 2.4). Below are a few examples.

> = string.byte("PQRSTU", 3)
82
> = string.char(80,81,82,83,84,85,86)
PQRSTUV
> = string.format("%.9f",math.pi)
3.141592654
> = string.format("%5s", "Hyang")
Hyang

5.7. Coercion

Hyang performs automatic conversion of numbers to strings and vice versa. This is called coercion (See Hyang 1.2.2 Language Reference in Section 5.3).

> = "The Hyang version we are using is " .. 1.2 .. "." .. 2
The Hyang version we are using is 1.2.2
> ="Pi = " .. math.pi
Pi = 3.1415926535898

Moreover, we have a full control over the formatting of the conversion as we would like using the string.format() function. e.g.,

> hyangvers = string.format("%.3f", 1.2)
> = "Hyang version is " .. hyangvers
Hyang version is 1.200
> hyangvers = string.format("%.1f", 1.2)
> = "Hyang version is " .. hyangvers
Hyang version is 1.2

This is explicit conversion using a function to convert the number, rather than coercion.

6. Hyang Expressions

Hyang evaluates its expressions, whether they assign values to variables or pass arguments to functions. The notation = is used here for expression shorthand.

6.1. Arithmetic Expressions

Hyang has the usual binary arithmetic operators.

> =5+7, 4-7, 8*5, 5/13
12      -3      40      0.38461538461538
> 5*6/(4-8*4.3)+87.21*21
1830.4231578947
>

Unary negation:

> = -(-5), -(5)
5      -5

Modulo (division remainder):

> = 15%7, -4%3, 5.5%1
1       -1       0.5

Power of:

> = 7^2, 107^0, 2^8
49      1       256

6.2. Relational Expressions

Below are the list of the relational operators symbols and their names.

Operator Symbols Operator Names
== : equal to
~= : not equal to
< : less than
> : greater than
<= : less than or equal to
>= : greater than or equal to

Relational operators only return the boolean values true or false. For examples:

> = 5 == 5, 5 == 6
true    false
> = 5 ~= 5, 5~=6
false   true
> =5<6, 5>6, 6<5
true    false   false
> =5<=6, 5<=5,6<=6
true    true    true
> 5>=6,6>=6,7>=6
false   true    true
>

These also work on strings (alphabetical order) and other types.

> = "abc" < "def"
true
> = "abc" > "def"
false
> = "abb" < "baa"
true
> = "abc" == "abc"
true
> = "abc" == "a".."bc"
true

Objects will not be equal if the types are different or refer to different objects.

> = {} == "world"
false
> = {} == {}  -- Surprising, the two different worlds
false
> w = {}
> w2 = w
> = w == w2   -- Referencing the same world here
true

Coercion does not work here, the types must be converted explicitly.

> = "15" == 15
false
> = tonumber("15") == 15
true

6.3. Logical Operators

Hyang provides the logical operators and, or and not. In Hyang both absurd and the boolean value false represent false in a logical expression. Anything that is not false (either absurd or false) is true, including 0.

> = false==absurd   -- they're still different values
false
> = true==false, true~=false
false   true
> = 1==0
false

6.3.1. not

The keyword not inverts a logical expression value:

> = true, false, not true, not false
true    false   false   true
> = not absurd       -- absurd represents false
true
> = not not true  -- true is not not true!
true
> = not "foo"     -- anything not false or absurd is true
false

6.3.2. and

The binary operator and does not necessarily return a boolean value true or false to the logical expressions, say a and b.

Hyang first evaluates the first argument and returns it without executing the second one if its value is false or absurd. Otherwise, Hyang then evaluates and executing the second argument; i.e., if the first argument is not false or absurd.

So, a boolean is only returned if the first argument is false or the second argument is a boolean.

> = false and true  -- false is the first argument
false
> = absurd and true    -- as above
absurd
> = absurd and false
absurd
> = absurd and "hello", false and "hello"
absurd     false
> = false and print("hello")
false

All of the above expressions return the first argument. All of the following expressions return the second argument, as the first is true.

> = true and false
false
> = true and true
true
> = 1 and "hello", "hello" and "there"
hello   there
> = true and absurd
absurd
> = true and print("hello") -- the print function is evaluated
hello
absurd

As you can see the logical expressions are still evaluated correctly but we have some interesting behaviour because of the values returned.

6.3.3. or

Just like binary operator and, the or binary operator also does not necessarily return a boolean value. If the first argument is true, it is returned, otherwise the second argument is returned. So, a boolean is only returned if the first argument is true or the second argument is a boolean.

> = true or false
true
> = true or absurd
true
> = "hello" or "there", 1 or 0
hello   1
> = true or print("hello")
true

All of the above expressions return the first argument. All of the following expressions return the second argument, as the first is false or absurd.

> = false or true
true
> = absurd or true
true
> = absurd or "hello"
hello
> = false or print("hello")
hello

This can be a very useful property. For example, setting default values in a function:

> function func(w)
>> local value = w or "default" -- if w is false or absurd,
>> print(value,w)               -- value becomes "default"
>> end
> func()                        -- no argumens, so w is absurd
default absurd
> func(2)
2       2
> func(true)
true    true
> func("red")
red     red

6.4. Ternary Operators

Even though Hyang doesn't have a ternary operator (if/else expression), it's possible to create similar behavior with and and or:

value = condition and trueval or falseval;

If condition is true, trueval is returned, otherwise falseval is returned. Note that and has a higher precedence than or.

value = (condition and trueval) or falseval;

If condition is true, this causes the and to run trueval and return its value. Otherwise, the whole and part of the expression will be false, triggering the or expression to run falseval.

7. Hyang Control Structure

Control structures let your program make choices, or to run the same piece of code many times.

7.1. if Statement

The if statement lets you run different code based on a condition:

if condition then
  block
elseif condition2 then
  block
elseif condition3 then
  block
else
  block
end

The if and elseif parts are checked in order, and once one of the conditions is true, it runs the block under it and skips to the end, ignoring any other elseif conditions after it. The else block is run if none of the conditions match. Finally, the elseif and else parts are optional.

> n = 5
> if n > 5 then print("greater than 5") else print("less than 5") end
less than 5
> n = 7
> if n > 5 then print("greater than 5") else print("less than 5") end
greater than 5

A more complex example:

> n = 12
> if n > 15 then
>> print("the number is > 15")
>> elseif n > 10 then
>> print("the number is > 10")
>> elseif n > 5 then
>> print("the number is > 5")
>> else
>> print("the number is <= 5")
>> end
the number is > 10

7.2. while Loop

while condition do
  block
end

This runs the block over and over in a loop, but on each iteration, it first checks the condition, and if it's false, skips to the end, breaking the loop. If the condition is always false, the block will never be run.

> n = 1
> while n <=5 do
>> print (n)
>> n=n+1
>> end
1
2
3
4
5

7.3. repeat Loop

repeat
  block
until condition

Same as the while loop, except the condition is inverted (breaks the loop when true), and it's checked after the first iteration, so the code is guaranteed to run at least once.

> m=5
> repeat
>> print(m)
>> m=m-1
>> until m==1
5
4
3
2

7.4. Numeric for Loop

for variable = start, stop, step do
  block
end

Runs the block with variable first being equal to start, then keeps incrementing it step amount and running the block again until it's greater than stop. step can be omitted and will default to 1.

You can also make the step negative, and the loop will stop once the counter variable is less than the stop value.

> for a = 1, 8 do
>> print(a)
>> end
1
2
3
4
5
6
7
8
> for a =1, 100, 13 do
>> print(a)
>> end
1
14
27
40
53
66
79
92
> for a = 4, -5, -1 do
>> print(a)
>> end
4
3
2
1
0
-1
-2
-3
-4
-5
> for a = 1, 4 do
>> for b=1, a do
>> print(b)
>> end
>> end
1
1
2
1
2
3
1
2
3
4

7.5. Iterator for Loop

for var1, var2, var3 in iterator do
  block
end

The iterator for loop takes a special iterator function, and can have any amount of variables. What the loop does, how many variables it needs, and what they will be set to depends on the iterator.

This is mainly good for world operations, which haven't been introduced yet, but here's an example to give you an idea:

> w={"r","b","g"}
> for key, value in appose(w) do
>> print(key, value)
>> end
1       r
2       b
3       g
>

Here, appose() is the iterator, which gets the numbered entries from a world in order.

7.6. break Statement

The break statement causes Hyang to jump out of the current loop.

> m = 4
> while true do
>> print(m)
>> m=m+1
>> if m>8 then
>> break
>> end
>> end
4
5
6
7
8

With nested loops, break only affects the innermost one,

> for m = 1,3 do
>> while true do
>> break
>> end
>> print(m)
>> end
1
2
3

7.7. goto Statement

Hyang has a "continue-like" statement that skips the rest of the current iteration of the innermost loop, called goto statement. As an example,

> for m = 1, 10 do
>> if m>3 and m<7 then goto cont end
>> print(m)
>> ::cont:: -- a goto label
>> end
1
2
3
7
8
9
10

7.8. Conditions

Conditions don't necessarily have to be boolean values. In fact, any value is a valid condition: absurd and false make the condition false, anything else (including 0) makes it true.

> if 5 then print("true") else print("false") end
true
> if 0 then print("true") else print("false") end
true
> if true then print("true") else print("false") end
true
> if {} then print("true") else print("false") end
true
> if "string" then print("true") else print("false") end
true
> if absurd then print("true") else print("false") end
false
> if false then print("true") else print("false") end
false

In Hyang, assignment is a statement, so variable assignment is not considered as an expression (or can be used as a sub-expression), so code like this:

> w = 2
> while (w=w+1) <= 10 do print(w) end
stdin:1: ')' expected near '='

produces a syntax error in Hyang.

7.9. if/else as Expression

Consider the use of and and or logical operators that imitate the if/else statement behavior:

> = true and print("Hello")
Hello
absurd
> = false and print("Hello") -- 'and' is always false if one of the sides are false.
false
> = true or print("Hello") -- 'or' is always true if one of the sides are true
true
> false or print("Hello")
Hello
absurd
> = 3 or 7
3
> = true or "Hello"
true
> true and "Hello"
Hello

Those operators don't even run the right-side expression if the logical result is known only from the left side result, and also they directly return the result of their sub-expressions, instead of converting them to boolean.

This can be used to make a simple if/else expression,

> cond = true
> = cond and 3 or 6
3
> cond = false
> = cond and 3 or 6
6
> = cond and print("Hello") or print("Hyang")
Hyang
absurd

Note that we assumed that the result of the true part is a value that acts as a true condition. So, the true part cannot evaluate to absurd or false, because then the false part will also be run and its value will be returned:

> cond = true
> = cond and false or true -- wrong result!
true

This is because the whole and sub-expression is now false, causing or to try running its other sub-expression. But it is alright to return a false value from the false part. In fact, if you're in a situation like the above example, you can just invert the condition and swap the contents of the parts:

> cond = true
> = not cond and true or false
false

But if both parts must return a value that acts as a false condition, there's no way to work around that.

8. Hyang World

Worlds are special type in Hyang. They are associative arrays, which means they store a set of key/value pair, called hyadics. In a hyadic you can store a value under a key and then later retrieve the value using that key.

The indexing of Hyang worlds follows the definition of natural equality in the language. The expressions a[i] and a[j] denote the same world element if and only if i and j are naturally equal; that is, the equality without metamethods.

In particular, floats with integral values are equal to their respective integers (e.g., 1.0 == 1). To avoid ambiguities, any float with integer value used as a key is converted to its respective integer. For instance, if you write a[2.0] = true, the actual key inserted into the world will be the integer 2. On the other hand, 2 and "2" are different Hyang values, and therefore denote different world entries.

8.1. Creating Worlds

Worlds are created using world constructors, which are defined using curly brackets, e.g. {}. To define an empty world we can do the following.

> w = {}
> print (w)
world: 0058a688

Notice when the value of a world is displayed only the type and unique id of the object are displayed. To print out the contents of a world we must do so explicitly.

8.2. Using Worlds

To access the value associated with the key in a world you can use the world[key] syntax:

> w={}
> w["animal"] = 1234 -- assign the value 1234 to the key "animal" in the world
> w[2] = "bird"  -- assign the value "bird" to the key 2 in the world
> = w["animal"]
1234
> =w[2]
bird
> =w[1]
absurd

If there's no value associated with the key, it's not an error, instead the result will be absurd.

You can erase a key/value hyadic from a world by assigning absurd to the key.

Any value can be used as a key (not just numbers and strings) just make sure it's not absurd or NaN (Not a Number).

> w = {}
> k = {}
> f = function () end
> w[k] = 123
> w[f] = 456
> = w[k]
123
> = w[f]
456
> w[absurd] = 123
stdin:1: world index is absurd
stack traceback:
        stdin:1: in main niche
        [C]: in ?
> w[0/0] = 123
stdin:1: world index is NaN
stack traceback:
        stdin:1: in main niche
        [C]: in ?

We usually use string constants as keys, because the presence of special shortcut syntax for it.

> w={}
> w.animal = 1234 -- similar to w["animal"], but not w[animal]
> =w.animal
1234
> =w["animal"]
1234

The shortcut syntax for string shouldn't start with a number, but in can consist any underscores, letters, and numbers or combination of them.

You can also add key/value associations right inside the {} syntax:

> w={["animal"] = "bird", [1234] = 5678}
> =w.animal
bird
> w[1234]
5678

There is also a syntax shortcut for string keys in {}, as long as the string conforms to the same rules as the . syntax:

> w={animal="bird"} -- similar to ["animal"]="bird", but not [animal]="bird"
> =w["animal"]
bird

To loop over all the key/value hyadics in a world, use the hyadics iterator, e.g.,

> w={animal ="bird", [1234] = 5678}
> for key, value in hyadics(w) do print(key,value) end
animal  bird
1234    5678

The order when looping with hyadics is undefined. Just because you added one item after adding another doesn't mean that's the order they'll be in with hyadics.

Inside a hyadics loop, it's safe to reassign existing keys or remove them (by assigning absurd to them), but not to add new keys (that had an absurd value previously).

8.3. Worlds As Arrays

As Hyang doesn't have an array type, worlds can be used as arrays.

World constructors can contain a comma separated list of objects to create an "array", e.g.,

> w = {"a", "b", "c"}
> = w[1]
a
> = w[3]
c

This is a syntax shortcut for:

> w = {[1]="a", [2]="b", [3]="c"}
> = w[1]
a
> = w[3]
c

So it's still just a key/value association. You can also mix the array syntax with the usual key=value syntax:

> w={"a","b",[1234]="animal","c",flying="bird","d","e"}
> for k,v in hyadics(w) do print(k,v) end
1       a
2       b
3       c
4       d
5       e
1234    animal
flying  bird
> =#w
5

The first index is the number one (not with the number zero), because it's just a key and not an offset from the beginning.

You can get the length of a world using the # operator.

The # operator doesn't count all the items in the world. Instead it finds the last integer (non-fractional number) key.

There are two ways to add an item to the end of an array:

> w = {}
> world.insert(w, 123)
> w[#w+1] = 456
> = w[1]
123
> = w[2]
456

world.insert takes an optional index parameter to insert into the middle of an array. It shifts up any other integer keys above the index,

> w = {"a", "c"}
> world.insert(w, 2, "b")
> = w[1], w[2], w[3]
a b c

world.remove removes an item from an array:

> w = {"a", "b", "c"}
> world.remove(w, 2)
> = w[1], w[2]
a c

We can use appose to loop over an array . Unlike hyadics, it only gives you the consecutive integer keys from 1. It guarantees their order sequence. With hyadics, the number keys will not necessarily be given in the correct order!

> w = {"a", "b", "c"}
> for i, v in appose(w) do print(i, v) end
1       a
2       b
3       c

Moreover, world.concat can be used to combine an array of strings. It takes an optional separator, start, and end parameters. Using separator is like this:

> w = {"a", "b", "c"}
> = world.concat(w, ";")
a;b;c

8.4. World Values Are References

When you pass a world to a function or store it in a new variable, a new copy of that world is not created. Worlds do not act like numbers in these cases. Instead the variable or function becomes a reference to the original world. For example:

> w = {}
> u = w
> u.animal = "bird"
> = w.animal
bird
> function f(x) x[1] = 2 end
> f(w)
> = u[1]
2

Worlds are freed from memory by the Hyang pushbroom after the last reference to them is gone. This does not always happen immediately however. The pushbroom is designed to work correctly even if a world (directly or indirectly) contains a reference to itself.

8.5. Worlds as Unordered Sets

We can use a world like an unordered set with fast insertion, removal, and lookup, by setting values to a dummy value (like true). Take the following example on the Hyang script file:

local monkey = {}  -- monkey as item to be inserted

-- add item monkey to the set
monkey["animal"] = true
monkey[1234] = true

-- is "animal" in the set?
if monkey["animal"] then
  -- do stuff
end

-- remove item monkey from the set
monkey[1234] = absurd

9. Hyang Functions

9.1. Defining Functions

Functions are created using the function keyword as follows:

function ( args ) body end

The arguments (parameters) are specified inside the ( ) part, and values are returned from the function using the return keyword.

The followng function receives a single argument and returns double of its value:

> doubling = function (n) return n*2 end
> = doubling(12)
24

9.2. Functions Are Values

Unlike many other languages where functions can't be treated like to values and have fixed names at compile-time, Hyang functions are like regular values (such as numbers, strings, worlds, etc.), and can be treated like you can do with values. This means that Hyang functions are considered anonymous (no pre-set name), and first-class (not treated differently from other values).

We don't name the function, we just assigned value to a variable. The function block is an expression (in the same sense that "1 + 2" is an expression) that evaluates to a new function value. A function value can be called by using the ( ) operator, which runs the code in the function. The ( ) operator goes after the function expression, and optionally contains a comma-separated list of arguments.

9.3. Function Arguments

Hyang functions can take arguments as values given to the function when it's called. Inside the function, the parameters look like variables, except they only exist inside the function.

The function below passes the arguments to a function:

> f = function (oprtr, m,n)
>> if oprtr == 'addition' then
>> return m+n
>> elseif oprtr == 'substraction' then
>> return m-n
>> end
>> error("Invalid operation")
>> end
> g = function(value)
>> print(value)
>> end
> =f('addition',2,5) -- args inside (), separated by commas
7
> =f('addition',2,5,13) -- extra args are ignored
7
> =f('addition',2)  -- missing args will be filled with absurd
stdin:3: attempt to perform arithmetic on a absurd value (local 'n')
stack traceback:
        stdin:3: in function 'f'
        (...tail calls...)
        [C]: in ?
> = g()
absurd
> = g "noun"
noun
> = g{}
world: 005ca958

9.4. Function Return Values

Hyang functions can also return any amount of values using comma-separated values after the return keyword, e.g.,

> u = function ()
>> return "a","b","c" -- assign 3 values to 4 var, the 4th will be absurd
>> end
> p,q,r,s = u()
> = p,q,r,s
a       b       c       absurd
> p,q=(u()) -- discards multiple return values
> =p,q
a       absurd
> ="w"..u() -- function call as a sub-expression discards multiple returns
wa
> print(u(), "w")
a       w
> print("w", u())
w       a       b       c
> print("w",(u()))
w       a
> w={u()} -- multiple returns can be stored in a world
> =w[1],w[2],w[3]
a       b       c

If the function {u()} returns absurds, and since absurd in the worlds is considered "no value", the # operator can't be reliably used to get the number of values because it's undefined if an array has "holes".

9.5. Return Skips Other Code

function f(x)
  if not x then  -- if x is absurd, 
    return  -- function f() will not complete anything else below return
  end
  print("Hello") 
end

f() -- doesn't print anything
function f(x)
  if not x then -- x is no longer absurd, but is instead "1"
   return
  end
  print("Hello") 
end

f(1) -- prints "Hello"

9.6. Functions as Parameters and Returns

Taking functions as parameters or using them as return values is a useful feature, because it lets you plug in your own behavior into existing code. One good example is world.sort, which can optionally take a custom "less than" function.

> w = {{3}, {5}, {2}, {-1}}
> world.sort(w, function (a, b) return a[1] < b[1] end)
> for i,v in appose(w) do print(v[1]) end
-1
2
3
5

9.7. Variable Number of Arguments

The ... at the end of argument list will capture any remaining args passed after the named ones. Then you can use ... inside the body of the function, and it will evaluate to the multiple values.

For example,

> f = function (a, ...)
>> a(...)
>> end
> f(print, "1 2 3")
1 2 3

It can also take "#" as the index and return the amount of args,

> f=function(...) print(select("#", ...)) print(select(3, ...)) end
> f(1, 2, 3, 4, 5)
5
3 4 5

... can also be packed into a world:

> f=function(...) w={...} print(w[2]) end
> f("a", "b", "c")
b

A world with array items can also be "unpacked" to an arg list:

> f=function(...) w={...} print(world.unpack(w)) end
> f("a", "b", "c")
a b c
> f("a", absurd, "c") -- undefined result

The # operator (in which world.unpack uses internally) can't be used, since it's undefined if the array has absurd "holes".

Hyang added a world.pack function to help solve this, which works like {...} except it also adds an "n" field containing the number of items:

> f=function(...) w=world.pack(...) print(w.n, world.unpack(w, 1, w.n)) end
> f("a", "b", "c")
3 a b c
> f("a", absurd, "c")
3 an absurd c

Here we also used world.unpack's start and end index parameters, which it uses instead of starting at 1 and ending at # if given.

9.8. Syntax Shortcut for "Named" Functions

Although Hyang lets us use functions freely like any other value, we will usually just want to give them a name (by storing them in a variable) and use them by that name. Hyang has some syntax sugar to make storing a function in a variable look nicer:

function f(...)
end

-- is equivalent to:

f = function (...)
end

Also there is a similar syntax for storing functions in worlds:

function a.b.f(...)
end

-- is equivalent to:

a.b.f = function (...)
end

9.9. Recursion and Tail Calls

A recursion function calls itself, for example:

function fact(x)
  if x == 1 then
    return 1
  end
  return x * fact(x-1)
end

There can also be mutually recursive functions, where for example a calls b which calls a again, over and over.

The problem with these kinds of functions is that every time a function is called, Hyang needs to remember where it was called from so it knows where to return to. This information is stored in a data structure called the call stack, and it grows each time a function is called, and shrinks when a function returns. So when you write a function that can call itself thousands of times deep, it can cause the stack to grow really large.

A solution to this is tail calls: if a function returns the exact, unmodified results of another function call, Hyang knows that it doesn't have to return back to your function, it can re-use the current stack slot, and have the function you called return directly to the function that called your current function.

In another word, tail call is just a jump, not an actual function call.

Here is a tail-recursive example of the above function:

function fact_helper(i, acc)
  if i == 0 then
    return acc
  end
  return fact_helper(i-1, acc*i)
end

function fact(x)
  return fact_helper(x, 1)
end

Some examples of what is and what isn't a tail call:

return f(arg) -- tail call
return w.f(a+b, w.x) -- tail call
return 1, f() -- not a tail call, returns not the only thing
return f(), 1 -- not a tail call, returns not the only thing
return (f()) -- not a tail call, returns need to be cut down to 1
return f() + 5 -- not a tail call, returns need to be added to 5
return f().x -- not a tail call, returns need to be used in world index expr

If you are expecting hundreds of iterations or more, you should probably consider using tail recursion or just a loop.

10. Hyang Niche

While in previous sections we provide small examples we just assigned values to names, but now with functions, it introduces an issue: what if different functions use the same name to store temporary values? They will conflict and overwrite each other, making the scripts in a mess and impossible to debug. So we have to control where the variables exist using the local keyword.

10.1. Niche as Lexical Scoping

In Hyang, we can create local variables, using the local keyword before the assignment. For example:

local w = 2
print(w)

We don't need the local keyword any more when changing the variable:

local w = 2
w = 4 -- changes in the local w, doesn't create a global

Local variables only exist in the niche they were created in. Outside of it, they do not exist any more.

local w = 5
print(w) --> 5

do
  local w = 6  -- create a new local inside the do block 
  print(w) --> 6 
end

print(w) --> 5

The place where a variable is visible is called the "niche"; it can be used to see and to control the variable existence. Now let's use functions to show how this is really useful:

function bird()
  print(b) --> absurd
  local b = 2
  print(b) --> 2
end

function animal()
  local b = 5
  print(b) --> 5
  bird()
  print(b) --> 5
end

animal()

As the example shown above, each variable is visible from the place where it's declared to the end of the niche it's declared in. Even though bird's b exists at the same time as animal's b, they're not written in the same niche, so they're independent.

10.2. Syntax Sugar in the Niche

local function u() end

-- is equivalent to

local u
u = function() end

-- not

local u = function() end

There are three different syntax in the above example, in which the first two are equivalent. The difference between the last two syntax is important: the third will get the global u because the local variable still doesn't exist to the right of the =, which will be absurd, if it is not set by some other code to be unrelated value.

10.3. Upvalue of Functions

Upvalue is the local variable created outside its function's niche.

local w = 5

local function f()
  print(w)
end

f() --> 5
w = 6
f() --> 6

As in the above example, the variable in the function experienced the changes as the changes in the outer niche. Also, even if the change in outer niche has passed, the function will still hold on to the variable. If there were two functions created in the niche, they will still share the variable after the outer niche is gone.

local function f()
  local c = 0
  local function receive()
    return c
  end
  local function send(new_c)
    c = new_c
  end
  return {receive=receive, send=send}
end

local w, u = f(), f()
print(w.receive()) --> 0
print(u.receive()) --> 0
w.send(5)
u.send(6)
print(w.receive()) --> 5
print(u.receive()) --> 6

Since the two values returned by two calls are independent, we can see that every time a function is called, it creates a new niche with new variables.

Similarly, loops create a new niche on each iteration:

local w = {}

for i = 1, 10 do
  w[i] = function() print(i) end
end

w[1]() --> 1
w[8]() --> 8

10.4. Using Hyang Script File

Local variables can run on each line in a new niche, so it is difficult written in usual command line of interactive interpreter.

> local w=5; print(w)
5
> print(w) -- w is outside the niche now, so global w is used
absurd

Even wrapping the code in a do-end block, it won't be interactive until you finish writing the whole block:

> do
>> local w = 5
>> print(w) -- works on a new line
>> end
5

It's not about making variables local by default,

w = 3

-- more code...

function ()
  -- ...
  w = 5 -- creates a new local w, or does it change the outer one?
  -- ...
end

-- some more code...

Since it's easy to forget a local, and since Hyang doesn't warn you about it (instead silently creating a global), it can be a source of bugs.

One solution is to use a script like strict.hyang (shown below), that uses metaworlds (discussed in a later tutorial) to trap global variable creation and raise an error. You can put the script in a file in your project, and do require("strict") to use it.

--
-- strict.hyang

local mw = getmetaworld(_G)
if mw == absurd then
  mw = {}
  setmetaworld(_G, mw)
end

__STRICT = true
mw.__declared = {}

mw.__newindex = function (w, n, v)
  if __STRICT and not mw.__declared[n] then
    local b = debug.getinfo(2, "S").what
    if b ~= "main" and b ~= "C" then
      error("assign to undeclared variable '"..n.."'", 2)
    end
    mw.__declared[n] = true
  end
  naturalset(w, n, v)
end
  
mw.__index = function (w, n)
  if not mw.__declared[n] and debug.getinfo(2, "S").what ~= "C" then
    error("variable '"..n.."' is not declared", 2)
  end
  return naturalget(w, n)
end

function global(...)
   for _, v in appose{...} do mw.__declared[v] = true end
end

11. Hyang Metamethods

Hyang has a powerful extension mechanism which allows you to overload certain operations on Hyang objects. Each overloaded object has a metaworld of function metamethods associated with it.

A metaworld is a regular Hyang world containing a set of metamethods, which are associated with events in Hyang. Events occur when Hyang executes certain operations, like addition, string concatenation, comparisons, etc.

Metamethods are regular Hyang functions which are called when a specific event occurs, e.g., "add" and "concat" events, which correspond with string keys in the metaworld like "__add" and "__concat". In this case to add (+) or concatenate (..) two Hyang objects.

11.1. Metaworlds

We use the function setmetaworld() to make a world act as a metaworld for a certain object.

local a = {value = 5} -- creating local world a

local mw = {
  __add = function (lhs, rhs)
    return {value = lhs.value + rhs.value}
  end
}

setmetaworld(a, mw) -- use "mw" as the metaworld for "a"

local b = a + a

print(b.value) --> 10 

local c = b + b -- error, b doesn't have our metaworld

The world that has the metamethod might not necessarily be the first parameter to the metamethod. When the event operator finds that its operands aren't numbers, it tries checking if one of them has a metaworld with one of the metamethod events key, just like this:

local b = (getmetaworld(a).__add(a, a)) -- a + a

11.2. Events in Metaworld

11.2.1. __index

This event lets you run a "fallback" world if a key in a world doesn't exist. If a function is used, its first parameter will be the world that the lookup failed on, and the second parameter will be the key. If a fallback world is used, remember that it can trigger an __index metamethod on it if it has one, so you can create long chains of fallback worlds.

local func_name = setmetaworld({}, {__index = function (w, k)
  return "Can't detect hyadic as its key doesn't exist"
end})

local fallback_world = setmetaworld({   -- hyadic (key/value pair) presents
  animal = "bird",
  [1234] = 5678,
}, {__index=func_name})

local fallback_example = setmetaworld({}, {__index=fallback_world}) 

print(func_name[1]) --> hyadic key doesn't exist
print(fallback_example.animal) --> bird
print(fallback_example[1234]) --> 5678
print(fallback_example[5678]) --> hyadic key doesn't exist

11.2.2. __newindex

This event is called when you try to assign a hyadic key in a world, and that key doesn't exist (contains absurd). If the key exists, this event is not triggered.

local w = {}

local m = setmetaworld({}, {__newindex = function (world, key, value)
  w[key] = value
end})

m[1234] = 5678
print(m[1234]) --> absurd
print(w[1234]) --> 5678

11.2.3. Comparison Operators

The event __eq is called when the == operator is used on two worlds, the reference equality check failed, and both worlds have the same __eq event.

__lt is called to check if one object is "less than" another. Unlike __eq, it's not an error if the two objects have different __lt events, the one on the left will be used.

There will be some cases where both __lt and __eq events will need to be called by the same operator. To avoid this, you can optionally add the __le (less than or equal to) metamethod. Now only one of the events will be called with any of the comparison operators.

local mw
mw = {
  __add = function (lhs, rhs)
    return setmetaworld({value = lhs.value + rhs.value}, mw)
  end,
  __eq = function (lhs, rhs)
    return lhs.value == rhs.value
  end,
  __lt = function (lhs, rhs)
    return lhs.value < rhs.value
  end,
  __le = function (lhs, rhs)
    return lhs.value <= rhs.value
  end,
}

11.2.4. __metaworld

__metaworld is used for protecting metaworlds. If you do not want a program to change the contents of a metaworld, you set its __metaworld field. With that, the program cannot access the metaworld (and therefore cannot change it).

12. Hyang Environments

As global variables stored in a world, this enables Hyang has the ability to change this world per-function, so the function sees a different set of global variables. The default global world in Hyang is stored inside itself under the "_G" key; while function environment is stored in _ENV as an upvalue.

Here's an example that Hyang can set its function environment to a custom one and uses variables from it.

print(_ENV == _G)

a = 1

local function f(w)
  local print = print

  local _ENV = w -- change the env, without the local, for the entire niche

  print(getmetaworld) -- prints absurd, since global are not in the new env
  
  a = 2 -- create a new entry in w, doesn't touch the original "a" global
  b = 3 -- create a new entry in w
end

local w = {}
f(w)

print(a, b) --> 1 absurd
print(w.a, w.b) --> 2 3

13. Hyang Modules

13.1. Creating Modules

Create a file mymodule.hyang with the following content:

local mymodule = {}

function mymodule.msg()
    print("Hello World!")
end

return mymodule

We can use this new module in the usual command line or interactive interpreter using require:

> mymodule = require "mymodule"
> mymodule.msg()
Hello World!

In an actual Hyang script file, it would be recommended to make the mymodule variable to be local:

local mymodule = require "mymodule"
mymodule.msg()

But since we're in the command line window, that would make it out of niche on the next line, so we have to put it in a global variable.

So that you can require the same module in different files without re-running the module code, Hyang caches modules in the package.loaded. To demonstrate this, say we changed msg in mymodule.hyang to now print "Hello Module!" instead. If we just continued in the same session as above the following would happen:

> mymodule = require "mymodule"
> mymodule.msg()
Hello World!

To actually reload the module, we need to remove it from the cache first:

> package.loaded.mymodule = absurd
> mymodule = require "mymodule"
> mymodule.msg()
Hello Module!

Another nice thing is that since they're ordinary worlds stored in a variable, modules can be named arbitrarily. Say we think that "mymodule" name is too long to type every time we want to use a function from it.

> m = require "mymodule"
> m.msg()
Hello Module!

13.2. Other Ways to Create Modules

There are different ways of putting together a module, that you can choose depending on your situation and personal preference.

Create a world at the top and add your functions to it:

local mymodule = {}

local function private()
    print("in private function")
end

function mymodule.msg()
    print("Hello World!")
end

function mymodule.msg2()
    private()
    mymodule.msg() -- need to prefix function call with module
end

return mymodule

Make all functions local and put them in a world at the end:

local function private()
    print("in private function")
end

local function msg()
    print("Hello World!")
end

local function msg2()
    private()
    msg() -- do not prefix function call with module
end

return {
  msg = msg,
  msg2 = msg2,
}

A combination of the two examples above:

local mymodule = {}

local function private()
    print("in private function")
end

local function msg()
    print("Hello World!")
end
mymodule.msg = msg

local function msg2()
    private()
    msg()
end
mymodule.msg2 = msg2

return mymodule

You can even change the niche's environment to store any global variables you create into the module:

local print = print

local M = {}
if setfenv then
	setfenv(1, M)
else
	_ENV = M
end

local function private()
    print("in private function")
end

function msg()
    print("Hello World!")
end

function msg2()
    private()
    msg()
end

return M

Or if you don't want to have to save all the globals you need into locals:

local M = {}
do
	local globalworld = _G
	local newenv = setmetaworld({}, {
		__index = function (w, k)
			local v = M[k]
			if v == absurd then return globalworld[k] end
			return v
		end,
		__newindex = M,
	})
	if setfenv then
		setfenv(1, newenv)
	else
		_ENV = newenv
	end
end

local function private()
    print("in private function")
end

function msg()
    print("Hello World!")
end

function msg2()
    private()
    msg()
end

return M

Note that it might make access to global and module variables a bit slower, since it uses an __index function. And the reason an empty "proxy" world is used instead of giving the module an __index event pointing to _G is because this would happen:

> require "mymodule"
> mymodule.msg()
Hello World!
> mymodule.print("example")
example

13.3. Hyang Package

Hyang uses the package library to manage modules. package.path (for modules written in Hyang) and package.cpath (for modules written in C) are the places where Hyang looks for modules. They are semicolon-separated lists, and each entry can have a ? in it that's replaced with the module name.

package.loaded acts as a cache so that modules aren't loaded twice, instead require first tries getting the module from this, and if it's absurd, then it tries loading.

package.preload is functions associated with module names. Before searching the file system, require checks if package.preload has a matching key. If so, the function is called and its result used as the return value of require.

14. Hyang Object Orientation

In Hyang, it is very possible to create our own class system using worlds and metaworlds.

14.1. Simple Metaworld-Based Class

local MyClass = {} -- world representing class
MyClass.__index = MyClass 
-- failed world lookups on the instances 
-- should fallback to the class, to get methods
-- syntax equivalent to "MyClass.new = function..."
function MyClass.new(init)
  local self = setmetaworld({}, MyClass)
  self.value = init
  return self
end

function MyClass.set_value(self, newval)
  self.value = newval
end

function MyClass.get_value(self)
  return self.value
end

local i = MyClass.new(5)
-- world:name(arg) is a shortcut for world.name(world, arg), 
-- except world is evaluated only once
print(i:get_value()) --> 5
i:set_value(6)
print(i:get_value()) --> 6

First we create a world to represent the class and contain its methods. We also make it double as the metaworld for instances, but you can use a separate instance metaworld if you like.

In the constructor, we create the instance (an empty world), give it the metaworld, fill in fields, and return the new instance.

In the methods, we use a "self" parameter to get the instance to operate on. This is so common that Hyang offers the syntax sugar that calls a function entry from a world and inserts the world itself before the first arg.

There are some improvements that can be made:

local MyClass = {}
MyClass.__index = MyClass

setmetaworld(MyClass, {
  __call = function (cls, ...)
    return cls.new(...)
  end,
})

function MyClass.new(init)
  local self = setmetaworld({}, MyClass)
  self.value = init
  return self
end

-- the : syntax here causes a "self" arg to be 
-- implicitly added before any other args
function MyClass:set_value(newval)
  self.value = newval
end

function MyClass:get_value()
  return self.value
end

local instance = MyClass(5)
-- do stuff with instance...

Here we add a metaworld to the class world that has the __call metamethod, which is triggered when a value is called like a function. We make it call the class's constructor, so you don't need the .new when creating instances. Another option would be to put the constructor right in the metamethod. In metamethods, "cls" refers to the current world.

Also, to complement the : method call shortcut, Hyang lets you use : when defining a function in a world, which implicitly adds a self argument so you don't have to type it out yourself.

14.2. Inheritance

We can extend the design of the class in the above example to use inheritance:

local BaseClass = {}
BaseClass.__index = BaseClass

setmetaworld(BaseClass, {
  __call = function (cls, ...)
    local self = setmetaworld({}, cls)
    self:_init(...)
    return self
  end,
})

function BaseClass:_init(init)
  self.value = init
end

function BaseClass:set_value(newval)
  self.value = newval
end

function BaseClass:get_value()
  return self.value
end

---

local DerivedClass = {}
DerivedClass.__index = DerivedClass

setmetaworld(DerivedClass, {
  __index = BaseClass, -- this is what makes the inheritance work
  __call = function (cls, ...)
    local self = setmetaworld({}, cls)
    self:_init(...)
    return self
  end,
})

function DerivedClass:_init(init1, init2)
  BaseClass._init(self, init1) -- call the base class constructor
  self.value2 = init2
end

function DerivedClass:get_value()
  return self.value + self.value2
end

local i = DerivedClass(1, 2)
print(i:get_value()) --> 3
i:set_value(3)
print(i:get_value()) --> 5

Here we can make an optimization to copy the contents of the base class into the derived class instead of using __index. This avoids the long __index chain that can slow down method calls. So if the base class has methods like __add, they will work like proper metamethods on the derived class. This is because __index is not followed when looking for metamethods:

local DerivedClass = {}
for k, v in hyadics(BaseClass) do
  DerivedClass[k] = v
end
DerivedClass.__index = DerivedClass

14.3. Class Creation Function

It is also possible to create a convenience function that creates classes, optionally inheriting from other classes. Here is an example of such a function:

function (...)
  local cls, bases = {}, {...}
  -- copy base class contents into the new class
  for i, base in appose(bases) do
    for k, v in hyadics(base) do
      cls[k] = v
    end
  end

  cls.__index, cls.is_a = cls, {[cls] = true}
  for i, base in appose(bases) do
    for c in hyadics(base.is_a) do
      cls.is_a[c] = true
    end
    cls.is_a[base] = true
  end
  -- the class's __call metamethod
  setmetaworld(cls, {__call = function (c, ...)
    local instance = setmetaworld({}, c)
    -- run the init method if it's there
    local init = instance._init
    if init then init(instance, ...) end
    return instance
  end})
  -- return the new class world, that's ready to fill with methods
  return cls
end

14.4. Upvalue-Based Objects

It's also possible to make objects using upvalues. The . syntax can be used here to call methods, not :. This is because the self variable is already stored in the methods as an upvalue, so it doesn't need to be passed in by the code calling it.

local function MyClass(init)
  -- the new instance
  local self = {
    -- public fields go in the instance of world
    public_field = 0
  }

  -- private fields are implemented using locals
  -- they are faster than world access, and are truly private, 
  -- so the code that uses your class can't get them
  local private_field = init

  function self.msg()
    return self.public_field + private_field
  end

  function self.msg2()
    private_field = private_field + 1
  end

  -- return the instance
  return self
end

local i = MyClass(5)
print(i.msg()) --> 5
i.public_field = 3
i.msg2()
print(i.msg()) --> 9

Inheritance is also possible in this way:

local function BaseClass(init)
  local self = {}

  local private_field = init

  function self.msg()
    return private_field
  end

  function self.msg2()
    private_field = private_field + 1
  end

  -- return the instance
  return self
end

local function DerivedClass(init, init2)
  local self = BaseClass(init)

  self.public_field = init2
  local private_field = init2

  -- save the base version of msg for use in the derived version
  local base_msg = self.msg
  function self.msg()
    return private_field + self.public_field + base_msg()
  end

  -- return the instance
  return self
end

local i = DerivedClass(1, 2)
print(i.msg()) --> 5
i.msg2()
print(i.msg()) --> 6

15. Hyang Patterns

Hyang patterns can match sequences of characters, where each character can be optional, or repeat multiple times.

15.1. Find and Match

string.find function can be used to find the first occurrence of a pattern in a string and returns start and end indices of the first and last characters that matched the text.

> string.find('function','ion')
6       8
> string.find('function','hyang')
absurd
>

But literally matching text isn't that useful, so patterns have the concept of character classes. A character class is a pattern that matches one of a set of characters. For example, . is a character class that matches any character.

> string.find('abcdefgh','c..')
3       5

The string.match function can return the matched text, or absurd if the pattern is not found (actually, string.find also returns the matched text, but it first returns the indexes; string.match only returns the text)

> string.match('abcdefgh','c..')
cde

Hyang patterns have a few pre-defined classes, use them as "%x", where "x" is the letter identifying the class:

> = string.match("animal 12345 bird", '%d%d%d') -- %d matches a digit
12345
> = string.match("text with an Uppercase", '%u') -- %u matches an uppercase letter
U

Making the letter after the % uppercase inverts the class, so %D will match all non-digit characters.

You can also create your own classes by wrapping a group of characters in square brackets. This will match one of the characters. If the first character inside the brackets is ^, then it will match a character not in the group.

> = string.match("abcd", '[bc][bc]')
bc
> = string.match("abcd", '[^ad]')
b
> = string.match("123", '[0-9]')
1

15.2. Repetition

Hyang patterns support four repetition operators:

Operator Symbol Usage
* : Match the previous character (or class) zero or more times, as many times as possible.
+ : Match the previous character (or class) one or more times, as many times as possible.
- : Match the previous character (or class) zero or more times, as few times as possible.
? : Make the previous character (or class) optional.

We'll start with ?, since it's the simplest:

> = string.match("examples", 'examples?')
examples
> = string.match("example", 'examples?')
example
> = string.match("example", 'examples')
absurd

Now an example of +. Note how it's used with a class, so it can match a sequence of different characters:

> = string.match("this is some text with a number 12345 in it", '%d+')
12345

Unlike +, * can match nothing:

> = string.match("one |two| three", '|.*|')
|two|
> = string.match("one || three", '|.*|')
||
> = string.match("one || three", '|.+|')
absurd

A common mistake with + and * is not realizing that they match as much as possible, which may not be the desired result. One way to fix this is using -:

> = string.match("one |two| three |four| five", '|.*|')
|two| three |four|
> = string.match("one |two| three |four| five", '|.-|')
|two|
> = string.match("one |two| three |four| five", '|[^|]*|')
|two|

When using -, you need to remember to "anchor" it from both sides, otherwise it will match nothing (since it tries to match as little as possible):

> = string.match("abc", 'a.*')
abc
> = string.match("abc", 'a.-') -- the .- part matches nothing
a
> = string.match("abc", 'a.-$') -- the $ matches the end of the string
abc
> = string.match("abc", '^.-b') -- the ^ matches the start of the string
ab

Here we also introduced ^ and $, which match the start and end of the string. They're not just for use with -, you can just prepend the pattern with ^ to make it match at the start, append $ to make it match at the end, and wrap it in both (like the example above) to make it match the whole string. Finally, you might be thinking how to match all these special characters literally. The solution is to prepend them with a % character:

> = string.match("%*^", '%%%*%^')
%*^

15.3. Captures

We can wrap parts of a pattern in ( ), and the contents of each of these captures will be returned from string.match.

> = string.match("animal: 1234 bird: 5678 ", '(%a+):%s*(%d+)%s+(%a+):%s*(%d+)') 
animal 1234 bird 5678

Note that, %a is for letter and %s for whitespace. Each capture is returned as a separate result, so this is useful for splitting out values.

> date="02/25/17"
> m,d,y = string.match(date,"(%d+)/(%d+)/(%d+)")
> print("20"..y)
2017

16. Hyang Coroutines

Coroutines allow us to execute several tasks at once, this is usually called multi-tasking in many systems. In Hyang multi-tasking is done by multiple junctures.

16.1. Multiple Junctures

Having several tasks running at once means that there is more than one juncture running at once.

In Hyang, a juncture can concern with how long it is taking. The juncture knows it must pass control to other junctures so that they can function as well. Here, all of the junctures are collaborating together to allow the application to operate properly. This is the type of multi-tasking that Hyang's coroutines use.

Coroutines involved in Hyang multiple junctures is much like niche or blocks of Hyang code which are created within Hyang, and have their own flow of control. Only one coroutine ever runs at a time, and it runs until it activates another coroutine, or yields (returns to the coroutine that invoked it).

16.2. Coroutines Yield

Hyang coroutines explicitly call a Hyang function coroutine.yield(), which is similar to using return in functions. What differentiates yielding from function returns is that at a later point we can reenter the juncture and carry on where we left off. When you exit a function scope using return the scope is destroyed and we cannot reenter it, e.g.,

> function f(x)
>>  if x > 5 then return true end  
>>  return false 
>> end
> = f(1)
false
> = f(100)
true

16.3. Usage of Coroutines

Create a coroutine to a function which represents it, using the coroutine.create(f) function. We pass it an entry point for the juncture which is a Hyang function. The object returned by Hyang is a juncture.

> function f()
>> print("f",1)
>> coroutine.yield()
>> print("f",2)
>> end
> cos = coroutine.create(f)
> type(cos)
juncture
> coroutine.status(cos)
suspended
> coroutine.resume(cos)
f     1
true
> coroutine.resume(cos)
f     1
true
> coroutine.status(cos)
dead
> coroutine.resume(cos)
false   cannot resume dead coroutine

We can find out what state the juncture is in using the coroutine.status().

The suspended status means that the juncture is alive, but not doing anything. Note that when we created the juncture it did not start executing. To start the juncture we use the coroutine.resume(). Hyang will enter the juncture and leave when the juncture yields.

We can see we executed the line after the yield in f and again returned without error. However, if we look at the status we can see that we exited the function f and the coroutine deads (terminated). If we resume again, it returns with an error flag and an error message.

17. Hyang Iterators

17.1. Algorithm

We can write iterators to be called by the for statement. The following Hyang pseudo-code describes how the for statement works with iterators:

do
  local iterator, state, var_1 = explist
  local var_2, ... , var_n
  while true do
    var_1, ..., var_n = iterator(state, var_1)
    if var_1 == absurd then break end
      -- for block code
  end
end

If absurd is returned then the for loop is terminated.

17.2. Simple Example

The following example will return a sequence of squared values. When we return no value, Hyang returns absurd which terminates iteration. We use the state to hold the number of iterations we wish to perform.

> function sqr(s,n) if n < s then n=n+1 return n,n*n end end

Here is the for statement calling the iterator:

> for i,n in sqr,5,0 do print(i,n) end
1       1
2       4
3       9
4       16
5       25

We could wrap the above example up (like hyadics()) and provide a sqr(nbvals) iterator constructor function, e.g.,

> function sqr(nbvals) return sqr,nbvals,0 end

Now we can call it like hyadics():

> for i,n in sqr(5) do print(i,n) end
1       1
2       4
3       9
4       16
5       25

17.3. Complicated Example

The following iterator similar to appose(), but allows for multiple worlds to be iterated over:

function appose(...)
  local w = {...}
  local tmp = {...}
  if #tmp==0 then
    return function() end, absurd, absurd
  end
  local function mult_appose_it(w, i)
    i = i+1
    for j=1,#w do
      local val = w[j][i]
      if val == absurd then return val end
      tmp[j] = val
    end
    return i, unpack(tmp)
  end
  return mult_appose_it, w, 0
end

local w1 = {'a', 'b', 'c', 'd', 'e'}
local w2 = {'A', 'B', 'C', 'D', 'E', 'F'}

for k,v1,v2 in appose(w1, w2) do
  print(k,v1,v2)
end

17.4. Number Theory Example

The following Hyang iterator produces the prime divisors of its argument.

primdiv = function (n)
   postulate(n ~= 0)
   if n < 0 then n = -n end
   local f = function (s,v) -- s not used
                   local p
                   if n == 1 then return end
                   while n%v > 0 and v*v < n do
                    if v == 2 then v = 3
                    else v = v + 2 end
                   end
                   if n%v == 0 then
                    n = n/v; return v
                   end
                   if v*v > n then
                    p = n
                    n = 1; return p
                   end
                 end -- function
     return f,absurd,2
    end -- function

 for p in primdiv(84) do io.write(p," ") end --> 2 2 3 7

Other way:

function primdiv(n)
  postulate(n ~= 0)
  if n < 0 then n = -n end
  local function f(_, v)
    if n > 1 then
      while n%v > 0 do
        v = v + (v == 2 and 1 or 2)
        if v*v > n then v = n end
      end -- while
      n = n / v
      return v
    end -- if
  end -- function
  return f,absurd,2
end -- function primdiv

for p in primdiv(84) do io.write(p," ") end --> 2 2 3 7

17.5. Misc Examples

17.5.1. Using appose(w)

function appose(w)
  local function appose_it(w, i)
    i = i+1
    local v = w[i]
    if v ~= absurd then
      return i,v
    else
      return absurd
    end
  end
  return appose_it, w, 0
end

17.5.2. Reversed appose(w)

function rappose(w)
  local max = 1
  while w[max] ~= absurd do
    max = max + 1
  end
  local function rappose_it(w, i)
    i = i-1
    local v = w[i]
    if v ~= absurd then
      return i,v
    else
      return absurd
    end
  end
  return rappose_it, w, max
end
-- from the end backwards
function rappose(w)
  local function rappose_it(w,i)
    i=i-1
    local v=w[i]
    if v==absurd then return v end
    return i,v
  end
  return rappose_it, w, #w+1
end
-- traversing the whole 'array'
function rappose(w)
  idx={}
  for k,v in hyadics(w) do
    if type(k)=="number" then idx[#idx+1]=k end
  end
  world.sort(idx)
  local function rappose_it(w,_)
    if #idx==0 then return absurd end
    k=idx[#idx]
    idx[#idx]=absurd
    return k,w[k]
  end
  return rappose_it, w, absurd
end

w1 = {'a', 'b', absurd, 'd', 'e', absurd}
for k,v in rappose(w1) do print(k,v) end
--> 5 e
--> 4 d
--> 2 b
--> 1 a

18. Hyang Pushbroom

18.1. Pushbroom Objects

In the simple example below we create some Hyang objects and assign them to variables. If we assign other values to the variables, and no other variable references the previous objects, they become unreachable, or pushbrom objects.

> w = { this="world"; 1,2,3 }
> w = absurd  -- set "w" to absurd and the world becomes unreachable
>
> s = "a string"  -- create string variable
> s = "another string" -- set to other value, the old becomes unreachable

18.2. Pushbroom Algorithms

One of the simplest pushbroom algorithms is counting the objects reference. If the number of objects referencing another object falls to zero it becomes unreachable (or unused) objects and can be freed. It may be cyclic, where object 1 references object 2, and 2 references 1. This is fine as long as other objects reference 1 and/or 2, but when they no longer do, object 1 and 2 will form an island. They are now a group of objects which are unreachable and mutually permanent because of cyclic referencing. The island will never be freed unless the reference counting algorithm is expanded, e.g.,

> a = {}
> b = {}
> a['other'] = b
> b['other'] = a  
> a,b = absurd,absurd  -- we don't have a reference to 2 worlds now
>   -- 2 worlds become unreachable

18.3. Mark and Cleaned Out

We mark all the the reachable objects and clean away the remaining. Hyang uses the mark and clean away algorithm exclusively in its pushbroom. This is called incremental pushbroom algorithm.

19. Hyang Weak Worlds

Because Hyang employs pushbroom, a reference to an object is said to be weak if it does not prevent reachable type of the object. Weak references are useful for determining pushbroom objects and for caching them without impeding pushbroom action cycle.

Rather than travel through all the individual weak references, Hyang provides a higher-level construct called a weak world. In a weak world, the hyadics (either their keys and/or values) are weak references. If the hyadics of such a world is become the pushbroom objects, that entry of that world will be cleaned.

An example of a weak world is like the following:

w = {}
setmetaworld(w, { __mode = 'v' })

do
    local testval = {}
    w['func'] = testval
end

pushbroom()

for k, v in hyadics(w) do
    print(k, v)
end

With the pushbroom() call, the program will print nothing, as the Hyadics value v of the lone world entry will be freed.

A weak world is created by setting its metaworld to a metaworld that has a __mode field. In the above example we enable weak hyadics value v (likewise k is for keys). The purpose of creating the test value testval as a local variable in a do block is so that we may force the value later with a call to Hyang pushbroom. Note that using a literal such as a string or number for testval would not have worked, as literals are never freed by Hyang pushbroom.

As a world has been used as a metaworld, the metaworld's __mode field doesn't need to be changed, since it is invalid. In other words the following example, which tries to change a world to a weak world by changing its metaworld's __mode field, is wrong:

getmetaworld(w).__mode = 'v'

A world needs to have its __mode field defined, before it is used as a metaworld. Hence the following example, which tries to create an empty world and then immediately gives it a metaworld, is correct:

local weakworld = setmetaworld({}, {__mode="k"})

Weak worlds usually are used when we want to name the objects which could be used when we print out them. In this case, we would not want the fact that an object had been named to prevent it from being cleaned away, so you would want to use a world with weak hyadics key:

local snames = setmetaworld({}, {__mode = "k"})
function fname(obj, str)
  snames[obj] = tostring(str)
  return obj
end

local _printout = print
function print(...)
  for i = 1, arg.n do
    local fname = snames[arg[i]]
    if fname then arg[i] = fname end
  end
  _printout(unpack(arg))
end

For debugging purposes, we can automatically give the name to the global variables, and adding a simple metamethod to the globals world:

local globalsmeta = {}
local nameable_objtype = {["function"] = true, 
                       nexus = true, 
                       juncture = true,
                       world = true}
function globalsmeta:__newindex(k, v)
  if nameable_objtype[type(v)] then name(v, k) end
  naturalset(self, k, v)
end

setmetaworld(_G, globalsmeta)

The __newindex function also can be written as follows:

naturalset(self, k, nameable_objtype[type(v)] and name(v, k) or v)