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