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, IvoryScript provides the necessary tools for efficient data management and logic. This guide is an introduction to the basic concepts of IvoryScript 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 IvoryScript 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 an IvoryScript session

An IvoryScript 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 demonstrates the basic functionality of IvoryScript for working with strings and how simple expressions can be executed in the console.

6 IvoryScript Type Declarations

In IvoryScript, 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!".

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

-- '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 as a string in IvoryScript.

-- 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 IvoryScript allows for type inference, it is considered good practice to declare types explicitly. This ensures clarity in scenarios where functions can return either evaluated or unevaluated expressions. The use of evaluation operators in IvoryScript makes such declarations particularly useful in controlling expression evaluation.

8.3 Conditional

IvoryScript supports conditional logic 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 in IvoryScript. 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".

In IvoryScript, 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, IvoryScript simplifies the language to a small, core set of constructs, making 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 in IvoryScript. 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 in IvoryScript 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 in IvoryScript 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 in IvoryScript. 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

In IvoryScript, 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 as the traditional version, since tail recursion optimizations ensure that performance remains comparable.

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 in IvoryScript. The function forLoop takes a start value, an end value, and an action to perform on each iteration. It recursively iterates from start to end, applying the action function on each value.

In this case, the forLoop prints the numbers from 1 to 5 by applying the show function, followed by a newline character.

Although recursion is used here, the efficiency remains the same as in a traditional for loop, thanks to optimizations such as tail recursion.

10 Unevaluated example

In IvoryScript, 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 generates 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 Currying and partial application

IvoryScript also supports currying, which allows functions to be applied one argument at a time. When a function is applied to fewer arguments than it requires, the result is a new function that expects the remaining arguments.

For instance, f x can return a new function that still expects another argument.

-- A curried function to add two numbers
add :: Int -> Int -> Int
add x y = x + y

-- Applying the function partially
add3 = add 3
result = 8

In this example, add 3 creates a new function, add3, that adds 3 to its argument. The new function is then applied to 5, yielding 8. This feature, known as currying, allows for greater flexibility in function application.

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 IvoryScript manages such structures efficiently. This example encourages exploration of IvoryScript’s underlying mechanisms for those who are eager to deepen their understanding.

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.