1. Preface

Funky is an all-purpose (pure) functional and object oriented programming language. It can be used to write small scripts, application programs and just about any software that does not rely on direct access to hardware.

One of the design goals was to create a language suited to create large software projects in the realm of classical artificial intelligence (e.g. "expert systems").

But meanwhile AI systems based on neural networks had an unimaginable boost. To accomodate this new development an interface to use LLMs (Large Language Models) is planned for the near future.

Funky makes it easy to write comprehensible software and at the same time allows the utilization of the vast amount of execution units of modern computers. The logical flow of control is always sequential. The functional design of the language makes it easy for the compiler and runtime system to exploit the lack of side effects to compute function results in parallel at a very fine grained level.

Funky uses an indentation based syntax for all multi-line language constructs including string literals and remarks.

It has a very simple syntax for defining anonymous functions which are widely used. There are no keywords and no "special forms".

Function calls (the only kind of "statement" in Funky) can be written in several ways that make their semantic intention more clear.

There are no loop- or branch-statements; they are replaced by recursion and polymorphic function calls.

Variables have lexical scope but dynamic lifetime.

To ease arithmetic and logic operations, C-style operators (+, -, &&, >>, etc.) are supported as syntactic sugar.

Funky supports completely separate compilation of source code modules. To compile a module no other file than the module’s source code file has to be consulted.

As a functional language Funky uses value semantics. So no object can ever be modified, but new objects can be derived from existing ones. This prevents problems like deep vs. shallow copies, equality vs. identity and offers uniform semantics for copying and updating.

The typing discipline of Funky is dynamic but it does not support duck typing and arguments are not converted automatically (so Funky uses strong typing). Funky compilers should test for potential runtime errors at compile time wherever possible.

The use of dynamic variables makes Funky well suited for parallel execution.

Funky offers uniform access to functions, data structures, methods and attributes - they are all function calls in Funky. So the implementation can be changed later on without the need to change the interface.

The definitions of methods and attributes for any specific object can be distributed over several source code modules. An application program can add (private or global) methods and attributes to a prototype object defined in a library.

Funky guarantees determinism: a program run on the same stream of input data will always produce the same stream of output data. This makes debugging Funky programs quite easy. All input events can be logged and a post mortem debugger can replay these events and allow the user to run his program forwards and backwards in small or large steps.

Funky allows defining objects and their parts in any order. Everything that can be resolved will be resolved!

2. Concepts

2.1. Determinism

Funky is a deterministic programming language. If fed the same input data, a Funky program will return the same results on each run and (hopefully!) each platform.

This sounds trivial but isn’t.

  • This prevents the usage of concurrency.

  • There is no place for implementation defined stuff.

But Funky shall make use of modern multi-core CPUs and even of compute-clusters to compute its results.

So Funky programs can execute functions in parallel. But there is no explicit language support for parallelism! Each and every Funky program is logically sequential. It’s up to the compiler and runtime system to run parts of the code in parallel.

This automatic parallelization must not undermine determinism.

2.2. Functional

Funky is - as the name suggests - a functional programming language.

All results of a called function are explicitely returned and handled by the caller. There are no changes behind the scenes - no global state is changed.

Note
There are so-called I/O-functions which cannot change state, too, but which can perform input/output-operations.

Calls to such functions must be annotated in the source code. Each function calling an I/O-function is itself an I/O-function and so calls to it must be annotated, too.

2.3. Object Oriented

Funky is also an object oriented programming language.

There are no classes but each value is represented by an object.

To create a new object, one makes a copy of an existing object. After copying, the two objects are completely distinct. There is no such thing as delegation.

For each object any number of methods and attributes can be defined. Both, methods and attributes, implement so-called polymorphic functions.

When an object is copied all its methods and attributes are copied, too.

2.4. Uniform Access

There is no syntactic difference in accessing lists, attributes, methods and (non-polymorphic) functions.

2.5. Immutability

All objects are immutable. One can create new objects based on existing ones, but one cannot alter an existing object.

2.6. Dynamic Variables

There is only a single kind of variable. It has lexical scope but dynamic lifetime.

At any time exactly one object is bound to every variable.

A function can change the bindings of variables, if these variable are in its scope. On return from the function all these bindings are undone and the previous bindings are restored.

2.7. Versioning

Funky has built-in support for symbol versioning. Every exported symbol has an attached version number in a major.minor-scheme. If no version is explicitly specified, the version defaults to "1.0".

Several versions of a symbol can coexist. Older versions should not be removed. Any existing application program will thus be able to link against future versions of a library.

Versioning is done via namespaces. In the simplest (and most common) case one only specifies the desired version of each namespace to be used once in every source code module.

Once published, a specific namespace-version should never change. No changes, no additions - only bug fixes are allowed.

To add a symbol to an already published namespace the minor version number of the namespace is incremented. To change the semantics of an existing symbol, the major version number is incremented and the minor version number is reset to 0.

Each version of the namespace also includes all older symbols that were not redefined in a version less or equal to the desired one.

3. Quick Start

3.1. Source Code Files

Source code files that are referred from other Funky source code files must adhere to strict rules:

  • they must have the extension ".fky"

  • their name must consist of ASCII-letters, digits and underscores only

  • their name must start with a letter

  • their name must not end with an underscore

  • their name must not contain two consecutive underscores

For source code files that are used as scripts, none of the above rules applies. The can be given any convenient name.

All source code files must be UTF-8-encoded (ASCII is a subset of UTF-8) and must not start with a byte-order-marker.

Line-feed-characters are used to terminate lines. The last line needs to be terminated, too.

The tab-width is assumed to be 8 spaces.

Tab-characters may only appear at the start of a line; they are not allowed within a line.

Empty lines must not contain whitespace and whitespace at the end of a line is disallowed, too.

Syntactic units are separated by exactly one space character.

Other whitespace-characters (including carriage returns) are not allowed.

3.2. Hello World

Here is the Funky source code for the "classical" hello-world-program:

#!/usr/bin/env fkyrun

print! "
  Hello, world!

4. Semantics

This chapter explains the semantics of program execution in Funky. It uses simplified Funky code to concentrate on the essential behaviour. Every Funky program can be converted into this simplified code and most compilers will do so as one step of compilation.

In simplified Funky there are only two kinds of arguments:

  • identifiers

  • literals

The detailed rules for identifiers and literals will be explained in later sections. For a quick start:

  • identifiers are made up of letters, digits and underscore characters

  • numeric literals are made up of digits

  • character literals are enclosed in single quotes

  • string literals are enclosed in double quotes

  • unique item literals consist of a single dot (".")

  • function literals start with a colon, followed by a list of parameters enclosed in parentheses and an indented list of statements.

4.1. Statements

Every statement in Funky is a function call. The caller calls the callee. The functor is the "name of the function" - a variable that stores a "function-object", but remember that every object in Funky is a function!

An argument is an object that is passed from the caller to the callee.

A parameter is a variable in the callee’s context that is used to store an argument passed by the caller.

A result is an object that is returned from the callee to the caller.

A destination is a variable in the caller’s context that is used to store a result returned from the callee.

The grammar rules for a simplified statement:

functor: identifier

destination: "$" identifier | "!" identifier

argument: identifier | literal

statement: functor destination* argument*

A destination that is prefixed with a "$"-sign defines a new variable.

A destination that is prefixed with a "!"-sign redefines an existing variable.

A statement consists of a functor followed by any number of destinations and then any number of arguments. Functor, destinations and arguments are separated by a single space from one another. (There are no commas, semicolons or parentheses used for separation).

Note
These simplified rules are much more strict then the general rules which allow a lot more ways to arrange the arguments and results of a statement!

If functor, destinations and arguments fit on one line they are written like this:

functor destination1 destination2destinationn argument1 argument2argumentn

If they do not fit onto a single line (or do contain a function-literal), then functor, destinations and arguments are spread onto multiple lines; the destinations and arguments are indented (all by the same amount) relative to the functor:

functor
  destination1
  destination2destinationn
  argument1
  argument2argumentn

4.2. Executing a Function Call

Each identifier describes a single variable. An environment is a table that holds a value for each variable. At any time there is exactly one active environment.

When the execution of a Funky program starts, there exists a single environment which is at the same time the active environment. All variables that were explicitely top-level-initialized hold their respective values. All other variables are initialized to a specific value that triggers an error when used. (All variables must be initialized upon definition in Funky, but local variables are not initialized at the top-level.)

When a function is called, an exact copy of the caller’s environment is created and becomes the active environment. At any time only the active enviroment is accessed (read or modified). Non-active environments are never accessed!

Before the function’s statements are executed all the caller’s arguments are copied into the appropriate parameters of the callee (in the now active environment). (For more details see the section Functions).

Then the function’s statements are executed one after the other.

The last statement is treated specially. It is a tail call. In a tail call no destinations are specified. Instead, the results are directly returned to the function’s caller.

On return from the function (every function will eventually return in Funky) its copy of the environment is destroyed and the caller’s environment becomes the active environment again.

4.3. Definitions and Redefinitions

Every variable is defined exactly once. A variable is defined by using it as a destination and prefixing its name with a dollar sign.

The scope of a variable spans all following statements of the function in which it is defined and all function literals embedded in this function - even if they are placed textually before the definition of the variable.

If the variable is assigned a constant expression and is never redefined then its scope starts at the beginning of the function where it is defined.

If a variable is defined outside of any function (at the top level) its scope is the whole source code module.

If it is defined within a namespace it can be accessed from absolutely everywhere (even from other modules).

Variables defined at the top-level must be initalized with literals or constant expressions.

A constant expression is a literal or a function call whose functor evaluates to one of several built-in functions allowed within constant expressions and whose arguments are themself constant expressions.

On the top-level every unknown symbol is treated as a constant by the compiler.

Functions allowed in constant expressions are:

  • arithmethic and logical operations ("+", "|", "<<", etc.)

  • the tuple constructor function: std::tuple

  • the list constructor function std::list

  • constructor functions for subtypes of tuples and lists: std::key_value_pair, std::value_range, std::sequence

Some examples:

$p: (x)
  println! a # invalid - not in scope
  $print_square:
    println! a*a # valid - in scope
  $a 2*x # define the variable "a"
  println! a+1 # valid - in scope
  print_square!
$p: (x)
  println! a # valid, because "a" is definitely constant
  $a 47 # define the variable "a"

There is an exception for the top-level of the main module: it can contain other function calls, too. It’s some kind of an implicit "main"-function.

A variable can be redefined within its scope any number of times.

To redefine a variable is is used as a destination and prefixed with an exclamation mark.

4.4. Distributed Definitions

At the global scope the definition of a variable can be split into several separate definitions. In the case of a namespaced identifier these definitions can be distributed over several source code modules.

An example:

$person std_types::object

$person.first_name_of "Joe"
$person.last_name_of "Doe"

$person/name_of: (self)
  -> string(first_name_of(self) " " last_name_of(self))

$person/: (myself^)
  print! "
    Hi, I'm @(first_name_of(myself)) @(last_name_of(myself))!

is equal to

$person
  std_types::object
    .first_name_of "Joe"
    .last_name_of "Doe"
    /name_of: (self)
      -> string(first_name_of(self) " " last_name_of(self))
    /: (myself^)
      print! "
        Hi, I'm @(first_name_of(myself)) @(last_name_of(myself))!

See the section about attribute-value-pairs for a detailed description of the syntax used here.

4.5. Runtime Linker

The runtime linker is a very important part of Funky. It does its job whenever a Funky-executable is executed and before the first statement of the program is executed.

Its job is to collect all distributed definitions and to initialize all variables of the environment.

5. Objects

5.1. Root Objects

All objects stem from three so-called root objects:

  • std_types::object

  • std_types::undefined

  • std_types::error

All "normal" objects are derived from std_types::object.

std_types::undefined is a one-of-a-kind object.

All error-objects are derived from std_types::error.

An object consists of

  • a function

  • slots for all defined polymorphic functions

  • built-in objects may contain additional internal fields

Such internal fields are used to store the numeric values of integers, the characters that build a string, and things like that.

5.2. Values

All "normal objects" are of one of the following kinds or directly or indirectly derived from std_types::object.

  • numbers

  • booleans

  • unique items

  • characters

  • strings

  • tuples

  • lists

  • arrays

  • functions

Numbers, booleans, unique items and characters are scalar values. Tuples, lists and arrays are compound values; strings are a bit of both. Functions contain no data but are called to execute their embedded statement sequences.

These value types will be described in the following sections. Functions play such an important part that the whole next chapter is dedicated to this topic.

5.2.1. Numbers

There are exact (integer) and inexact (real) numbers.

It is planned to also add rational numbers (fractions) and complex numbers.

Integers

Integers are of arbitrary size and can literally consist of billions of digits.

There are positive and negative integers. Some operations can only be performed on positive integers.

All integer literals are positive. To negate an integer number use the negation operator.

Integer literals can be written in various numeral systems: decimal, binary, octal and hexadecimal.

Binary numeric literals are prefixed with "0b", octal with "0o" and hexadecimal with "0x". The letters must be written in lower case.

In addition to the standard digits from 0 to 9 hexadecimal literals make use of the letters a to f. These letters can be written in lower case or upper case.

Any group of digits can be separated by apostrophs.

Examples for valid integer literals:

23
1'000'000
0xAffe
0b10'1010'1010
0o755
0644

The literal in the last example stands for the decimal number 644.

Examples for invalid integer literals:

2'000'
0Xaffe
0b'0101'0101
0o123456789

Operations on integers return an integer number if the result is an integral number and a real number otherwise.

So 4/2 will return the integer number 2 while 5/2 will return the real number 2.5.

Real Numbers

These are IEEE 754 64 bit ("double precision") floating point numbers. All computations on reals will return real numbers; 2.5-2.5 will return the real number 0.0 and 3.7/3.7 will return the real number 1.0.

Like integer literals all real literals are positive. To negate a real number use the negation operator.

Real literals can only be written in decimal notation. They must

  • contain a decimal point and at least a single digit after the point

or

  • an exponent

They can, of course, contain a decimal part as well as an exponent.

The exponent is introduced by the letter "e" (lower or upper case), followed by an optional sign ("+" or "-") and a decimal exponent.

Digit separators (apostrophs) are not allowed in real literals, neither in the mantissa nor in the exponent.

Examples for valid real literals:

1.0
3.1415
2.9e4
1e-100
123.456E+16

Examples for invalid real literals:

1.
3.141'592'653
1e -100
0x1234e8
1e+0b1000

5.2.2. Booleans

In contrast to most other programming languages there is not a single boolean type instead there are two boolean objects:

  • std::true

and

  • std::false

These two objects are the single instances of their prototypes:

  • std_types::true

and

  • std_types::false

These boolean objects are used to implement control structures like if as polymorphic functions (see the section about polymorphic functions below).

5.2.3. Unique Items

Unique items are unique values that are different to any other value (any other object). They are mostly used as enumeration items.

A neat trick is to use a (local) unique item to initialize an optional parameter, so one can simply check if the parameter was explicitely initialized or not.

Unique item literals are written as a standalone point ("."). Each standalone point in the source code represents a different unique item.

5.2.4. Characters

Characters are implemented as 32-bit unsigned integers. They form the elements of strings and are mostly used to hold Unicode codepoints, but can also be used to store waveform data, colour information, etc.

Character literals are written between apostrophs.

Simple Character Literals

A simple literal is just the Unicode character between two apostrophs. The character must consist of a single code point. Because Funky source code files are always stored in UTF8-format, practically all printable Unicode characters can be used.

Valid examples:

'a'
' '
'#'
'α'
'统'
Numeric Character Literals

To create a character literal with a specific numeric code one can specify this code between an opening at-sign ("@") and a terminating semicolon (";"). The numeric code is an integer literal.

Valid examples:

'@65;'
'@0x40;'
'@0b0111'111;'

Invalid examples:

'@-3;'
'@x40;'
'@65'
At-Character-Literal

To represent the at-character itself one can write '@@' (two consecutive at-signs).

Named Character Literals

There are 276 different named character literals (see appendix). They are written in the form

'@name;'

Valid examples:

'@cr;'
'@at;'
'@quot;'
'@alpha;'
'@Omega;'

See appendix Named Characters.

5.2.5. Strings

Strings have an explicitly stored length and contain exactly that number of characters.

Strings that contain only 8-bit-character-values form a "subtype" that can be used in input/output-operations.

A string (like every other object) is a function. If called with a single integer argument the character at the specified position is returned.

String indices are one-based.

If called with two arguments the first argument is an index of an existing character and the second argument is a replacement character. The returned string is a copy of the original string with exactly this one character replaced.

There are lots of functions to create new strings out of existing ones (<strings.html:see Strings>).

Inline String Literals

String literals are enclosed within double quotes, e.g.:

"Hello, world!"

Instead of a Unicode character (more precise: Unicode codepoint) all kinds of character literals (see above) can be used, e.g.:

"From @alpha; to @Omega;"
Multi-Line String Literals

A multi-line string literal starts with a single double quote at the end of a line. All following indented lines are the contents of the string literal. The least indented line of the string literal defines the zero indent for all lines.

The lines after the double quote describe the strings contents, e.g.:

"
    This is a multi-line
    string literal.
  It spans three lines.

Each line-end within the string literal is converted into a linefeed-character. The above literal is therefor equal to:

"  This is a multi-line@nl;  string literal.@nl;It spans three lines.@nl;"

The sequence "@;" in a string literal expands to nothing. It can be used to add empty lines at the end of a multi-line string literal or to add spaces at the end of a line of a multi-line string literal.

A single "@" at the end of a line prevents the linefeed character and any whitespace at the start of the next line from beeing added to the string. It can also be used to define the zero indent.

Another example:

"
  @
      This is a single line @
      of text.

is equal to

"    This is a single line of text.@nl;"
String Interpolation

Funky supports string interpolation. They look like string literals with embedded expressions.

The expressions are embedded within "@(" and ")".

They are converted into calls to the string constructor function std::string, e.g.:

"@(a) * @(b) = @(a*b)"

is converted into

std::string(a " * " b " = " a*b)

String interpolations can be used within inline string literals as well as within multi-line string literals.

It is possible to embed multiple expressions at once, e.g.:

"
  This is @(article substantive).

is converteed into

std::string("This is " article substantive ".")

It is even possible to use a string expression as an embedded expression. The following examples are all equal:

"
  This is @(article " " substantive).
"
  This is @(article) @(substantive).
std::string("This is " article " " substantive ".")

5.2.6. Tuples

Tuples consist of a fixed number of fields. Each field stores an object. To create a tuple one has to supply all its fields at once. Also if a tuple is read then all its fields are returned.

Tuples are often used as base objects to implement more complex types (e.g. tree structures). To clone a tuple with all its modified attributes and methods one can use the new method.

There is no special syntax for normal tuple literals, but there are two types that are based upon tuples.

Key-Value-Pairs

A key-value-pair associates a key with a value. Technicalls it is simply a tuple with two fields.

The syntax is

key = value
Important
This is not an assignment!

Some examples:

FIRST_NAME = "John"
LAST_NAME = "Doe"
AGE = 47
CHILDREN = list("Jane" "Patrick")
Value-Ranges

A value-range is typically used to specify the start and end of a range of values (both, start and end value, are included in the range). Technically it is simply a tuple with two fields.

Some examples:

1..10
'a'..'z'
-100 .. +100

5.2.7. Lists

Like strings lists have an explicitly stored length and contain exactly that number of items.

A list (like every other object) is a function. If called with a single integer argument the item at the specified position is returned.

Like string indices, list indices are one-based.

If called with two arguments the first argument is an index of an existing item and the second argument is a replacement item. The returned list is a copy of the original list with exactly this one item replaced.

There are lots of functions to create new lists out of existing ones.

List are sometimes used for special purposes (e.g. grammer rules). To clone a list with all its modified attributes and methods one can use the new method.

There is no special syntax for normal list literals, but sequences are based upon lists.

5.2.8. Sequences

Sequences are used (often together with value-ranges) to describe the arguments in calls to the case-function or grammar rules.

The items of the sequence are separated by colons.

Syntax:

value_1, value_2, …, value_3

An example:

case chr
  'a'..'z', 'A'..'Z', '0'..'9':

6. Functions

Functions are values. All functions in Funky are anonymous but they can be assigned to (named) variables like any other value.

When a function is called, it receives a copy of the caller’s environment. The function can modify any variable in its copy of the enviroment provided the variable is in the function’s scope.

First, the function’s parameters are initialized; then the function’s statements are executed. A function can contain any number (one or more) of statements. There is only a single kind of statement in Funky: the function call.

6.1. Parameters

A function can have any number of parameters. A parameter is a variable. (The scope of a parameter is the function and all embedded functions.)

A function can have any number of mandatory and optional parameters as well as a single rest parameter.

The caller of the function must supply an argument for every mandatory parameter of the called function.

The caller can supply an argument for each optional parameter of the called function.

If there is no rest parameter the caller must not supply any excess arguments to the called function.

6.1.1. Mandatory Parameters

Mandatory parameters can be written at the start as well as at the end of a parameter list.

A caller must supply an argument for each mandatory parameter.

The first supplied arguments are assigned to the mandatory parameters at the start of the parameter list in their corresponing order.

The last supplied arguments are assigned to the mandatory parameters at the end of the parameter list in their corresponing order.

If a function is used to implement a method of an object then there must be a mandatory parameter at the start of the parameter list. This parameter takes the role of self or this in other object oriented programming languages. In Funky the name of this parameter can be freely chosen. Usually it is called "self".

6.1.2. Optional Parameters

Optional parameters follow the mandatory parameters at the start of a parameter list or start the parameter list.

For each optional parameter a default value must be specified. This default value must be either

  • a literal

or

  • an identifier

More complex default values are not allowed.

The syntax is as follows:

optional_parameter = default_value

If the caller supplies more arguments than the number of mandatory parameters the surplus arguments are used to assign values to the optional parameters from left to right.

6.1.3. Rest Parameter

A rest parameter follows the last optional parameter if there are one or the last starting mandatory parameter if there is one or starts the parameter list otherwise.

If the caller supplies more arguments than the sum of mandatory and optional parameters then the surplus arguments are collected into a list and this list is assigned to the rest parameter.

If there are no surplus arguments then an empty list is assigned to the rest parameter.

6.1.4. Myself

A special "parameter" that describes the function object itself can be supplied as the very first "parameter" of the function. It somewhat takes the role of argc(0) when calling a C-executable.

It is most important when accessing containers where the object itself stores all the items of the container.

The syntax is as follows:

myself^

6.1.5. Single-Line Parameter List

A parameter list can be written on a single line:

(myself^ left1leftn optional1optionaln rest right1rightn)

Some examples:

(myself^ key value = NONE)
(body args*)

6.1.6. Multi-Line Parameter List

If a parameter list is written on mutltiple lines then each line must contain exactly one parameter:

(
  myself^
  left1leftn
  optional1optionaln
  rest
  right1rightn
)

Some examples:

(
  myself^
  key
  value = NONE
)
(
  body
  args*
)

6.2. Calls

A function call consists of a function name (a variable) and the appropriate number of (input) arguments for the called function. It also expects one or more results (I/O-functions can expects no results).

The canonical form of a function calls looks like this:

function_name
  !destination1
  !destination2
  …
  !destinationn
  argument1
  argument2argumentn

If the variable to store the result is defined here the exclamation mark ("!") is replaced by a dollar sign ("$").

The destinations and arguments can be spread over multiple lines. They are separated with a single space from the function_name and beyond each other. The follow-up lines are all indented (by the same amount of whitespace) relative to the first line of the function call.

So the above call could be written like this:

function_name !destination1 !destination2
  …
  !destinationn argument1
  argument2argumentn

Results and arguments can me mixed in any way as long as the relative order of destinations and the relative order of arguments is not altered.

So the following calls are all identical:

assign !a 1 !b 2
assign 1 !a 2 !b
assign !a !b 1 2
assign 1 2 !a !b

6.2.1. Backquoted Arguments

This is syntactic sugar and most often used in conjunction with the dump! or edump statement. (The use of dump! and edump is not allowed in production code. It is meant for debugging purposes only.)

The expression

`expr

is converted into

"expr" expr

An example:

dump! `width `height

is the same as:

dump! "width" width "height" height

6.2.2. Input-Ouput Arguments

Often one wants to update the contents of a specific variable like that:

!x sqrt(x)

This will be translated by the compiler into the canonical form:

sqrt !x x

To prevent the repetition of the variable name (x) one can use an input-output argument like this:

sqrt &x

6.2.3. Attribute-Value-Pairs

Attribute-value and method-value pairs are used to initialize object slots.

The syntax for attribute-value pairs:

.attribute_of value

The syntax for method-value pairs:

/method function

Here an example:

$person
  std_types::object
    .first_name_of "John"
    .last_name_of "Doe"
    .name_of: (self)
      -> string(first_name_of(self) " " last_name_of(self))

There is also a special syntax to define or redefine the function of an object:

!person/: (myself^)
  print! "
    Hi, I'm @(first_name_of(myself)) @(last_name_of(myself))!

6.2.4. Assignments

Because it’s so common a call to the assign-function can be abbreviated if there is only a single input argument and a single destination. In the call

assign $destination source

the assign can be ommitted:

$destination source
Complex Destinations

The Funky compiler can handle more complex destinations. They are translated into more basic function calls.

!tab(idx) val

is converted into

tab !tab idx val

This is no typo! The function tab is called with the arguments idx and val and will hopefully return a new table that is then assigned to tab.

!obj.value_of val

creates a clone of obj where the contents of the slot value_of are replaced by the contents of val.

These expressions can be combined to any depth and complexity, e.g.:

!tab(i+1).values_of(2*j+k) val

is converted into

std::plus $temp__1 i 1
tab $temp__2 temp__1
values_of $temp__3 temp__2
std::times $temp__4 2 j
std::plus $temp__5 temp__4 k
temp__3 !temp__3 temp__5 val
!temp__2.values_of temp__3
tab !tab temp__1 temp__2
Note
The compiler might generate symbols with two consecutive underline characters for internal use to prevent name clashes with user defined variables.

The evaluations of "i+1" and "2_j+k" are guaranteed to be free of side effects because Funky is a functional language! So there is no need to compute them twice.

6.2.5. Tail Calls

The last function call in a body’s statement sequence is treated specially. It is a tail call. That means that its results are directly returned to the function’s caller. So no stack space is needed to hold (temporary) results.

Do not specifiy destinations in the last statement of a function! These would store the results in no longer used local variables and the results would be thrown away!

Compilers are allowed to report the occurrence of destinations in the final call of a statement sequence as an error.

Return

To return results from a function one could call the assign function in tail position without specifying any destinations for the results:

assign result1 result2resultn

To make the intention more clear there is a special syntax for this case:

-> result1 result2resultn

If a function has no parameters and consists of a single return-statement it can be abbreviated. The following expressions are all equal:

: () -> result1 result2resultn
: -> result1 result2resultn
-> result1 result2resultn

6.2.6. Function Call Arguments

All arguments of function calls should be identifiers or literals. But this would be very impractical. So the compiler transforms more complex arguments into function calls of their own.

A complex argument is a function call itself. These function calls must return exactly one result.

Here an example how the compiler might transform a complex function call:

Source code:

p $z f(x) g(h(y))

is transformed into

f $temp__1 x
h $temp__2 y
g $temp__3 temp__2
p $z temp__1 temp__3

The following sections describe the plentiful variants of writing function-call-arguments.

Inline-Function-Calls

All arguments fit on a single line. They immediately follow the function name, enclosed in parentheses:

function_name(argument1 argument2argumentn)
Multiline-Function-Calls

The function name stands on a new line and must not be followed by any arguments on this line. Instead all arguments must follow on equally indented successive lines. It is therefore not possible to call a function with no arguments this way.

function_name
  argument1
  argument2argumentn

It is possible to write multiple arguments on a single line.

Infix-Operator-Style

Any inline-function-call with exactly two arguments of the form

function(argument1 argument2)

can be replaced with

argument1 .function. argument2
Suffix-Operator-Style

Any inline-function-call with a single argument of the form

function(argument)

can be replaced with

argument.function
Note
The argument must be an idenfier or a parenthesed expression.

This is very convenient for calling test- or conversion-functions.

Instead of

is_defined(value)

one can write

value.is_defined
Infix-Operators

To make writing arithmetic and logic expressions more convenient one can use C-style infix operators.

Instead of

plus(a times(b c))

one can simply write

a+b*c

Each infix operator has an assigned operator precedence and an association.

Association defines the order in which operators are applied.

The (right associative) expression

a && b && c

is equal to

a && (b && c)

If the association is undefined then the compiler can decide whether to use left or right association. Only operations where association does not matter (e.g. addition) have undefined association.

If the association is none then you must explicitely use parentheses when combining multiple operators.

This is the complete list of infix-operators:

Symbol Function Precedence Association

*

std::times

9

undefined

/

std::over

9

none

+

std::plus

8

undefined

-

std::minus

8

left

<<

std::shift_left

7

none

>>

std::shift_right

7

none

&

std::bit_and

7

none

|

std::bit_or

7

none

^

std::bit_xor

7

none

==

std::equal

6

none

!=

std::not(std::equal(left right))

6

none

<

std::less

6

none

<=

std::not(std::less(right left))

6

none

>

std::less(right left)

6

none

>=

std::not(std::less(left right))

6

none

..

std::value_range

5

none

.named_operator.

named_operator

4

none

&&

std::and

3

right

||

std::or

3

right

,

std::sequence

2

undefined

=

std::key_value_pair

1

none

All operators evaluate their left argument before their right argument (if that matters). This also holds for right associative operators and operators where associativity does not matter (e.g. addition)!

The predefined precedences can be overwritten through the use of parentheses like in mathematics, e.g.:

(a+b) * (c-d)

See the following sections for special treatment of some operators.

Comparison Operators

There are only two comparison functions (std::equal and std::less). So all comparisons are based on those two functions and the logical negation (std::not).

left <= right

is converted into

std::not(std::less(right left))

and

left >= right

is converted into

std::not(std::less(left right))

Further

left > right

is converted into

std::less(right left)

and

left != right

is converted into

std::not(std::equal(left right))
Short Circuit Evaluation

The operators && and || call the functions std::and and std::or respectively. These two function take a boolean value as their first argument and a function that evaluates to a boolean value as their second argument. So these operators are converted as shown below:

left && right

is converted into

std::and(left (-> right()))

and

left || right

is converted into

std::or(left (-> right()))

So the second argument is only evaluated if necessary. This is called short circuit evaluation.

Prefix Usage Of Infix Operators

Expressions with infix operators can not be spread over multiple lines.

But one can use them in prefix notation. The operator is put on a line of its own and the arguments are written (indented) on the following lines.

*
  a+b
  c-d

is the same as

(a+b) * (c-d)

It is also possible to have more than two arguments.

&&
  condition1
  condition2
  condition3

is converted into

condition1 && condition2 && condition3

The association rules are obeyed!

Prefix-Operators

There is only a single prefix operator: “-”. It calls the function std::negate.

So

-123

is a shortcut for

std::negate(123)

6.3. Polymorphic Functions

Polymorphic functions are the way object oriented programming works in Funky.

When declaring a polymorphic function two things happen

  • all objects receive a named slot for the polymorphic function

  • a global bariable of the same name is assigned a dispatcher function that invokes the method or retrieves the attribute stored in this slot

For each object this slot can hold an attribute or a method.

6.3.1. Attributes

An attribute is a value (an object) stored in an object’s slot.

Attributes are assigned using postfix-operator syntax:

!object.slot value

E.g.:

!person.age_of 47

When accessing the corresponding polymorphic function the attribute value is returned.

6.3.2. Methods

A method is a function stored in an object’s slot.

Methods are assigned using a special syntax:

!object/slot method

E.g.:

person/age_of: (self)
  -> years_of(current_date()-birth_date_of(self))

When accessing the corresponding polymorphic function the method is invoked. All arguments passed to the polymorphic function are passed to the method.

It is not possible to retrieve the function stored as the method!

Important
This is a very important feature. It prevents one from moving a method of a built-in object (that accesses internals of built-in objects directly) to a object of the wrong type!

6.3.3. Polymorphic Functions With Setter

A polymorphic function can be defined to have setter functionality.

This kind of polymorphic function is used when the polymorphic function is implemented as an attribute in most objects. But it does not hinder the implementation as a method in some objects.

A polymorphic function with a setter is defined like this:

$attribute (!)

An example:

$age_of (!)

The setter functionality works only on objects that implement this polymorphic function as an attribute.

By default all objects derived from std_types::object receive an attribute with the value std::undefined for all polymorphic functions with a setter.

If an object implements this polymorphic function as a method then the setter functionality is not used. If later on the method is replaced by an attribute the setter functionality will work again.

The setter can be used to derive a new object from an existing one where the attribute is replaced by a new attribute:

attribute(object new_attribute_value)

And here an example:

age_of(person 47)

This returns a copy of the person object where the attribute age_of is replaced with the value 47.

6.3.4. Polymorphic Functions Without Setter

A polymorphic function without a setter is defined like this:

$method ()

This kind of polymorphic function is used when the polymorphic function is implemented as a method in most objects. But it does not hinder the implementation as an attribute in some objects.

By default all objects derived from std_types::object return an appropriate error-object when a polymorphic function without a setter is invoked on an object where the corresponding slot has not been explicitely set.

6.3.5. Control Flow

<loops.html:Loops> are implemented with the use of recursion and <branches.html:branches> with polymorphic functions.

7. Variables

Each variable is at any time bound to an object. If a variable has not yet been initialized it is bound to the error-object uninitialized.

The set of all variables of a program is called the program’s environment.

Variables are identified by identifiers.

7.1. Identifiers

An identifier consists of an (optional) namespace and a name.

The rules for namespaces and names are the same:

They consist of one or more "words". A word consists of one or more ASCII letters or digits. Words are separated by a single underscore-symbol ("_"). The first word must start with a letter.

Letters can be upper or lower case. Case is relevant, so identifiers differing only in letter case are different identifiers.

7.1.1. Unused Identifiers

The compiler might issue a warning or even an error if a defined variable is not used.

To prevent such warnings or errors precede the identifier with an underscore character ("_") in its declaration, e.g.:

$_dummy
$_unused

Most common is the use in parameter lists:

(_self value)

The underscore ist not part of the name. So the following code is invalid:

p $_dummy
assign $dummy 47

Both definitions use identical names!

7.1.2. Local Variables

Identifiers without a namespace are local to the source code module in which they are defined.

Some valid examples:

This_is_a_valid_identifier
s7
x
A_B_C

Some invalid examples:

_invalid_name
x__11
7_angels
just_to_be_continued_

Local variables must not be shadowed - that means that a variable with the same name must not be defined again in an inner scope (= embedded function) or again in the same function.

7.1.3. Global Variables

Variables that shall be visible in other source code modules must belong to a declared namespace.

A namespaced variable is written like

namespace::name

Global variables must be defined on the top-level (= unindented) of their module.

7.1.4. Naming Conventions

Funky programs use "snake_case" for normal identfiers. Constants are written in "SCREAMING_SNAKE_CASE".

Names that are not used only inside a single function are not abbreviated.

Typical identifiers:

insert_items
is_an_upper_case_letter_character
MAXIMUM_VALUE

Unusual identifiers:

ins_itms
isAnUpperCaseLetterCharacter
MAX_VAL

When appropriate the article "an" is used instead of "a".

Names that denote collections are written in plural form.

Attributes usually have the suffix "_of".

Tests often have the prefix "is_a_" ("is_an_") or "has_a_" ("has_an_").

Conversion functions usually have the prefix "to_"

Some examples:

$value_of ()
$is_an_integer ()
$to_integer ()
$items empty_list

8. Meta-Instructions

Meta-instructions or directives are used to combine source code modules into application programs or libraries and to manage namespaces.

Meta-instructions are always located at the start of a source code module, before any other statements.

8.1. Requiring Modules

The require-instruction is used to include a specific source code module into an application or library.

The syntax is streightforward:

<require module_name>

The module_name ist the filename of the source code module without the ".fky"-extension.

If the filename starts with a dot-character (".") then the filename is relative to the requiring module’s directory.

If the filename starts with a letter then the filename specifies a module in a library. The absolute location of library files is system dependent. The environment variable FUNKY_LIBRARY_PATH can be used to overwrite this system dependent location and to use one’s own version of library modules.

The rules for naming source code files are the same as the rules for naming identifiers.

Some examples:

<require basic/stdlib>
<require ./my_tools>

If an application program (not a library) does not require a single module then the module “basic/stdlib” is automatically included.

In Funky it’s common that only the main module of an application program requires other modules. The Funky standard libraries do not require other libraries by themselves! There are special library modules that require a set of other modules, e,g. basic/stdlib.

So the application writer can choose to use other implementations for functions than the default implementations supplied by the standard library.

8.2. Managing Namespaces

In Funky namespaces have two responsibilities:

  • defining symbols that can be accessed from other modules

  • version management

Every namespace (and its major versions) needs to be declared exactly once in an application.

It’s an error to forget to declare a namespace or to declare a namespace twice.

<namespace namespace>
<namespace namespace-major.minor>
<namespace namespace-unstable>

An unspecified version number always defaults to "1.0".

The version unstable is special and denotes the most current version that is still under development and functions defined with this version number can change their behaviour at any time without further notice.

An unstable namespace should only be used for testing new functionality.

There must be a namespace declaration for each major-version number of a namespace. The respective minor-version number declares the highest minor version that is available for the corresponding major version number.

E.g.:

<namespace std-1.3>
<namespace std-2.1>
<namespace std-unstable>

makes the following namespaces available:

std-1.0
std-1.1
std-1.2
std-1.3
std-2.0
std-2.1
std-unstable

When defining a namespaced variable it is necessary to explicitly specify the version number of the namespace. using- and resolve-directives are ignored when a variable is defined, but alias-directives are obeyed.

It is optional to specify a namespace when reading a namespaced variable. A using-directive can be used to resolve the namespace automatically:

<using namespace>
<using namespace-major.minor>
<using namespace-unstable>

For a single namespace each source code module can only contain one using-directive.

Whenever a variable is redefined or read that is not locally defined and without the explicit use of a namespace the using-directive is used by the runtime linker to find the corresponding variable.

It the runtime linker cannot resolve it unambigously it refuses to start the program and terminates with an appropriate error message.

It is possible to specify a namespace when reading or redefining a namespaced variable but it is not possible to explicitly specify a version number of a namespace.

The version number for a specific namespace can be selected for the whole module using a resolve-directive:

<resolve namespace-major.minor>
<resolve namespace-unstable>

To use a namespace under a different name an alias-directive can be used:

<alias namespace_alias = namespace>

alias-directives are used for variable definitions as well as for redefinitions and reads.

<namespace my_long_namespace>

<alias mln = my_long_namespace>

$mln::twice: (body)
  body
  body

is equivalent to

<namespace my_long_namespace>

$my_long_namespace::twice: (body)
  body
  body

9. Remarks

Remarks always start with a hash-tag-character ("#").

Everything after the hash-tag until the end of the line is part of the remark.

Consecutive and indented lines are also part of the remark!

Examples:

$x 1 # this is an end-of-line remark
$y 2
  #
    This is a
    multi-line remark
# This is not the
  usual way to write a
  remark in Funky, but it's
  valid, too.
#
  Empty lines

  do not end the
  remark!

Conventions for writing remarks:

  • remarks are written to the right of a statement

  • remarks are written (indented) below a statement

  • remarks are not written before a statement

Before you write a remark describing a variable’s meaning, think of a more comprehensive name for the variable!

9.1. Documentation Remarks

The compiler associates remarks with syntactic expressions.

Remarks are either

  • statements of their own

  • pseudo arguments to procedure or function calls or return statements

  • part of a body declaration

  • part of a parameter declaration

An indentifier does not become a function call, when it contains only remark pseudo arguments.

A return statement containing only remark pseudo arguments is invalid.

There are two ways to associate a remark with a function:

$function:
  #
    remark
  (
    parameters
  )
  …

or if the function has no parameter list:

$function
  #
    remark
  :
    …

To associate a remark with an expression:

expression # remark

or

expression
  #
    remark

An example to associate a remark with a parameter:

expression # remark

$twice:
  (
    body # the function to execute twice
  )
  …

or

expression
  #
    remark

$twice:
  (
    body
      #
        the function to execute twice;
        the function is called without any arguments

  )
  …

Glossary

Argument

arguments are passed from the caller to the callee

Attribute

an object’s slot that contains a value

Callee

the called function in a function call

Caller

the calling function in a function call

Constant Expression

an expression that will always return the same (constant) value

Definition

a variable is defined and initialized

Destination

where the caller stores the results returned from the callee

Environment

a table that stores all variables

Functor

a variable that stores a function

Inline Expression

an expression that fits completely on a single line

Literal

describes a simple constant like a string

Method

an object’s slot that contains a function marked for direct execution

Multi-Line Expression

an expression that spreads over multiple lines

Object

a value with its associated function and slots

Parameter

where the callee stores the caller’s arguments

Polymorphic Function

defines a slot in each object

Redefinition

an existing variable is bound to another value

Result

results are passed from the callee to the caller

Slot

stores an argument or a method

Statement

a function call

String Interpolation

looks like a literal, but is a call to the string-constructor function

Top-Level

everything that is not indented and therefor on the outmost level

Appendix A: Named Characters

Name Code Glyph Name Code Glyph Name Code Glyph Name Code Glyph

nul

0x0

soh

0x1

stx

0x2

etx

0x3

eot

0x4

enq

0x5

ack

0x6

bel

0x7

bs

0x8

ht

0x9

nl

0xa

vt

0xb

ff

0xc

cr

0xd

so

0xe

si

0xf

dle

0x10

dc1

0x11

xon

0x11

dc2

0x12

dc3

0x13

xoff

0x13

dc4

0x14

nak

0x15

syn

0x16

etb

0x17

can

0x18

em

0x19

sub

0x1a

esc

0x1b

fs

0x1c

gs

0x1d

rs

0x1e

us

0x1f

spc

0x20

amp

0x26

&

quot

0x22

"

apos

0x27

'

at

0x40

@

del

0x7f

csi

0x9b

nbsp

0xa0

 

iexcl

0xa1

¡

cent

0xa2

¢

pound

0xa3

£

curren

0xa4

¤

yen

0xa5

¥

brvbar

0xa6

¦

sect

0xa7

§

uml

0xa8

¨

copy

0xa9

©

ordf

0xaa

ª

laquo

0xab

«

not

0xac

¬

shy

0xad

­

reg

0xae

®

macr

0xaf

¯

deg

0xb0

°

plusmn

0xb1

±

sup2

0xb2

²

sup3

0xb3

³

acute

0xb4

´

micro

0xb5

µ

para

0xb6

middot

0xb7

·

cedil

0xb8

¸

sup1

0xb9

¹

ordm

0xba

º

raquo

0xbb

»

frac14

0xbc

¼

frac12

0xbd

½

frac34

0xbe

¾

iquest

0xbf

¿

Agrave

0xc0

À

Aacute

0xc1

Á

Acirc

0xc2

Â

Atilde

0xc3

Ã

Auml

0xc4

Ä

Aring

0xc5

Å

AElig

0xc6

Æ

Ccedil

0xc7

Ç

Egrave

0xc8

È

Eacute

0xc9

É

Ecirc

0xca

Ê

Euml

0xcb

Ë

Igrave

0xcc

Ì

Iacute

0xcd

Í

Icirc

0xce

Î

Iuml

0xcf

Ï

ETH

0xd0

Ð

Ntilde

0xd1

Ñ

Ograve

0xd2

Ò

Oacute

0xd3

Ó

Ocirc

0xd4

Ô

Otilde

0xd5

Õ

Ouml

0xd6

Ö

times

0xd7

×

Oslash

0xd8

Ø

Ugrave

0xd9

Ù

Uacute

0xda

Ú

Ucirc

0xdb

Û

Uuml

0xdc

Ü

Yacute

0xdd

Ý

THORN

0xde

Þ

szlig

0xdf

ß

agrave

0xe0

à

aacute

0xe1

á

acirc

0xe2

â

atilde

0xe3

ã

auml

0xe4

ä

aring

0xe5

å

aelig

0xe6

æ

ccedil

0xe7

ç

egrave

0xe8

è

eacute

0xe9

é

ecirc

0xea

ê

euml

0xeb

ë

igrave

0xec

ì

iacute

0xed

í

icirc

0xee

î

iuml

0xef

ï

eth

0xf0

ð

ntilde

0xf1

ñ

ograve

0xf2

ò

oacute

0xf3

ó

ocirc

0xf4

ô

otilde

0xf5

õ

ouml

0xf6

ö

divide

0xf7

÷

oslash

0xf8

ø

ugrave

0xf9

ù

uacute

0xfa

ú

ucirc

0xfb

û

uuml

0xfc

ü

yacute

0xfd

ý

thorn

0xfe

þ

yuml

0xff

ÿ

OElig

0x152

Œ

oelig

0x153

œ

Scaron

0x160

Š

scaron

0x161

š

Yuml

0x178

Ÿ

fnof

0x192

ƒ

circ

0x2c6

ˆ

tilde

0x2dc

˜

Alpha

0x391

Α

Beta

0x392

Β

Gamma

0x393

Γ

Delta

0x394

Δ

Epsilon

0x395

Ε

Zeta

0x396

Ζ

Eta

0x397

Η

Theta

0x398

Θ

Iota

0x399

Ι

Kappa

0x39a

Κ

Lambda

0x39b

Λ

Mu

0x39c

Μ

Nu

0x39d

Ν

Xi

0x39e

Ξ

Omicron

0x39f

Ο

Pi

0x3a0

Π

Rho

0x3a1

Ρ

Sigma

0x3a3

Σ

Tau

0x3a4

Τ

Upsilon

0x3a5

Υ

Phi

0x3a6

Φ

Chi

0x3a7

Χ

Psi

0x3a8

Ψ

Omega

0x3a9

Ω

alpha

0x3b1

α

beta

0x3b2

β

gamma

0x3b3

γ

delta

0x3b4

δ

epsilon

0x3b5

ε

zeta

0x3b6

ζ

eta

0x3b7

η

theta

0x3b8

θ

iota

0x3b9

ι

kappa

0x3ba

κ

lambda

0x3bb

λ

mu

0x3bc

μ

nu

0x3bd

ν

xi

0x3be

ξ

omicron

0x3bf

ο

pi

0x3c0

π

rho

0x3c1

ρ

sigmaf

0x3c2

ς

sigma

0x3c3

σ

tau

0x3c4

τ

upsilon

0x3c5

υ

phi

0x3c6

φ

chi

0x3c7

χ

psi

0x3c8

ψ

omega

0x3c9

ω

thetasym

0x3d1

ϑ

upsih

0x3d2

ϒ

piv

0x3d6

ϖ

ensp

0x2002

emsp

0x2003

thinsp

0x2009

zwnj

0x200c

zwj

0x200d

lrm

0x200e

rlm

0x200f

ndash

0x2013

mdash

0x2014

lsquo

0x2018

rsquo

0x2019

sbquo

0x201a

ldquo

0x201c

rdquo

0x201d

bdquo

0x201e

dagger

0x2020

Dagger

0x2021

bull

0x2022

hellip

0x2026

permil

0x2030

prime

0x2032

Prime

0x2033

lsaquo

0x2039

rsaquo

0x203a

oline

0x203e

euro

0x20ac

larr

0x2190

uarr

0x2191

rarr

0x2192

darr

0x2193

harr

0x2194

crarr

0x21b5

forall

0x2200

part

0x2202

exist

0x2203

empty

0x2205

nabla

0x2207

isin

0x2208

notin

0x2209

ni

0x220b

prod

0x220f

sum

0x2211

minus

0x2212

lowast

0x2217

radic

0x221a

prop

0x221d

infin

0x221e

ang

0x2220

and

0x2227

or

0x2228

cap

0x2229

cup

0x222a

int

0x222b

there4

0x2234

sim

0x223c

cong

0x2245

asymp

0x2248

ne

0x2260

equiv

0x2261

le

0x2264

ge

0x2265

subset

0x2282

superset

0x2283

nsub

0x2284

sube

0x2286

supe

0x2287

oplus

0x2295

otimes

0x2297

perp

0x22a5

sdot

0x22c5

lceil

0x2308

rceil

0x2309

lfloor

0x230a

rfloor

0x230b

loz

0x25ca

spades

0x2660

clubs

0x2663

hearts

0x2665

diams

0x2666