Huginn programming language tutorial

Influence

Huginn is semantically based on Python and syntactically based on Java/C++ programming languages.

References, Variables, Values, aliasing operator, literals

In Huginn values are accessed by references.
A reference is a result of an expression.
Accessing (dereferencing) a variable name is also an expression.
An aliasing operator resets a reference so it points to a new value.
All references (with an exception of function call results) can be re-aliased.

An example of a value referenced by a variable:

In [1]:
x = 0
Out[1]:
0

Here:

  • x is a variable name
  • = is an aliasing operator
  • 0 is a literal of value 0

In case of a literals, an aliasing operator copies literal's value before aliasing.

Referential consistency, simple types mutability

In Huginn values of all types behave the same with respect to how references and aliasing operator works.
It means that aliasing operator, with an exception of literals, never copies values to be aliased.
It is also true for simple scalar types like integers, reals or strings.
Values of simple types (like integers) can by shared by multiple references and are mutable.

In [2]:
y = x
Out[2]:
0
In [3]:
x += 1
Out[3]:
1
In [4]:
y
Out[4]:
1

Here, both x and y variables reference the same value.

Types, type conversions, polymorphism

Huginn has introspective but non-reflective type system, it means that there is no monkey patching or in other words all types are known and fixed prior to program execution.
Huginn is strongly typed language, it means that each value has a specific type, there are no implicit value conversion nor there is any implicit type coercion.
At the same time Huginn is dynamically typed language, it means that final type consitency checks are done at run time, it also means that any given variable can reference values of different types during program execution, it also means that type is never explicitly mentioned in variable or function definitions.
Huginn uses duck typing style of polymorhpism, it means that a set of members determine the valid semantics of a type rather than inheritance over specific type hierarchy path.

Inspecting expression type:

In [5]:
type( x )
Out[5]:
integer

Aliasing a variable to point to a value of different type:

In [6]:
x = "Huginn"
Out[6]:
"Huginn"
In [7]:
type( x )
Out[7]:
string

Mixing binary operators operand types is forbidden:

In [8]:
x = 3
Out[8]:
3
In [9]:
type( x )
Out[9]:
integer
In [10]:
y = 0.14159
Out[10]:
0.14159
In [11]:
type( y )
Out[11]:
real
In [12]:
x + y
	x + y;
Operand types for `+' do not match: an `integer' vs a `real'.

Converting values between different types:

In [13]:
real( x ) + y
Out[13]:
3.14159
In [14]:
s = "2 + 2 * 2"
Out[14]:
"2 + 2 * 2"
In [15]:
real( s )
Out[15]:
6.0

Built-in scalar types, literals

Huginn has following scalar types:

integer

Integer literal syntax:
integer := [base]digit*
base := "0x" | "0X" | "0o" | "0O" | "0b" | "0B"
digit := decDigit | 'a' | 'A' | 'b' | 'B' | 'c' | 'C' | 'd' | 'D' | 'e' | 'E' | 'f' | 'F'
decDigit := octDigit | '8' | '9'
octDitit := binDigit | '2' | '3' | '4' | '5' | '6' | '7'
binDigit := '0' | '1'

In [16]:
x = 7
Out[16]:
7
In [17]:
y = 0xc0de
Out[17]:
49374
In [18]:
type( x )
Out[18]:
integer
In [19]:
%doc integer
Out[19]:

The integer is a scalar type that is used to represent and operate on integers. It supports basic operations of addition, subtraction, multiplication, division, modulo and comparisons. The range of possible values it can hold is [-2^32, 2^32).

real

Real literal syntax:
real := decDigit* '.' decDigit+ | decDigit+ '.' decDigit*

In [20]:
π = 3.141592653589793
Out[20]:
3.14159265359
In [21]:
type( π )
Out[21]:
real
In [22]:
%doc real
Out[22]:

The real is a scalar type that is used to represent and operate on floating point numbers. It supports basic operations of addition, subtraction, multiplication, division, modulo, power and comparisons, it can also be used as an argument in functions and algorithms from Mathematics package. The range of possible values it can hold is the same as double long from C++ programming language.

number

Number literal syntax:
number := '$' real

In [23]:
e = $2.718281828459045
Out[23]:
$2.718281828459045
In [24]:
type( e )
Out[24]:
number
In [25]:
%doc number
Out[25]:

The number is a scalar type that is used to represent and operate on fixed point numbers of arbitrary size. It supports basic operations of addition, subtraction, multiplication, division, modulo, power, factorial and comparisons, it can also be used as an argument in functions and algorithms from Mathematics package. The range of possible values it can hold is limited only by size of your hardware resources.
Class number has following members:

  • get_precision
  • is_exact
  • is_integral
  • set_precision

string

String literal syntax:
string := '"' codePoint* '"'

In [26]:
s = "Huginn programming language"
Out[26]:
"Huginn programming language"
In [27]:
type( s )
Out[27]:
string
In [28]:
%doc string
Out[28]:

The string is a scalar type that is used to represent and operate on character strings. It supports basic operations of addition and comparisons, it also supports subscript and range operators.
Class string has following members:

  • clear
  • find
  • find_last
  • find_last_one_of
  • find_last_other_than
  • find_one_of
  • find_other_than
  • format
  • replace
  • strip
  • strip_left
  • strip_right
  • to_lower
  • to_upper

character

Character literal syntax:
character := "'" codePoint "'"

In [29]:
c = 'H'
Out[29]:
'H'
In [30]:
type( c )
Out[30]:
character
In [31]:
%doc character
Out[31]:

The character is a scalar type that is used to represent and operate on single characters. It supports basic operations of comparisons, case modification and classification.
Class character has following members:

  • is_alnum
  • is_alpha
  • is_digit
  • is_lower
  • is_space
  • is_upper
  • is_xdigit
  • to_lower
  • to_upper

boolean

Boolean literal syntax:
boolean := "true" | "false"

In [32]:
f = true
Out[32]:
true
In [33]:
type( f )
Out[33]:
boolean
In [34]:
%doc boolean
Out[34]:

The boolean is a scalar type that is used to represent and operate on boolean values. It supports basic operations of negation, logical "and", "or", and "xor" and comparisons.

none

None literal syntax:
none := "none"

In [35]:
v = none
Out[35]:
none
In [36]:
type( v )
Out[36]:
*none*
In [37]:
%doc *none*
Out[37]:

A type of none value.

function reference

In [38]:
r = type
Out[38]:
type
In [39]:
type( r )
Out[39]:
*function_reference*
In [40]:
%doc *function_reference*
Out[40]:

The *function_reference* is a Huginn's way of providing information about a value's runtime type.

observer

In [41]:
o = observe( x );
type( o )
Out[41]:
*observer*
In [42]:
%doc *observer*
Out[42]:

The *observer* is a type representing a reference cycle breaking, non-owning weak "pointer" to a value.

Built-in collection types, literals

Huginn has following collection types:

tuple

Immutable, use to return multiple values from a function.
Tuple literal syntax:
tuple := '(' [ expr ( ',' expr )* ] ')'

In [43]:
t = (1, 2, 3)
Out[43]:
(1, 2, 3)
In [44]:
type( t )
Out[44]:
tuple
In [45]:
%doc tuple
Out[45]:

The tuple is a collection type that is used to represent and operate on tuple of values. It supports basic subscript and range operators.
Class tuple has following members:

  • add
  • equals
  • hash
  • less

list

Equivalent of std::vector<> from C++, constant time back insertion.
List literal syntax:
list := '[' [ expr ( ',' expr )* ] ']'

In [46]:
l = [1, 2, 3]
Out[46]:
[1, 2, 3]
In [47]:
type( l )
Out[47]:
list
In [48]:
%doc list
Out[48]:

The list is a collection type that is used to represent and operate on list of values. It supports basic subscript and range operators. It also supports efficient operations of addition and removal of its elements from its (right) end.
Class list has following members:

  • add
  • append
  • clear
  • equals
  • hash
  • insert
  • less
  • pop
  • push
  • resize
  • sort

deque

Equivalent of std::deque<> from C++, constant time front and back insertion.

In [49]:
d = deque( 1, 2, 3, 4 )
Out[49]:
deque(1, 2, 3, 4)
In [50]:
type( d )
Out[50]:
deque
In [51]:
%doc deque
Out[51]:

The deque is a collection type that is used to represent and operate on deque of values. It supports basic subscript and range operators. It also supports efficient operations of addition and removal of its elements at its both ends.
Class deque has following members:

  • add
  • append
  • clear
  • equals
  • hash
  • insert
  • less
  • pop
  • pop_front
  • prepend
  • push
  • push_front

dict

Ordered associative container with uniform key type. Equivalent of std::map<> from C++.
Dict literal syntax:
dict := '[' keyValuePair ( ',' keyValuePair )* ']'
keyValuePair := expr ':' expr

In [52]:
m = [ "Huginn": 1, "programming": 2, "language": 3 ]
Out[52]:
["Huginn": 1, "language": 3, "programming": 2]
In [53]:
type( m )
Out[53]:
dict
In [54]:
%doc dict
Out[54]:

The dict is a collection providing a sorted key to value map. It supports operations of iteration, key-value insertion, key removal and key search. The keys stored in given dict instance must be of uniform type.
Class dict has following members:

  • add
  • clear
  • ensure
  • equals
  • erase
  • get
  • has_key
  • hash
  • update
  • values

lookup

Associative container with constant time element access. Equivalent of std::unordered_map<> from C++ or dict from Python.
Lookup literal syntax:
lookup := '{' [ keyValuePair ( ',' keyValuePair )* ] '}'
keyValuePair := expr ':' expr

In [55]:
h = {"Huginn": 1, 2: "programming", "language": π}
Out[55]:
{"language": 3.14159265359, 2: "programming", "Huginn": 1}
In [56]:
type( h )
Out[56]:
lookup
In [57]:
%doc lookup
Out[57]:

The lookup is a collection providing a sorted key to value map. It supports operations of iteration, key-value insertion, key removal and key search.
Class lookup has following members:

  • add
  • clear
  • ensure
  • equals
  • erase
  • get
  • has_key
  • hash
  • update
  • values

order

Ordered unique element set container with uniform element types. Equivalent of std::set<> from C++.

In [58]:
q = order( 7, 9, 2, 1, 3, 6, 4 )
Out[58]:
order(1, 2, 3, 4, 6, 7, 9)
In [59]:
type( q )
Out[59]:
order
In [60]:
%doc order
Out[60]:

The order is a collection of sorted values of uniform types. It supports operations of addition, search and element removal.
Class order has following members:

  • add
  • clear
  • equals
  • erase
  • has_key
  • hash
  • insert
  • update

set

Unique element set container with constant time element access. Equivalent of std::unordered_set<> from C++ or set from Python.
Set literal syntax:
set := '{' expr ( ',' expr )* '}'

In [61]:
a = { x, s, e, c, π }
Out[61]:
{$2.718281828459045, "Huginn programming language", 3.14159265359, 'H', 7}
In [62]:
type( a )
Out[62]:
set
In [63]:
%doc set
Out[63]:

The set is a collection of unique elements of varying types. It supports operation of element insertion, removal and search.
Class set has following members:

  • add
  • clear
  • equals
  • erase
  • has_key
  • hash
  • insert
  • update

Built-in functions

Built-in type constructors/converters, and...

type

get type of an expression

In [64]:
ctor = type( 2. ^ 7. )
Out[64]:
real
In [65]:
x = ctor( "2 + 4 * 10" )
Out[65]:
42.0
In [66]:
type( x )
Out[66]:
real

size

get number of elements in a collection or call get_size() on an object

In [67]:
[s, l]
Out[67]:
["Huginn programming language", [1, 2, 3]]
In [68]:
size( l )
Out[68]:
3
In [69]:
size( s )
Out[69]:
27

copy

perform deep copy of a value

In [70]:
x = 0;
y = x;
[x, y]
Out[70]:
[0, 0]
In [71]:
x += 1;
[x, y]
Out[71]:
[1, 1]
In [72]:
y = copy( x );
x += 1;
[x, y]
Out[72]:
[2, 1]

observe/use

create weak reference of a value to break reference cycle

In [73]:
class Node {
    _name = none;
    _next = none;
    _previous = none;
    constructor( name_ ) {
        _name = name_;
        print( "{} constructed\n".format( _name ) );
    }
    destructor() {
        print( "{} destructed\n".format( _name ) );
    }
}
In [74]:
print( "---> before scope\n" );
handle = none;
/* scope */ {
    n1 = Node( "one" ); n2 = Node( "two" );
    n1._next = n2;
    n2._previous = n1;
    handle = observe( n1 );
}
print( "---> after scope\n" );
use( handle )._next = none; // break the cycle
Out[74]:
---> before scope
one constructed
two constructed
---> after scope
two destructed
one destructed
none
In [75]:
print( "---> before scope\n" );
handle = none;
/* scope */ {
    n1 = Node( "one" ); n2 = Node( "two" );
    n1._next = n2;
    n2._previous = observe( n1 ); // simple alias would create reference cycle
    handle = observe( n1 );
    print(
        "---> {} {} {}\n".format(
            string( type( n2._previous ) ),
            string( type( use( n2._previous ) ) ),
            use( n2._previous )._name
        )
    );
}
print( "---> after scope\n" );
[use( handle )]
Out[75]:
---> before scope
one constructed
two constructed
---> *observer* Node one
one destructed
two destructed
---> after scope
[none]

Syntax

In Huginn scopes are denoted by curly braces.
All scopes must be denoted explicitly, this also applies to scopes that consists of only single expression or statement.

Free expressions, i.e. expressions present independently at scope level must be delimited by semicolon character.

Control statements

if/else

In [76]:
res = "";
if ( size( string( $52! ) ) >= 60 ) {
    res = "a big number";
} else {
    res = "not so big";
}
res
Out[76]:
"a big number"

while

In [77]:
res = 0.0;
x = 2;
while ( x > 0 ) {
    res += π;
    x -= 1;
}
res
Out[77]:
6.28318530718

for

In [78]:
res = "";
for ( item : a ) {
    res += "{} of value {}\n".format( string( type( item ) ), item );
}
res.strip()
Out[78]:
"number of value 2.718281828459045
string of value Huginn programming language
real of value 3.14159265359
character of value H
integer of value 7"

switch (+case)

In [79]:
res = [];
for ( e : [t, d, q, a] ) {
    t = string( type( e ) );
    switch ( size( e ) ) {
        case ( 0 ): {
            res.push( "{} is empty".format( t ) );
        } break;
        case ( 2 ): {} /* fallthrough */
        case ( 4 ): {} /* fallthrough */
        case ( 6 ): {
            res.push( "{} has even number of elements".format( t ) );
        } break;
        case ( 7 ): {
            res.push( "{} has lucky number of elements".format( t ) );
        } break;
        default: {
            res.push( "some strange number of elements in {}".format( t ) );
        }
    }
}
res
Out[79]:
["some strange number of elements in tuple", "deque has even number of elements", "order has lucky number of elements", "some strange number of elements in set"]

try/catch

In [80]:
res = none;
try {
    q = 7 / x;
} catch ( ArithmeticException ex ) {
    res = ex.message();
}
res
Out[80]:
"*huginn jupyter*:146:11: Division by zero."

break/continue

In [81]:
res = [];
assert( x == 0, "inconsistent state" );
while ( true ) {
    n = x * x;
    x += 1;
    if ( n < 10 ) {
        continue;
    } else if ( n >= 100 ) {
        break;
    }
    res.push( n );
}
res
Out[81]:
[16, 25, 36, 49, 64, 81]

Functions

Functions support:

  • default arguments
  • variadic parameters
  • named parameters
In [82]:
greet( who_, how_ = "Hello,", args..., kwArgs::: ) {
    return ( "{} {} {} {}".format( how_, who_, args, kwArgs ) );
}
In [83]:
greet( "Johnny" )
Out[83]:
"Hello, Johnny () {}"
In [84]:
greet( how_: "Oh, Hi", who_: "Mark" )
Out[84]:
"Oh, Hi Mark () {}"
In [85]:
greet( "World", "Hello", "extra", "nice" )
Out[85]:
"Hello World ("extra", "nice") {}"
In [86]:
greet( "Huginn", "hi", "extra", "nice", greeting: "flexible" )
Out[86]:
"hi Huginn ("extra", "nice") {"greeting": "flexible"}"

Lambdas/Closures

Closure captures must be explicit.

In [87]:
square = @( x ) { x * x; }; // lambda
double = @( x ) { 2. * x; }; // another lambda
circ = @[π]( r, t ) { π * t( r ); }; // Closure! - Note how π is passed.
[circ( 7.0, double ), circ( 7.0, square )]
Out[87]:
[43.982297150257, 153.9380400259]

Exceptions

In [88]:
import Mathematics as math
In [89]:
solve( a, b, c ) {
    Δ = b ^ $2 - $4 * a * c;
    if ( Δ < $0 ) {
        throw ArithmeticException( "Δ is negative." );
    }
    x1 = ( -b - math.square_root( Δ ) ) / ( $2 * a );
    x2 = ( -b + math.square_root( Δ ) ) / ( $2 * a );
    return ( [ x1, x2 ] );
}
In [90]:
res = none;
exm = none;
try {
    res = solve( $-1, $2, $3 );
    res = solve( $1, $2, $3 );
} catch ( ArithmeticException ex ) {
    exm = ex.message();
}
[res, exm]
Out[90]:
[[$3, $-1], "*huginn jupyter*:23:34: Δ is negative."]

Packages

Packages are provided either by language core, or as external C++ binary plugins or user defined Huginn submodules.

In [91]:
import Algorithms as algo
In [92]:
import Text as textManip
In [93]:
words = textManip.split( s, " " )
Out[93]:
["Huginn", "programming", "language"]
In [94]:
wordsSortedByLength = algo.sorted( words, size )
Out[94]:
["Huginn", "language", "programming"]
In [95]:
textManip.join( wordsSortedByLength, " <3 " )
Out[95]:
"Huginn <3 language <3 programming"
In [96]:
%doc Algorithms
Out[96]:

The Algorithms package contains basic low-level algorithms.
Class Algorithms has following members:

  • AlgorithmsException
  • enumerate
  • filter
  • map
  • materialize
  • max
  • min
  • range
  • reduce
  • reversed
  • sorted
In [97]:
%doc Algorithms.map
Out[97]:

map( iterable, callable ) - create Mapper object that maps elements from iterable transforming each of them with callable when iterated over

Classes

Huginn's object model supports:

  • single base inheritance
  • method overloading
  • constructors
  • destructors (user defined, with deterministic point of invocation)
  • this keyword (pass or return "self")
  • super keyword (access base class members)
In [98]:
class Celestial {
    _name = none;
    _mass = none;
    _radius = none;
    _orbitalPeriod = none;
    _satellites = [];
    _primary = none;
    constructor( name_, mass_, radius_, orbitalPeriod_ = none ) {
        _name = name_;
        _mass = mass_;
        _radius = radius_;
        _orbitalPeriod = orbitalPeriod_;
    }
    add_satellite( satellite_ ) {
        _satellites.push( satellite_ );
        satellite_._primary = observe( this );
        return ( this );
    }
    to_string() {
        s = "Celestial body `{}` has mass of {} kilograms and radius of {} meters,".format(
            _name, _mass, _radius
        );
        if ( _primary != none ) {
            p = use( _primary );
            s += " it orbits around `{}` and".format( p._name );
        }
        noOfSatellites = size( _satellites );
        s += " it has {} satellite{}.".format(
            noOfSatellites > 0 ? noOfSatellites : "no",
            noOfSatellites != 1 ? "s" : ""
        );
        return ( s );
    }
}
In [99]:
Sun = Celestial( "Sun", $1.98855 * $10 ^ $30, $695700000 )
Out[99]:
Celestial body `Sun` has mass of 1988550000000000000000000000000 kilograms and radius of 695700000 meters, it has no satellites.
In [100]:
Earth = Celestial( "Earth", $5.97237 * $10 ^ $24, $6371000, $365.256363004 * $3600 * $24 )
Out[100]:
Celestial body `Earth` has mass of 5972370000000000000000000 kilograms and radius of 6371000 meters, it has no satellites.
In [101]:
Sun.add_satellite( Earth )
Out[101]:
Celestial body `Sun` has mass of 1988550000000000000000000000000 kilograms and radius of 695700000 meters, it has 1 satellite.
In [102]:
Earth
Out[102]:
Celestial body `Earth` has mass of 5972370000000000000000000 kilograms and radius of 6371000 meters, it orbits around `Sun` and it has no satellites.