Funky Manual

  1. Preface
  2. Concepts
    1. Determinism
    2. Functional
    3. Object Oriented
    4. Uniform Access
    5. Immutability
    6. Dynamic Variables
    7. Versioning
  3. Quick Start
    1. Source Code Files
    2. Hello World
  4. Semantics
    1. Statements
    2. Executing A Function Call
    3. Definitions And Redefinitions
    4. Distributed Definitions
    5. Runtime Linker
  5. Objects
    1. Root Objects
    2. Values
      1. Numbers
        1. Integers
        2. Real Numbers
      2. Booleans
      3. Unique Items
      4. Characters
        1. Simple Character Literals
        2. Numeric Character Literals
        3. At-Character-Literal
        4. Named Character Literals
      5. Strings
        1. Inline String Literals
        2. Multi-Line String Literals
        3. String Interpolation
      6. Tuples
        1. Key-Value-Pairs
        2. Value-Ranges
      7. Lists
      8. Sequences
  6. Functions
    1. Parameters
      1. Mandatory Parameters
      2. Optional Parameters
      3. Rest Parameter
      4. Myself
      5. Single Line Parameter List
      6. Multi Line Parameter List
    2. Calls
      1. Expansion Arguments
      2. Backquoted Arguments
      3. Input-Ouput Arguments
      4. Attribute-Value-Pairs
      5. Assignments
        1. Complex Destinations
      6. Tail Calls
        1. Return
      7. Function Call Arguments
        1. Inline-Function-Calls
        2. Multiline-Function-Calls
        3. Infix-Operator-Style
        4. Suffix-Operator-Style
        5. Infix-Operators
          1. Comparison Operators
          2. Short Circuit Evaluation
        6. Prefix Usage Of Infix Operators
        7. Prefix-Operators
    3. Polymorphic Functions
      1. Attributes
      2. Methods
      3. Polymorphic Functions With Setter
      4. Polymorphic Functions Without Setter
      5. Control Flow
  7. Variables
    1. Identifiers
      1. Unused Identifiers
      2. Local Variables
      3. Global Variables
      4. Naming Conventions
  8. Meta-Instructions
    1. Requiring Modules
    2. Managing Namespaces
  9. Remarks
    1. Documentation Remarks
  10. Glossary
  11. Named Characters

Home | Topics Index | Type Index | Symbol Index

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 artificial intelligence.

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 makes the 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 of any 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 also offers 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!

Concepts

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.

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.

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.

Remark: 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.

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. They both implement so-called polymorphic functions.

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

Uniform Access

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

Immutability

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

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.

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 library versions.

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.

Quick Start

Source Code Files

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

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 one or more space characters.

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

Hello World

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

#!/usr/bin/env fkyrun

write "
  Hello, world!

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:

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

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 any number of arguments after that. Functor, destinations and arguments are separated by whitespace from one another. (There are no commas, semicolons or parentheses used for separation).

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

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 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.

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 textual before the definition of the variable.

If the variable is assigned a literal 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.

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

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

Constant expression exist only at the top-level where it is not allowed to redefine any variables.

Functions allowed in constant expressions are:

Arguments allowed in constant expressions are:

Some examples:

$p: (x)
  writeln! a # invalid - not in scope
  $print_square:
    writeln! a*a # valid - in scope
  $a 2*x # define the variable "a"
  writeln! a+1 # valid - in scope
  print_square!
$p: (x)
  writeln! 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.

Distributed Definitions

At a 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^)
  write! "
    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^)
      write! "
        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.

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.

Objects

Root Objects

All objects stem from three so-called root objects:

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

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

Values

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

Numbers, booleans, unique items and characters are scalar values. Tuples and lists 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 next chapter ist dedicated to this topic.

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

or

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

Booleans

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

These two objects are the single instances of their prototypes:

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

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.

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 (see above).

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;'

Strings

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

Strings that consist of 8-bit-character-values only 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 (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 following lines describe the strings contents, e.g.:

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

Each lineend 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 "@" before 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 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 ".")

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. 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

Attention: 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

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 fist 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.

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':

Functions

Functions are values. All functions in Funky are anonymous but they can be assigned to (named) variables as 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.

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.

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".

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

or

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.

Rest Parameter

A rest parameter follows the last optional parameter if there is 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 number of mandatory and optional parameters together 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.

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^

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*)

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*
)

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 at this position the exclamation mark ("!") is replaced by a dollar sign ("$").

The destinations and arguments can be spread over multiple lines. They are separated with whitespace from the function_name and beyond each other. The follow-up lines are all indented (by the same amount) 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

Expansion Arguments

A function call can contain any number of expansion arguments. These arguments must refer to list objects. Instead of the list as a whole each list item (from left to right) is passed as an argument of its own.

Syntax:

expansion_argument*

Example:

items*

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:

edump `width `height

is the same as:

edump "width" width "height" height

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 us an input-output argument like this:

sqrt &x

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^)
  write! "
    Hi, I'm @(first_name_of(myself)) @(last_name_of(myself))!

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. Instead of

assign $destination source

one can omit the assign and simply write

$destination source
Complex Destinations

The Funky compiler can handle more complex destination. 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(2j+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

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

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 (temporar) 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!

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

In the latter case there is a notation similar to input-output-arguments:

-> !result1 !result2 … !resultn

is equal to

!result1 !result2 … !resultn -> result1 result2resultn

Function Call Arguments

All arguments of function calls should be identifiers or literals. But this would be very impractical. So the compiler transforms all 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

Remark: The compiler might generate symbols with two consecutive underline characters for internal use to prevent name clashes with user defined variables.

The following sections describe the plentiful variants of 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 each line.

Infix-Operator-Style

Any inline-function-call with 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

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 (very similar to those in the C-language) and an associated left- or right-association (these are different to those in the C-language!).

This is the complete list of infix-operators:

SymbolFunctionPrecedenceAssociation
`\*``std::times`14left
`/`std::over`14left
`+`std::plus`13left
`-`std::minus`13left
`<<`std::shift_left`12left
`>>`std::shift_right`12left
`&`std::bit_and`11left
`^`std::bit_xor`10left
`\|`std::bit_or`9left
`<=`std::less`8left
`<`std::less`8left
`>=`std::less`8left
`>`std::less`8left
`==`std::equal`7left
`!=`std::equal`7left
`&&`std::and`5right
`\|\|`std::or`4right
`..`std::value_range`3right
`,`std::sequence`2right
`=`std::key_value_pair`1right

All operators evaluate their left argument before their right argument (if that matters). This also holds for right associative operators!

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)

Polymorphic Functions

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

When declaring a polymorphic function two things happen

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

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.

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!

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!

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_ofis replaced with the value 47.

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.

Control Flow

Loops are implemented with the use of recursion and branches with polymorphic functions.

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.

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.

Unused Identifiers

The compiler might issue a warning if a defined variable is not used.

To prevent such warnings precede the identifier with an underscore character ("_") it 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!

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.

Global Variables

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

A namespaced variable is written like

namespace::name

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

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

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.

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 FKYLIBPATH 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 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.

Managing Namespaces

In Funky namespaces have two responsibilities:

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 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

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:

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

Documentation Remarks

The compiler associates remarks with syntactic expressions.

Remarks are either

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

Named Characters

NameCodeGlyphNameCodeGlyphNameCodeGlyphNameCodeGlyph
nul0x0soh0x1stx0x2etx0x3
eot0x4enq0x5ack0x6bel0x7
bs0x8ht0x9nl0xavt0xb
ff0xccr0xdso0xesi0xf
dle0x10dc10x11xon0x11dc20x12
dc30x13xoff0x13dc40x14nak0x15
syn0x16etb0x17can0x18em0x19
sub0x1aesc0x1bfs0x1cgs0x1d
rs0x1eus0x1fspc0x20amp0x26&
quot0x22"apos0x27'at0x40@del0x7f
csi0x9bnbsp0xa0 iexcl0xa1¡cent0xa2¢
pound0xa3£curren0xa4¤yen0xa5¥brvbar0xa6¦
sect0xa7§uml0xa8¨copy0xa9©ordf0xaaª
laquo0xab«not0xac¬shy0xad­reg0xae®
macr0xaf¯deg0xb0°plusmn0xb1±sup20xb2²
sup30xb3³acute0xb4´micro0xb5µpara0xb6
middot0xb7·cedil0xb8¸sup10xb9¹ordm0xbaº
raquo0xbb»frac140xbc¼frac120xbd½frac340xbe¾
iquest0xbf¿Agrave0xc0ÀAacute0xc1ÁAcirc0xc2Â
Atilde0xc3ÃAuml0xc4ÄAring0xc5ÅAElig0xc6Æ
Ccedil0xc7ÇEgrave0xc8ÈEacute0xc9ÉEcirc0xcaÊ
Euml0xcbËIgrave0xccÌIacute0xcdÍIcirc0xceÎ
Iuml0xcfÏETH0xd0ÐNtilde0xd1ÑOgrave0xd2Ò
Oacute0xd3ÓOcirc0xd4ÔOtilde0xd5ÕOuml0xd6Ö
times0xd7×Oslash0xd8ØUgrave0xd9ÙUacute0xdaÚ
Ucirc0xdbÛUuml0xdcÜYacute0xddÝTHORN0xdeÞ
szlig0xdfßagrave0xe0àaacute0xe1áacirc0xe2â
atilde0xe3ãauml0xe4äaring0xe5åaelig0xe6æ
ccedil0xe7çegrave0xe8èeacute0xe9éecirc0xeaê
euml0xebëigrave0xecìiacute0xedíicirc0xeeî
iuml0xefïeth0xf0ðntilde0xf1ñograve0xf2ò
oacute0xf3óocirc0xf4ôotilde0xf5õouml0xf6ö
divide0xf7÷oslash0xf8øugrave0xf9ùuacute0xfaú
ucirc0xfbûuuml0xfcüyacute0xfdýthorn0xfeþ
yuml0xffÿOElig0x152Œoelig0x153œScaron0x160Š
scaron0x161šYuml0x178Ÿfnof0x192ƒcirc0x2c6ˆ
tilde0x2dc˜Alpha0x391ΑBeta0x392ΒGamma0x393Γ
Delta0x394ΔEpsilon0x395ΕZeta0x396ΖEta0x397Η
Theta0x398ΘIota0x399ΙKappa0x39aΚLambda0x39bΛ
Mu0x39cΜNu0x39dΝXi0x39eΞOmicron0x39fΟ
Pi0x3a0ΠRho0x3a1ΡSigma0x3a3ΣTau0x3a4Τ
Upsilon0x3a5ΥPhi0x3a6ΦChi0x3a7ΧPsi0x3a8Ψ
Omega0x3a9Ωalpha0x3b1αbeta0x3b2βgamma0x3b3γ
delta0x3b4δepsilon0x3b5εzeta0x3b6ζeta0x3b7η
theta0x3b8θiota0x3b9ιkappa0x3baκlambda0x3bbλ
mu0x3bcμnu0x3bdνxi0x3beξomicron0x3bfο
pi0x3c0πrho0x3c1ρsigmaf0x3c2ςsigma0x3c3σ
tau0x3c4τupsilon0x3c5υphi0x3c6φchi0x3c7χ
psi0x3c8ψomega0x3c9ωthetasym0x3d1ϑupsih0x3d2ϒ
piv0x3d6ϖensp0x2002emsp0x2003thinsp0x2009
zwnj0x200czwj0x200dlrm0x200erlm0x200f
ndash0x2013mdash0x2014lsquo0x2018rsquo0x2019
sbquo0x201aldquo0x201crdquo0x201dbdquo0x201e
dagger0x2020Dagger0x2021bull0x2022hellip0x2026
permil0x2030prime0x2032Prime0x2033lsaquo0x2039
rsaquo0x203aoline0x203eeuro0x20aclarr0x2190
uarr0x2191rarr0x2192darr0x2193harr0x2194
crarr0x21b5forall0x2200part0x2202exist0x2203
empty0x2205nabla0x2207isin0x2208notin0x2209
ni0x220bprod0x220fsum0x2211minus0x2212
lowast0x2217radic0x221aprop0x221dinfin0x221e
ang0x2220and0x2227or0x2228cap0x2229
cup0x222aint0x222bthere40x2234sim0x223c
cong0x2245asymp0x2248ne0x2260equiv0x2261
le0x2264ge0x2265subset0x2282superset0x2283
nsub0x2284sube0x2286supe0x2287oplus0x2295
otimes0x2297perp0x22a5sdot0x22c5lceil0x2308
rceil0x2309lfloor0x230arfloor0x230bloz0x25ca
spades0x2660clubs0x2663hearts0x2665diams0x2666