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.
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.
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.
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.
A common starting point for any programming language is writing a simple "Hello, world" script.
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
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.
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.
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"
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.
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"
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.
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"
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.
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.
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.
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]
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
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
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.
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.
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.
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.
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.
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.
The examples presented above provide an introduction to basic features of IvoryScript. Users may explore more advanced features, such as:
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.
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.