Getting started with IvoryScript

1 Introduction

IvoryScript is a declarative and functional programming language, with features designed for dynamic and persistent applications. Whether developing a phone app, modelling a VR scene, or any other application, it provides the necessary tools for efficient data management and logic. This guide is an introduction to the basic concepts and demonstrates how to use the online console for writing and testing scripts.

2 Accessing the online console

To begin using IvoryScript, navigate to the console.

The console presents a list of system modules to be used for an interactive session, each of which can be viewed for inspection.

For advanced use, which modules to include or exclude can be specified by checking or unchecking the "Use" checkbox. However, it is important to note that module dependencies are not tracked automatically, so care is needed to ensure that all necessary modules are also included.

3 Starting a session

A console session provides a simple interface to run, test, and explore the language. Each session spawns a new process, offering a clean independent environment for script compilation, data and execution.

The console includes an on/off toggle at the top right that controls the session.

When the session is active, it is possible to interact with a terminal and run IvoryScript code.

Important: Please note that due to system resource constraints, there is an inactivity timeout of 15 minutes. If no activity is detected within this period, the session will automatically expire, and a new session will need to be started.

4 Using the terminal

The main text box is where IvoryScript code is input and output displayed. Various controls below are provided to manage the scripting process.

  • Invoke: Submits the contents of the text area for execution. The text area is replaced by the output generated by the script.

  • Clear: Clears the text area to allow for new input.

  • Recall: Provides access to a history of the last 10 scripts submitted. By pressing the Recall button, previous scripts can be cycled through for editing or resubmission.

5 A first script: "Hello, world"

A common starting point for any programming language is writing a simple "Hello, world" script.

5.1 The simplest version

The simplest way to output "Hello, world" is to write it as a string:

"Hello, world"

Copy this line into the terminal and press Invoke. The output will be displayed as:

Hello, world

5.2 Concatenating strings

Alternatively, two strings can be concatenated using the ++ operator:

"Hello" ++ ", world"

Again, copy this into the terminal and press Invoke. This approach combines “Hello” and “, world” into a single string, producing the same output.

5.3 With a class method

The show method of the Show can also be used. In this case appended with a newline.

show "Hello, world\n"

Copy this line into the terminal and press Invoke. This approach can be adopted when a value is not otherwise displayed implicitly.

These examples demonstrate working with strings and how simple expressions can be executed in the console.

6 Type declarations

Types define the range of values that can exist. For example, the Int type includes whole numbers, the String type includes sequences of characters, and the Bool type includes logical values like True or False. A name can be associated with a value of a specific type.

greet :: String -> String;
greet name = "Hello, " ++ name ++ "!";
greet "IvoryScript"

In this example, a simple type declaration is shown. The line greet :: String -> String defines the function greet as a mapping from one value of type String to another value of the same type.

The symbol -> is used to denote a functional relationship. Thus, String -> String expresses that the function associates each input of type String with an output of the same type.

For example, when the string "IvoryScript" is provided, the result is "Hello, IvoryScript!".

Type declarations ensure clarity by explicitly stating the relationship between the types of inputs and outputs, reducing ambiguity and making the function’s behavior more predictable.

Note: This example won’t run directly in the console without a surrounding let as below. See also the let example.

let {
   greet :: String -> String;
   greet name = "Hello, " ++ name ++ "!"
} in
   greet "IvoryScript"

7 Function application

Function application in IvoryScript works differently from many other languages. Instead of using parentheses or commas between arguments, functions are applied by writing the function name followed by the arguments, separated by spaces.

For example, f x y applies the function f to both x and y without requiring any additional symbols like parentheses or commas.

Basic function application example
-- 'addInt' is a built-in function to add two integers
-- addInt :: Int -> Int -> Int
-- Applying the function
addInt 3 5
-- result = 8

In this example, the function add is applied to 3 and 5, resulting in 8. Function application is straightforward, and parentheses are only needed if parts of an expression require grouping.

8 Basic examples

8.1 Arithmetic and formatting

The following examples demonstrate how to use the console for basic arithmetic and formatting the result.

-- Basic Arithmetic
2 + 3  -- Output: 5

-- String concatenation and formatting
"Integer " ++ (format 42 "%03d") ++ ", Double " ++ (format pi "%.10f")
-- Output: "Integer 042, Double 3.1415926536"

8.2 Local definition

let {
   sum :: Int -> Int -> Int;
   sum x y = x + y
} in
   sum 3 4

In this example, the let block introduces a function sum with its explicit type declaration. The body of the let uses the declared name sum as needed, allowing flexible reuse of the function in the body.

While types are inferred, it is considered good practice to declare types explicitly. This ensures clarity in scenarios where functions can return either evaluated or unevaluated expressions.

8.3 Conditional

Conditional logic is used for performing different operations based on specific conditions. The following function checks if a number is even or odd.

let {
   checkEven :: Int -> String;
   checkEven x = if (x mod 2 = 0) then "Even" else "Odd"
} in
   checkEven 4
-- Output: "Even"

8.4 Case

let {
   describeNumber :: Int -> String;
   describeNumber n = case n of {
      0 -> "Zero";
      1 -> "One";
      otherwise -> "Other"
   }
} in
   describeNumber 1

This example demonstrates the use of a case expression. The function describeNumber takes an integer and matches its value against specific cases: if the value is 0, the result is "Zero"; if the value is 1, the result is "One"; otherwise, the result is "Other".

The if/then/else construct is syntactic sugar that is transformed into a corresponding case expression:

An expression of the form if expr1 then expr2 else expr3 translates to:

case expr1 of {
   True -> expr2;
   False -> expr3
}

Similarly, an expression of the form if expr1 then expr2 (without an else branch) translates to:

case expr1 of {
   True -> expr2
}

By expanding if/then/else into case expressions, the simplification to a small set of core constructs makes it easier to reason about the behaviour of programs.

8.5 Unnamed Function

let {
   applyFn :: (Int -> Int) -> Int -> Int;
   applyFn f x = f x
} in
   applyFn (\y -> y * 2) 10;  -- Applies the unnamed function to double the value 10

This example demonstrates the use of an unnamed (anonymous) function. The function applyFn takes a function and an integer as arguments, and applies the function to the integer.

In this case, an unnamed function (\y -> y * 2) is passed to applyFn, which doubles the value 10, resulting in 20.

8.6 Using pointers

let x :: Ptr Int = ^0 in {
   show (@x); show '\n';  -- Display the initial value (0)
   x @= 42;             -- Update the value at the pointer to 42
   show (@x); show '\n';  -- Display the updated value (42)
}

This example demonstrates how to initialise pointers and access/update the referenced value. The @ operator is used to dereference a pointer and access its referenced value, while @= is used to update its referenced value.

Here, the pointer x is first initialised with a reference to the integer value 0, displayed, updated to 42, and then displayed again.

Note: While pointers in IvoryScript may appear similar to traditional pointers in languages like C, the Ptr a constructor serves only as a reference mechanism.

8.7 List operations

The following example demonstrates a list operation, producing a new list from one where each element is doubled using the mapf function.

let {
   doubleList :: [Int] -> [Int];
   doubleList xs = mapf (\x -> x * 2) xs
} in
   doubleList [1, 2, 3, 4]
-- Output: [2, 4, 6, 8]

8.8 Recursive functions

Recursion is an essential concept in functional programming. The following function calculates the factorial of a number.

let {
   factorial :: Int -> Int;
   factorial n = if n = 0 then 1 else n * (factorial (n - 1))
} in
   factorial 5
-- Output: 120

8.9 Tuples

Tuples group multiple values. The following example demonstrates how to access the first element of a tuple.

let {
   tupleExample :: (Int, String);
   tupleExample = (42, "IvoryScript")
} in
   fst tupleExample
-- Output: 42

9 For loop equivalent

Explicit loops like the traditional for loop are replaced by recursive functions that achieve the same effect. While this may seem different, it is just as efficient due to tail call optimisation.

let {
   forLoop :: Int -> Int -> (Int -> Void) -> Void;
   forLoop start end action =
      if start <= end then {
         action start;
         forLoop (start + 1) end action;
      }
} in
   forLoop 1 5 (\i -> {show i; show '\n'})

This example demonstrates an equivalent to a traditional for loop to print the numbers from 1 to 5 with the show function, followed by a newline. The function forLoop takes start and end values, and recursively applies the action function on each iteration.

10 Unevaluated example

Unevaluated values allow expressions to be defined without being immediately computed. These expressions can represent potentially infinite sequences or deferred computations that are only evaluated when necessary.

let {
   listFrom :: Int -> Int -> Exp [Int];
   listFrom s step = s :+ (listFrom (s + step) step);
   infiniteList = listFrom 1 1
} in
   take 10 infiniteList

In this example, listFrom is a function that denotes an infinite list of integers, starting from s and increasing by step. The operator :+ is used to construct the list recursively, and the expression remains unevaluated until required.

The function take is used to extract the first 10 elements from the unevaluated infinite list, making it possible to handle only a finite portion of the otherwise infinite sequence.

Note: A syntactic sugar equivalent for infinite lists, such as 1.., will be added in future updates.

11 Higher-order functions and partial application

A higher-order function is one that accepts a function as an argument or returns a function as its result.

One example is the list function mapf, which accepts a function as an argument.

Currying is the transformation of a function taking multiple arguments into a nest of functions, each accepting a single argument and returning either another function or the final result. As a consequence, applying fewer arguments naturally results in a function that expects the remaining arguments. This is known as partial application.

However, returning a function as a result (which may include captured values) is not restricted to partial application. A function may directly return another function.

A function-returning function
let {
addN :: Int -> (Int -> Int);
addN n = (\x -> x + n);

add3 :: Int -> Int;
add3 = addN 3
} in
add3 5
                  
--result = 8

The function addN returns a function that captures the value of n. Here addN 3 yields a function that adds 3 to its argument. This is a function-valued result defined explicitly, rather than a partial application of a two-argument function.

Partial application occurs when a function is applied to fewer arguments than it requires. The remaining arguments are then expected by the resulting function.

Partial application
let {
add :: Int -> Int -> Int;
add x y = x + y;

add3 :: Int -> Int;
add3 = add 3
} in
add3 5
                  
--result = 8

The expression add 3 is a partial application of add. It outputs a function that supplies 3 as the first argument and expects the second.

A function-consuming function
                        mapf ((*) 2) [1, 2, 3, 4]
                  
--result = [2, 4, 6, 8]

The function mapf is higher-order because it accepts another function as an argument. The expression (*) 2 is a partial application that generates the function passed to mapf.

12 Prepared examples

The examples presented above provide an introduction to basic features of IvoryScript. Users may explore more advanced features, such as:

  • Memoization and dynamic programming for performance optimization.
  • Complex data structures, including trees and maps.
  • Advanced recursion techniques, such as the eight queens puzzle or prime number generation.

At the bottom right of the screen, there is an "Examples" button. Clicking this opens a dialog containing various example scripts.

Selecting an example will populate the terminal with the selected script. The script can then be invoked directly or edited before running.

More examples and documentation can be found in the advanced section of the IvoryScript documentation.

13 Efficiency example: Efficient representation of names and types in IvoryScript

Lastly, in this example, the efficiency of representing names and types in IvoryScript is demonstrated. A dynamic system module is utilized to create a binding between a name and a dynamic value of type Any. Through the use of the special concrete pattern match ~Bind, both the type of the binding and its internal representation are inspected and displayed, providing insight into how such structures are managed efficiently.

let x :: Binding Name Any = the_answer_to_everything:42
in
   case x of {~Bind repr ->
     let {
        type_ :: Type = typeOf x;
        reprType_ :: Type = typeOf repr;
        size :: Int  = sizeOf type_
      } in {
        show "A value of type ("; show type_;
        show ") such as "; show x;
        show " is represented by the type "; show reprType_;
        show " and occupies "; show size; show " bytes of storage.\n"
      }
}
A value of type (Binding Name Any) such as the_answer_to_everything:42 is represented by the type (Name,Any) and occupies 16 bytes of storage.

The output highlights the difference between the type of the binding and its internal representation, which is aligned based on the processor architecture. In this case, a 32-bit architecture leads to a storage requirement of 16 bytes, including padding for efficient memory alignment. On different architectures, memory usage might vary, underscoring IvoryScript’s flexibility in adapting to different platforms.