Introduction
Welcome to Loop! Thanks for checking out my little research project. This is the programmer's manual, you should be able to see an index of contents on your right.
Enjoy your stay.
A quick note: Please remember that while there are hundreds of tests covering the language runtime, Loop is still a young project and there may be errors you run into along the way--especially with the shell. Any help in reporting them (on the Mailing List) or fixing them (by submitting patches) is greatly appreciated.
Philosophy
As a programming language, Loop prizes readable, compact and elegant syntax. Many design choices are made in favor of clarity over convenience.
Its syntax is strongly influenced by Haskell, Scheme, Ruby and Erlang.
Loop also emphasizes performance over purity or theoretical considerations. This means that practical design choices generally rule the day. One of the key motivations behind running on the JVM is the vast array of existing libraries that are instantly available; and of course the outstanding performance of a VM techonology developed over decades, and runs easily on multiple platforms.
Programs (or scripts) are compiled on-the-fly to optimized JVM bytecode and therefore suffer no performance penalty to interpretation; all while maintaining the quick, edit-and-run responsiveness of interpreted code.
The overall philosophy is to bring together the best features of functional programming with the practical and ease-of-use lessons from modern imperative and OO languages, but to do so in a consistent, pragmatic and elegant form.
Sometimes new languages feel like an endless agglomeration of features, a collection of niche features, or just plain bolted-on, even if they have excellent ideas. Loop's balance many new ideas with performance considerations and features are designed with a comprehensive view right from the start. For example, concurrency is built right in to the language, with consistent design choices across the spectrum of features (e.g. immutable types, software transactional memory).
Being functional in nature, Loop doesn't have any of the baggage of the host platform (Java), but interoperates tightly and borrows semantically from Java where appropriate.
Loop is currently a work-in-progress, I invite you to come and play.
Getting started
You will need Java 1.6 or higher already installed. Check if you have it by running:
$ ~java -version~ Java version "1.6.0_31" Java(TM) SE Runtime Environment Java HotSpot(TM) 64-Bit Server VM
Download and unzip the Loop distribution. If using a *-nix operating system (Unix, Linux or Mac OS), in a terminal run:
$ ~tar zxvf loop.tar.gz~ $ cd loop
From the "loop" directory (created after unzipping), run:
$ ~./loop~
this will bring up the Loop shell, from where you can execute simple expressions and functions
loOp (http://looplang.org) by Dhanji R. Prasanna >> ~1 + 2 + 4~ 7 >> ~alert('hi there') ~ >> :help
Hint: :help
brings up a card of helpful hints
If you want to run Loop from other directories, add loop
to the PATH
.
If using the bash shell:
$ export PATH=$PATH:/path/to/loop $ loop
Note: make sure you replace /path/to
with the actual path =)
You can make this permanent by adding the aforementioned export
line to
.bash_profile
located in your home directory.
Note: On Windows or on platforms where the loop
shell script does not work for
some reason, you can directly invoke the runtime from the loop directory:
$ java -jar loop.jar
Building from source
If you prefer to build Loop yourself you will need Maven 3 installed. First clone down the git repo, then run the following maven command:
$ git clone https://github.com/dhanji/loop.git $ mvn clean package assembly:single
This will produce a self-contained JAR in target/
called loop-with-dependencies-1.0.jar
,
or something like that. You can rename this to loop.jar
and launch it with the loop
shell
script by setting the LOOP_HOME
environment variable appropriately.
My first loop
Like all other decent things, Loop programs are stored in their own files. You can recognize
a Loop program file by its extension .loop.
They can be executed either from the shell (using :run
) or directly from the command line:
$ ~./loop myfirst.loop~ hello there
This snippet above looks for a program file called myfirst.loop and runs it. You can create and save this file in any text editor, it contains nothing more than the following:
print('hello there')
Give this a try now. You can print other things too:
print(1 + 2) print(5 * 50 / 44) print(new java.util.Date())
These are called free expressions and may appear anywhere inside a loop function or
program. Specifically, the function print
is being called with different values.
As you will learn soon, everything in Loop is either a function or a value.
You should be able to run all of the example code in this documentation via this method (Loop program files) or via the Loop shell itself. I recommend starting with the shell and moving to program files when working with more complex examples--particularly when declaring your own functions.
Evaluation
So far you have seen how small computations and function calls work in loop. these are known as expressions, and form the basic building blocks of all programs.
$ ~./loop myfirst.loop~ hello there
While free expressions in a file are useful for writing quick scripts, if you write a larger
application (consisting of many .loop
files) you will want to define a clear entry point for
your app. Loop uses the C convention of a function called main
, which is
called by the runtime if present:
main -> print('hello there')
Give this a try now. You should see the same result.
This is the first function we've seen. All functions in Loop are declared with a
name followed by an arrow - ->
. Any expressions that
appear below, belong to the function and will be evaluated when the function is called.
function_name(arg1, arg2 ...) -> < expression >
Function main
can optionally take arguments:
main(args) -> print(args)
...which are passed in via the command line:
$ ~./loop echo.loop ahoyhoy~ [ahoyhoy]
Note that this prints a list of arguments, denoted by the brackets - []
. We'll
learn about lists and functions in later sections.
Program structure
Loop files are organized into a formal structure which is enforced by the program parser. it is fairly straightforward:
module declaration import declarations functions & type definitions free expressions
Here's an example of a loop program:
module mymodule require othermod require yet.another class Pair -> left: 0 right: 0 main -> new Pair() # comments may appear anywhere # free expressions must appear last print('mymodule is go!')
A module declaration appears at the top (usually this is the name of the loop file without the
.loop
extension), followed by require
statements that import other
modules or Java types.
Loop type and function definitions come next and may be mixed, but free expressions are always last.
Free expressions in a module are evaluated any time a module is imported and always
run before main
, which, only executes if you specifically run that file. If
you are familiar with static initializers in Java, that's essentially what free expressions are. You are
encouraged to use main when writing anything more than trivial programs.
Data structures
In addition to any available Java data structures and types, there are a few built-in types in Loop.
Strings
Strings in Loop are Java strings (Unicode, UTF-16 java.lang.String
objects).
However there are many special loop-specific extensions for working with them. A normal string
as we've seen before is enclosed in single quotes:
print('this is a string of characters')
Double quoted strings are also equivalent, but they may contain expressions that are expanded at runtime. For example:
greet(name) -> print("hello, @{name}") greet('Dhanji')
Running this program prints hello, Dhanji
to the console. Any loop expression
can be embedded inside orb tags - @{}
, similarly.
"26 * 34 is @{26 * 34}" => "26 * 34 is 884"
If you want to print the orb tags literally, fall back to single quoted strings:
'this is inside an @{orb tag}' => "this is inside an @{orb tag}"
Numbers
Loop has built-in support for most common numeric types. The following Java types are supported natively:
- Integer (32-bit)
- Long (64-bit)
- Double (64-bit)
- BigInteger (arbitrary)
- BigDecimal (arbitrary)
Loop has no support for floats, shorts or bytes, however these may be manipulated using Java
primitive wrapper types if needed. By default, any integer appearing in a Loop program is
treated as a 32-bit Integer
print(23.getClass()) => java.lang.Integer
Longs, like in Java, are specified by suffixing 'L' to a number. Note that unlike Java this must be an uppercase L.
print(23L.getClass()) => java.lang.Long
A special syntax is also available for arbitrary precision numbers (numbers too large or too precise to fit into 64 bits):
print(@23129038102938102938102938) print(@2.3129038102938102938102938) => 23129038102938102938102938 => 2.3129038102938102938102938
Notice the @
prefix denoting arbitrary precision
You can add, subtract, multiply, divide, remainder or compare any two values if they are of the same numeric type:
3 + 4 => 7 46L / 2L => 23 @2.0 > @1.9999999999999999 => true
However, unlike Java, Loop forbids mixing numeric types in expressions. You must convert all values to the same, compatible type before computing with them.
\3L + 3\ => \#error\
This is done to prevent loss of information when converting between types of different precision. You can convert any number to the largest precision type easily:
@3 + 4.embiggen() => 7 @3.0 + 4.0.embiggen() => 7.0
Note: embiggen()
is a built in function that converts integers and longs to
BigInteger
and double to BigDecimal
.
To convert a long down to an integer, you can use the intValue()
method
that Java provides:
3 + 4L.intValue() => 7
Note: Remember that this conversion necessitates a loss of information as you're going from 64 to 32 bits
Operators
Operators in Loop are symbols that combine two values to produce a resultant value, for example
the +
(plus) operator adds two numbers and produces their sum. Operators need not
necessarily produce the same type of value as the values they combine. For instance, the
>
(greater-than) operator takes two numbers and returns true
if the
first is greater than the second.
The following operators are provided by loop:
+, -, /, *, %, >, <, >=, <=, ==, not, and, or
Some of these operators work on more than one data type. For instance, the +
operator can be used
to concatenate strings together:
'1' + '2' => '12'
In fact, as long as the string appears on the left, any value may be concatenated with it:
'1' + @2 => '12'
Note that no loop operator ever mutates its operands. It simply reads and combines them to produce a third, resultant value. (the assign operator is an exception, but we'll deal with that later).
Lists
Lists are an essential component of programming in Loop, as as such they are supported
natively in syntax. All loop lists are perfectly compatible with java.util.List
and like String
, come with some nifty, loop-specific extensions.
Lists may contain any (or many) type of object. Here is a simple list of integers:
[1, 2, 3, 4, 5]
Lists are zero-based and can be indexed using familiar brackets syntax - []
third(ls) -> ~ls[2]~ third([1, 2, 3]) => 3
You can also use a similar syntax to slice portions off lists:
pick(ls) -> ~ls[0..2]~ pick([1, 2, 3, 4, 5, 6, 7]) => [1, 2, 3]
Leaving out a lower or upper bound slices the list from or to the given index:
pick(ls) -> ~ls[2..] + ls[..1]~ pick([1, 2, 3, 4]) => [3, 4, 2, 1]
Note that i used the +
operator here to add the two sub-lists together. It is
also sometimes useful to generate lists without typing out each entry. You can use the list
range
syntax to accomplish this:
pick(ls) -> ls[0..2] pick(~[1..7]~) # generate a list of 1-7 => [1, 2, 3]
The range syntax is very similar to the slice syntax except that slices must operate on some existing
list (in the above cases, a list variable, ls
). So keep that in mind when reading
Loop code.
Sets
Similarly, Loop provides a natural syntax for working with sets. A set is similar to a list except that it is a collection of unique items and that they are present in no particular order:
~{1, 1, 2, 2, 3, 3}~ => {1, 2, 3}
Loop sets are fully compatible with java.util.Set
classes.
Maps
Maps are an extremely useful data structure. Sometimes called hashtables or just hashes, a map is basically an collection of name (or key)/value pairs. They are supported natively by loop and created using a familar json-like syntax:
{ 'name' : 'Tyrion', 'house' : 'Lannister' } => { name=Tyrion, house=Lannister }
Maps may contain any type or many types as either keys or values:
{ 1 : 'one', @2 : 'two' } => { 1=one, 2=two }
Read values out of maps using simple dot syntax (like in json):
{ 'name' : 'Tyrion', 'house' : 'Lannister' }.~house~ + ' pays his debts' => "Lannister pays his debts"
Dot syntax can be chained to read from nested maps (maps inside maps) too:
~mymap.city.name~ => "Sydney"
Note that the map keys that occur after the '.' must be strings to use this syntax.
For keys that are not strings, or not known until runtime, you can use the bracket syntax, similar to indexing into a list:
read(map, key) -> ~map[key]~ read({ 'name' : 'Tyrion'}, 'name') => "Tyrion"
Here the [key]
part tells loop to look for an entry in the map whose value is
equivalent to the variable, key. Later, when we call read()
, the variable
key
contains "name"
and so prints "Tyrion"
You can also write to maps, similarly:
starkify(map) -> ~map.house: 'Stark'~ starkify({ 'name' : 'Tyrion'}) => { name=Tyrion, house=Stark }
This is the first time we've seen the assign operator ':'
(interchangeable with
'='
). We'll encounter it a lot more further on but for now, read it as assigning
the value to the right of the :
to the slot represented by its left.
Like sets and lists, maps are also fully compatible with Java's java.util.Map
type.
Trees
Like sets are to lists, loop provides a simple syntactic abstraction to represent binary trees.
A binary tree in loop is also compatible with java.util.Map
and is used to
associate keys to values. Its keys are, however, kept in natural sorted order which may be
realized during iteration. We'll see more about this when looking at list comprehensions.
['Sansa': 'Stark', 'Catelyn': 'Stark', 'Brandon': 'Stark', 'Arya': 'Stark', 'Robb': 'Stark', 'Eddard': 'Stark'].keySet() => [ Arya, Brandon, Catelyn, Eddard, Robb, Sansa ]
Note that the keys went in in an arbitrary order and when printed produced a sorted result according to alphabetical order.
Symbols
Strings are extremely useful to represent data, however they are somewhat cumbersome to represent particular kinds of data--map keys for example. Like ruby, Loop ships with support for interned strings called symbols. They are more concise to write and perform marginally better at runtime:
{ @name: 'Tyrion', @house: 'Lannister' }
In loop symbols are prefixed with the @
character and unlike ruby are
completely interchangeable with strings:
@popsicle == ('pop' + 'sicle') => true
All these data types and structures we've learned about can be combined to model rich ontologies to suit your needs.
{ @houses: { 'Lannister', 'Stark', 'Targaryen' }, @protagonists: { 'Jon', 'Dany', 'Tyrion' }, @locations: [ 'Winterfell' : 'Stark', 'Targaryen' : 'Dragonstone', 'Casterly Rock' : 'Lannister' ]}
String manipulation
Just as with lists, Loop provides a number of special syntax extensions for working with strings. These are just normal Java strings underneath. Let's look at a few: indexing works with strings exactly as it does with lists
'hello'[2] => 'l'
Note that the object returned is also a string (rather than a java.lang.Character
).
And like lists you can also slice strings:
str: 'hello' print(str[2..4]) print(str[2..]) print(str[..3]) => llo => llo => hell
You can also test if a string is a subsequence of another string with the index-of syntax:
sub?(str, smaller) -> str[smaller] > -1 'yellow'.sub?('ell') 'madam im adam'.sub?('mima') 'yellow'['lo'] => true => false => 3
These tools make working with strings more declarative and readable, in line with loop's functional philosophy.
Basic expressions
Ok, now we're armed with numeric, string and structure types and know a tiny bit about how to call functions with arguments. Let's delve a bit deeper
Calling functions
A function is one of the most essential constructs in loop. A function takes in values (called arguments) and returns a single value, always. There are no void functions in loop.
At runtime, loop functions are compiled directly into Java functions.
You call a loop function by writing its name followed by an argument list
enclosed in parentheses - ()
print('hi')
Not all functions have arguments, but the parentheses must always be present:
hi -> print('hi') hi() => hi
Loop functions can take anything as an argument: numbers, strings, objects, or even other functions:
meta(func) -> func.@call('hi') # calls print() meta(print) => hi
Note the special syntax to call a function via reference. The variable func
contains a reference
to the print()
function. To call it, we use the special form
@call
, which calls the references function print
, with the given arguments.
A function can be treated like any other value in loop, it can be stored in a variable or passed around to other functions as we've just done.
As we saw earlier you can also call methods on Java objects. This happens in exactly the same way as in Java:
print('hi'.toUpperCase()) => HI
Here, we're calling the Java method String.toUpperCase()
, and then passing its
result to print()
. You can call methods on any object in Loop this way.
print('hi'.toUpperCase().toLowerCase()) print(22L.intValue()) print(new java.util.Date().getClass().getName()) => hi => 22 => java.util.Date
Loop also provides a form for calling loop functions the way you call methods. This is known as the
call-as-method feature. Let's define a simple loop function triple()
that
takes an integer and multiplies it by 3.
triple(i) -> i * 3 triple(2) => 6
This is the standard way to call loop functions, but you can also write it like this:
triple(i) -> i * 3 2.triple() => 6
In this instance we're treating the left hand side of the function as its first argument. You can call any loop function 'as a method' this way, including ones that take multiple arguments:
multiply(x, y) -> x * y 5.multiply(6) => 30
Call-as-method provides a nice scheme for 'extending' the functionality of existing objects. All these values we've dealt with so far are standard Java strings and integers. You can even use this form to override existing Java methods with your own functionality:
toLowerCase(str) -> str.toUpperCase() 'hi'.toLowerCase() => 'HI'
Of course, you'll want to be careful when doing this to avoid confusing yourself later ;). Loop provides an escape hatch in case you want to bypass overrides and ensure that the underlying Java method is being called.
toLowerCase(str) -> str.toUpperCase() 'HI'<-toLowerCase() => 'hi'
The reverse arrow - '<-'
operator signifies that a Java method on the object is
needed. You are encouraged to use clear conventions to keep these schemes clear. For instance,
I like to use the underscore naming convention for Loop functions to distinguish them from
Java methods:
# loop function 'Now is the winter of our discontent'.to_lower() # Java method 'Now is the summer of our content'.toLowerCase()
You may instead, prefer to use the '<-'
reverse arrow to distinguish them too.
# loop function 'Now is the winter of our discontent'.toLowerCase() # Java method 'Now is the summer of our content'<-toLowerCase()
Loop doesn't mandate one or the other, although I prefer using the reverse arrow only when I want to clarify a potential override.
Navigating objects
We've looked at calling functions, now let's look at the values that functions manipulate and operate on: objects. Values in loop are generally compatible with Java objects and vice versa. Often objects are more than just a simple scalar value, they consist of a structure of values--as we saw earlier in the case of maps and lists.
We also saw that maps could be navigated by using the dot syntax, in other words, we can read values from the deep structure of the map by specifying a path to the desired slot:
food: { @name: 'Pancake', @nutrition: { @calories: 810, @protein: 0.2 } } print(~food.nutrition.calories~) => 810
In this example, we pick out calories
by specifying a path down the
graph structure of the map. This should be familiar to you if you've used ruby or python
(or any modern object-oriented language, really).
Loop provides an identical scheme for navigating all types of objects:
now: new java.util.Date() print(~now.time~) => 1335878239084
Here, I'm reading the time
property out of a Java Date
object using
the same dot-syntax. This 'property', in Java, is represented by the pair of methods:
getTime()
and setTime()
. The Java convention is to declare method
pairs like this to expose properties.
(in Ruby they are called attribute accessors.)
Loop will correctly translate this to a Java method call as required. Let's look at a more realistic example:
# Java code public class Person { private Person parent; private String name; public Person getParent() { return parent; } public String getName() { return name; } // etc.. } # loop code that reads Java object print("Gran's name is") print(~person.parent.parent.name~) => Gran's name is => Grantastic E. Gran
The property navigation syntax becomes extremely useful as you write complex programs that interact with existing Java code and libraries. Loop objects can also be navigated similarly, as we'll see in the section on classes & objects.
If/Then/Else
Branching in loop is similar to branching in Haskell or Scheme; because all expressions must produce a value, an
if-then statement must also have an else clause. The if
portion
takes any boolean expression and evaluates the then
clause if true, or the else
clause if not.
if ~3 > 4~ then 'hooray' else 'booray' => booray
Or more generally:
if < boolean > then < expression > else < expression >
The < boolean >
part may consist of any expression that results in a
true
or false
value. The entire
statement should generally appear on a single line:
classify(val) -> if val > 1000 then print('big') else print('small') 99.classify() => small
But you can split it across multiple lines if the whole expression is wrapped in parentheses:
classify(val) -> (if (val < 1000) and (val > 100) then print('middling') else print('unknown')) 101.classify() => middling
Notice I snuck in an and
keyword here. you can similarly use or
and
not
operators to combine boolean expressions, arithmetically.
List comprehension
We've already seen how lists can be sliced and their indexes read; but we can do a lot more with them. Loop provides a class of expressions known as list comprehensions to quickly transform and make sense of lists and sets. Comprehensions replace the traditional do-while constructs of C and Java. Take a look:
i * 2 for i in [1, 2, 3] => [2, 4, 6]
Here i am saying, for every item in the given list [1, 2, 3]
, multiply that item
by 2. List comprehensions act on any kind of collection (all instances of
java.util.Collection
) not just lists or sets; and they always produce lists of
equal or smaller size.
i for i in [1..10] ~if i % 2 not 0~ => [1, 3, 5, 7, 9]
Here i am filtering the list of integers from 1 to 10 by whether or not they are odd
(i.e. i % 2 not 0
). The resulting list contains only items that passed the filter.
You can even combine the filter and transformation expressions:
~i * 10~ for i in [1..10] ~if i % 2 not 0~ => [20, 40, 60, 80, 100]
Expressed more generally, a list comprehension is,
< expression > for < var > in < list > [if < filter >]
The first expression is evaluated once for every item in the list that passes the filter. The result of that expression is added to the output list. And the filter clause is optional.
List comprehensions must also appear on a single line but can be multiline if enclosed in parentheses, just like if/then/else expressions.
Null-safety
Loop is a null-safe language. This means that loop expressions will not throw a
NullPointerException
on evaluating null values. There are some exceptions to this,
but as a general rule, Loop strives to acheive a practical reduction of NPEs and the use of
null
in code.
Since there is no null
keyword, in the loop type system, we have what is known as
a bottom type. If Object
is the top type, meaning every type
inherits from object, then Nothing
is the bottom type--meaning that it inherits
from all types. No types may inherit from Nothing
, and there is only one global
instance of it, which is also referred to as Nothing
, in expressions.
Nothing => Nothing
This bit of theory aside, in practical terms you can rest assured that calling a method on
a Nothing
will not break your code--it simply returns Nothing
again
Nothing.some_method().some_other_method() => #nothing empty: {:} empty.name => Nothing
This even holds for Java code that returns null
:
''.getClass().getResource('doesnt_exist').toString() => #nothing
If you need to pass null
into Java apis for some reason, you can use
Nothing
instead:
person.parent = Nothing city.setName(Nothing) j_option_pane.showMessageDialog(Nothing, 'hi there') # ..
Loop will correctly translate this when calling into Java, without any performance penalty.
Of course, Loop cannot prevent NullPointerException
s from being thrown in Java
code(!), so code that already does will continue to behave as expected. Loop does not try to
catch and translate them into Nothing
s. Instead, you will have to handle them as you do
any other exception (more in the section on control flow).
Additionally, Loop does not permit Nothing
in computations. It is a type error to
attempt to compute with Nothing
.
1 + \Nothing\ => \#error\
This is somewhat of an escape-hatch to the bottom-type rigor. While, strictly speaking, 1 and
Nothing
are type-compatible, loop prevents mixed-type computations involving the
bottom-type. This is to prevent absurd questions like determining the value of Nothing in the context
of another type. Nothing is both the sentinel value for all types and the is contravariant with
all types. This is the sane choice when dealing with a bottom-type.
Functions
We've already been declaring and using functions throughout the examples thus far. Let's look at them in a bit more depth.
Sequences
A function generally takes zero or more arguments and returns a value. So far, all the functions have contained only one expression, but sometimes this is not sufficient. Sometimes you need to do a sequence of things in a go, this is very easy:
increment(num) -> print(num), num + 1 3.increment() => 3 => 4
Here, we're executing two expressions in sequence, first we print the value of the number, then we return the number plus one. A sequence may contain as many expressions as you need:
do_stuff() -> print('hit any key to continue...') pause() print('no, hit the ANY key to continue...') pause() print('done!') => hit any key to continue... => no, hit the ANY key to continue... => done!
Note that the last expression must not end in a comma. Haskell programmers may be familiar
with this as a do block, and for Schemers, this is the equivalent of a begin
sequence. However, unlike Haskell, in Loop, a sequence is guaranteed to execute in order.
Local scope
It is often useful to define a set of values and functions specifically for a purpose. Loop provides a lightweight and simple system to accomplish this:
minutes_in(num) -> num * year where hour: 60 day : 24 * hour week: 7 * day year: 52 * week print("there are @{minutes_in(2)} minutes in 2 years") => there are 1048320 minutes in 2 years
The local variables hour, day, week, year
are only available inside the
scope of the minutes_in()
function.
minutes_in(num) -> num * year where hour: 60 day : 24 * hour week: 7 * day year: 52 * week print(\year\) => \#error\
This is exactly equivalent to the following, more familiar looking code:
minutes_in(num) -> hour = 60 day = 24 * hour week = 7 * day year = 52 * week num * year print(\year\) => \#error\
You can even declare functions in local scope:
initials(name) -> first(words[0]) + first(words[1]) where words: split(name) ~first(word) ->~ if word.isEmpty() then '' else word[0] initials('Theon Greyjoy') => 'TG' \first\('Elric') => \#error\
Here the function first()
is scoped locally to the parent function
initials()
. It's useful to save on duplicating code within the parent function,
but not needed elsewhere, so it goes out of scope
Locally-scoped functions can also have local scopes of their own. There is no limit to the nesting depth. So please use it wisely =)
minutes_in(num) -> yearify(num) where yearify(num) -> weekify(num) where weekify(num) -> dayify(num) where ...
Module scope
By default all top-level functions in loop are public. That means any function that is not part of a where block of another function is visible to (and callable from) all other parts of the program. Now, local-scope is useful for helper functions that are private to a single function, but what if you want to share helpers between top-level functions?
Loop provides a third scope known as module scope for exactly this purpose. Functions scoped at the module level are visible to all other parts of the same module, but nowhere else. In this way you can reuse important blocks of code easily, and still hide unnecessary implementation details by sectioning them off from other parts of your app.
@last(ls) -> ls[ls.length() - 1] func1 -> [1, 2, 3].@last() func2 -> [30, 20, 10].@last() => 3 => 10 # a different module func3 -> [10, 20].\@last()\ => \#error\
You declare a function as module-scoped by simply prefixing its name with @
.
Loop will ensure that this function is restricted to the current module. Note that this is
not merely a naming convention: @
-prefixed functions have their visibility
enforced by the runtime.
Closures
Anonymous functions, are quite useful in a language like loop where functions are a first-class construct. Because they can be held in variables and passed around, it is sometimes useful to create small blocks of functionality that aren't necessarily declared as top-level or scoped functions. These blocks of code capture the variables in the nearest surrounding scope (i.e. they "close over" the current lexical scope). These are called closures:
manipulate(ls, func) -> func.@call(i) for i in ls manipulate([1, 2, 3], @(x) -> { x * 5 }) => [5, 10, 15]
In this example, I am passing a code block in the form of an anonymous function
(@
-sign followed by an argument list and ->
arrow) to the
meta-function manipulate()
, which takes a list of items and calls the given
function for each item in the list.
Notice that the closure's expression is housed inside braces - {}
. This is done
to clearly distinguish code belonging to the anonymous function from outer code. Using
braces also means you can put the expression on multiple lines and retain readability:
manipulate(ls, func) -> func.@call(i) for i in ls manipulate([1, 2, 3], ~@(x) -> { x * 5 }~) => [5, 10, 15]
The nice thing about closures is that they can refer to values in scope at the time of their creation long after the scope is lost:
make(num) -> @() -> print(num) make(10).@call() => 10
Pattern matching
So far we've looked at simple functions and sequences--functions that essentially contain a single expression to manipulate and respond to input values. This is well and good for a lot of use cases, but loop provides a more powerful, declarative construct called pattern matching.
Pattern matching in loop is inspired by the same idea in Haskell and closely mirrors the latter's concepts. When used properly, pattern matching can be extremely powerful, giving you concise, elegant programs that read like a solution rather than a list of instructions.
Pattern matching functions in loop are denoted by a =>
arrow and embody one or
more pattern rules that follow:
speak(num) => 1 : 'one' 2 : 'two' speak(2) => two
This is a naively simple pattern matching function that tests if the value passed in is either 1 or 2 and returns the appropriate string. Note that it reads like a table of mappings between inputs and outputs. This is much nicer to work with than the equivalent standard form
speak(num) -> if num == 1 then 'one' else (if num == 2 then 'two' else ...) speak(2) => two
...much less readable.
Now let's go back to the pattern matching form:
speak(num) => 1 : 'one' 2 : 'two' speak(\ 3\ ) => \#error: Non-exhaustive pattern rules\
Notice that we don't have a rule for values other than 1 or 2. What happens if we call
the pattern matching version of speak()
with 3?
Since there is no behavior specified, Loop complains that the rules are insufficient.
In the standard if/then case we can handle this with an additional else
,
however in pattern matching we use a wildcard rule
speak(num) => 1 : 'one' 2 : 'two' ~* : 'other'~ speak(3) => other
The asterisk - *
denotes any value, and is matched last, if all the other rules
fail.
Of course this will match anything not just other numbers:
speak(num) => 1 : 'one' 2 : 'two' * : 'other' speak(~'dont say a word!'~) => other
If you want the rules to be stricter (so the function only accepts integers), you can impose a type pattern rule instead:
speak(num) => 1 : 'one' 2 : 'two' ~Integer : 'other'~ speak(3) => other speak(\'say no more'\) => \#error: Non-exhaustive pattern rules\
Note that the order in which the rules appear is important.
Polymorphism
As you may have guessed, loop's alternative to function overloading is the use of mixed pattern rules. A single function can be made to perform different actions depending on the input type, using pattern matching:
times(val, num) => Integer, * : val * num String, * : val for val in [1..num] 1.times(4) => 4 'hi'.times(2) => [hi, hi]
Here we've introduced two new concepts:
- polymorphism
- multiple arguments
Polymorphism in this instance is simply allowing you to call the same function with an integer and string respectively. With multiple arguments, the second argument is a wildcard rule and is separated by a comma after the first rule. All argument rules must appear on the same line.
A pattern matching function must have a rule for each of its arguments. If you dont care what an argument is, use the wildcard rule (as we've just done). Otherwise remember that all the rules you declare are tested in the order the appear.
You can of course, freely mix type rules with other kinds of rules to easily match a variety of different inputs.
List patterns
The simplest form of list pattern matching is literally matching the empty list:
empty?(ls) => [] : true * : false empty?([]) => true
In this instance the empty list matcher matches ahead of the wildcard. You may also match a single element list:
only_item(ls) => ~[x]~ : x [88].only_item() => 88
The neat thing here is that not only do you match the pattern rule but it also lets you
extract items from the list conveniently. the variable x
holds the only list
element, and can be used in the right-hand side conveniently,
as though it were an argument itself. This is known as list destructuring.
List patterns are among the most useful of pattern matching capabilities in loop. The basic idea is to pull apart (destructure) a list into individual parts and process the items declaratively. As we'll see, this makes list processing code really easy to write and read.
reverse(ls) => [] : [] [x:rest] : reverse(rest) + [x] reverse([1, 2, 3, 4, 5]) => [5, 4, 3, 2, 1]
This function as its name implies, reverses any list passed in. The way you read this is as follows.
The reverse of the empty list is the empty list:
reverse(num) => ~[] : []~ [x:rest] : reverse(rest) + [x]
The reverse of any list that has at least one item is the first item (x
)
appended to the
reverse of the remaining items (rest
).
reverse(num) => [] : [] ~[x:rest] : reverse(rest) + [x]~
This is what's known as a recursive function, meaning that it calls itself (recurses)
in order to complete its task. Let's look at how this function destructures a simple list like
[1, 2, 3]
reverse([1, 2, 3]): 1:[2, 3] : reverse(~[2, 3]~) + [1] reverse([2, 3]): 2:[3] : reverse(~[3]~) + [2] reverse([3]): 3:[] : reverse(~[]~) + [3] reverse([]): [] : ~[]~ => [3, 2, 1]
Each line above represents a call of the function. On the left are the arguments it is called with, and on the right the pattern rule that matched and its values, after destructuring. What happens is that each additional recursive call gets a step closer to the empty list and when you unravel this stack of calls, you get the list in reverse order.
Read the rightmost part of the expression (in bold) from the bottom up to illustrate the unraveling:
reverse([1, 2, 3]): 1:[2, 3] : reverse([2, 3]) ~+ [1]~ reverse([2, 3]): 2:[3] : reverse([3]) ~+ [2]~ reverse([3]): 3:[] : reverse([]) ~+ [3]~ reverse([]): [] : ~[]~ # reading upwards: ~[] + [3] + [2] + [1]~ => [3, 2, 1]
Object patterns
Any object (including maps and Java objects) can also be pattern matched, like lists. The primary use is to destructure object graphs and enable more declarative code that is easier to write and read.
gran(person) => ~{ g <- person.parent.parent }~ : g.name
This will print person
's grandparent's name, if you have an object graph that looks
something like this:
billy: { @name: 'Billy', @parent: { @name: 'Willy', @parent: { @name: 'Silly' } } } gran(billy) => Silly
Let's look at this code in a bit more detail:
gran(person) => ~{ g <- person.parent.parent }~ : g.name
First we have a new type of pattern rule, encased in braces - {}
. This signals
to loop that the function should expect an object to destructure. Next there is a variable,
g
, followed by a reverse arrow <-
, which indicates that
g
should receive the result of the destructuring.
gran(person) => { ~g <-~ person.parent.parent } : g.name
On the right side of the reverse arrow is the path expression in dot syntax. Recall path
expressions from the section on object navigation. In this instance we are asking for
the parent
of the parent
of argument person
.
gran(person) => { g <- ~person.parent.parent~ } : g.name
Finally on the right hand side of the rule itself, is another path expression
g.name
, which, is the result we want to return if this pattern rule matches.
gran(person) => { g <- person.parent.parent } : ~g.name~
This is a rather simple example, and it can be rewritten as a standard function as follows:
gran(person) -> person.parent.parent.name
But for more complex object patterns, or for polymorphism, the pattern matching form is quite useful. You can add as many destructurings as required in a single object pattern rule:
gran(person) => { g <- person.parent.parent, ~n <- person.name~ } : "@{n}'s gran is @{g.name}" => Billy's gran is Silly
And you can also mix object pattern rules with other kinds of patterns.
String patterns
String pattern matching is one area where loop really shines. There are a variety of useful tools for matching patterns in text. These can be combined to make quite powerful parsers and text processors, that are readable and concise.
First, as you might guess, any string literal can be matched as a simple rule:
numberize(str) => 'one' : 1 'two' : 2 'three' : 3 numberize('two') => 2
This scheme will also match symbols:
numberize(str) => 'one' : 1 'two' : 2 'three' : 3 numberize(@two) => 2
Of course, for clarity you should use symbols directly in your pattern rule where appropriate:
numberize(str) => @one : 1 @two : 2 @three : 3 numberize(@two) => 2
But this is just the tip of the iceberg. Strings can be destructured in a manner very similar to lists:
initial(name) => (~i:rest~) : i initial('Megatron') => M
Here the destructuring works by pulling apart the leading character of the string. Note that
we use parentheses - ()
instead of brackets - []
in the pattern rule.
This is necessary to inform loop that we are matching a string and not a list.
But strings can be destructured in even cooler ways. By specifying a delimiter, you can tell loop to split a string around a particular sequence:
flip(name) => (~first~ : ' ' : ~last~) : "@{last}, @{first}" flip('Optimus Prime') => Prime, Optimus
Note that in this example, we use a single space to pattern match around. All the characters
before it are destructured into first
and those after it into last
.
There is no limit to how many delimiters you can add to create a pattern:
select(str) => ('A':a:'B':b:'C':c:'D':d) : a + b + c + d select('A1B2C3D4') => 1234
And there is no restriction on the length of the delimiters:
extract(str) => ('Now is the ': ~season~ :' ':ignore) : season extract('Now is the winter of our discontent') => ~winter~ extract('Now is the summer of your content') => ~summer~
You can use this technique recursively to process several lines, from a text file for example:
uncomma(str) => '' : '' (word: ',' : ~rest~) : word + '\n' + ~uncomma(rest)~ uncomma(read('bots.csv')) # given a file, 'bots.csv' containing: Perceptor,Astrotrain,Starscream,Soundwave => Perceptor => Astrotrain => Starscream => Soundwave
Notice the base case for the empty string, which is needed to terminate the recursion when the end of the file text is reached.
On top of this, loop also provides powerful regex pattern matching:
judge(str) => /(t|w)alk(ing)?/ : print('Good') * : print('Lazy!') judge('talk') judge('walking') judge('idle') => Good => Good => Lazy!
In this instance, the regular expression (signified by bounding slashes - //
) is
used to match whether or not the first rule should apply. It is slightly simpler than the
earlier examples in that it does not destructure the string in any way, instead is used to
accept a more flexible range of inputs.
But! If you really want to destructure using a regex pattern, that is also
possible using a regex standard known as named capturing groups. A named capturing
group is any capturing group
occurring inside a regular expression (enclosed in parentheses - ()
) that also is
tagged with a name. This is done using a special syntax.
the name is then extracted and made available as a variable:
extract(activity) => /(?< ~verb~ >(t|w)alk)(ing)?/ : verb extract('walking') extract('talk') extract('talking') => walk => talk => talk
See this website for more information on named capturing groups and how to write them. But do use them sparingly, excessive use of named groups can make regexes (which are, let's face it, already quite messy) totally illegible.
Guards
Guards are an addendum to pattern matching and can be seen as an extra line of defense for declarative code before resorting to if/then/elses function bodies. Whereas pattern rules focus on the type and structure of the data, guards are meant to be able to test specific conditions using arbitrary logic. Let's take a look:
gender(name) => String | name.startsWith('Mr.') : @male | name.startsWith('Mrs.') : @female | name.startsWith('Ms.') : @female | else : @unknown * : @error gender('Mr. Rogers') gender('Ms. Marple') => male => female
The trick here is that a guard is just any boolean expression that allows the execution of
the right-hand expression if true. You can place arbitrary logic in a guard expression, as
long as it evaluates to true
or false
:
gender(name) => String | name.startsWith('Mr.') : @male | name.startsWith('Mrs.') : @female | ~name.startsWith('Ms.') :~ @female | else : @unknown ...
In our case we're testing the salutation of a name against a number of literal values (Mr, Mrs
and Ms). But notice that we have an extra clause at the end, an else
clause. This
is necessary if the pattern matches but none of the other guard expressions do. All guarded
patterns must have an else
expression (if you recall what we said about
if/then/else expressions, this will make sense).
Furthermore, it is important to distinguish between the else
expression, which is
effectively the default case against a matched pattern and the wildcard
pattern--which is a catchall if no patterns matched.
In the example case we can trigger the else clause by specifying any string that is missing a salutation:
gender('Pat') => unknown
This is different from the wilcard rule, which matches anything that failed the previous patterns (and is effectively polymorphic):
gender(\ 22 \) => \#error: non-exhaustive pattern rules\
In a sense, you can think of guards as analogous to a switch
statement in C or
Java, except that they can handle any kind of expression, and do so in linear order. A better
analogue might be an if-elseif-elseif-elseif-... style block, with a final else. Except that
guards are far easier to read and far simpler to write.
Here is a guarded function that determines if an integer falls into predefined ranges:
range(x) => * | x > 100 : @large | x < 0 : @negative | x < 100 : @small | else : @exactly100 range(224) range(4) range(-6) => large => small => negative
Notice how the order of evaluation played a part in our function, which correctly classifies
values between 0 and 100 as small, even though the @small
rule is only written to check for
x < 100
. Using guards this way makes complex branching easy to visualize and
reason about.
Sometimes people are confused between when you need to use a guard and when you use just another pattern rule. There are no strict guidelines about this, but one way to think of it that patterns are about the structure of objects, and guards about their value (or data).
This distinction becomes clear when you consider that pattern rules do not cause any functions to be called, whereas guards often do. Meaning that pattern rules describe the static behavior of the program (what types are accepted, what structure of objects, or pattern of strings), and guards describe the dynamic behavior of the program, i.e. what result is produced if a condition is satisfied.
Guards are useful in a variety of situations, and they may be applied on any kind of pattern rule. Look at some Loop examples to get a feel for how guards can be used.
Control flow
Since loop is a JVM language, it honors the Java system of exceptions for handling control
flow in error cases. Loop exceptions are just Java exceptions except that it does not support
the concept of checked exceptions. all exceptions in loop are unchecked,
usually a kind of RuntimeException
.
Raising exceptions
Exceptions in loop are thrown using the raise()
function which is always
available in all programs:
divide_by(x, y) -> if y not 0 then x / y else ~raise~('divide by zero') main -> 1.divide_by(0) => \#error: divide by zero\
This simplistic function checks if the divisor is equal to 0
and raises an
exception if so. The function raise()
terminates execution immediately and
unravels the function stack until it is handled or the program exits in error.
The familiar JVM stack trace showing each function call in the chain of execution that led to this exception is generated properly by all Loop programs. Here's an example:
java.lang.RuntimeException: ~divide by zero~ at prelude.raise(prelude.loop:9) at _default.divide_by(test.loop:3) at _default.main(test.loop:6)
By default, raise()
throws a java.lang.RuntimeException
with an
embedded cause message.
(this example presupposes that the divide_by()
function is saved in a file called
test.loop
, which is called from the command line using the loop runtime.)
Otherwise, it is nice to see that the trace points correctly to the offending line of code,
line #3 in test.loop
:
java.lang.RuntimeException: divide by zero at prelude.raise(prelude.loop:9) ~at _default.divide_by(test.loop:3)~ at _default.main(test.loop:6)
You may also note that the stack trace is clean, and only presents Loop functions with none of the intermediate Java gunk that other JVM languages sometimes leave behind.
Of course, if your loop code calls third-party Java methods you may see it after all ;)
Handling exceptions
Exceptions in loop are handled via a specialization of pattern matching functions called handler forms. These are pretty much the same as a pattern matching function, but with some additional constraints imposed. Let's try to handle the divide-by-zero error from the previous example:
divide_by(x, y) ~except handler~ -> if y not 0 then x / y else raise('divide by zero') ~handler~(e) => * : e.message 1.divide_by(0) => divide by zero
There are a couple of changes we've made here. Firstly, there is a new function simply called
handler()
(you can call it whatever you want). Secondly, we have added a clause
to the divide_by()
function which reads except handler
. This is the
exception handler clause which tells loop to catch exceptions thrown by the declaring function
and hand them off to the specified function for processing.
Furthermore, we see that this time there is no error, instead we simply get back the message
divide by zero
, which was extracted from the exception object itself:
divide_by(x, y) except handler -> if y not 0 then x / y else raise('divide by zero') handler(e) => * : ~e.message~ 1.divide_by(0) => ~divide by zero~
Since the exception is passed as an argument to handler()
, we can use pattern
matching to provide the appropriate recovery response.
This lets you handle any exception thrown using raise()
. You can even use guards
here to do interesting things:
divide_by(x, y) except handler -> if y not 0 then x / y else raise(~'dbz'~) handler(e) => { msg <- e.message } | ~msg == 'dbz'~ : 0 | else : raise(e) 1.divide_by(0) => 0
Here we're using a combination of object pattern matching (see earlier section) and guards to check if the exception contains a particular message. If it does, we return zero as if nothing abnormal happened. Otherwise, the exception is rethrown.
Handling Java exceptions
Of course, loop's simple exceptions aren't the only kind you'll have to deal with. Java
exceptions come in a variety of types that don't all extend from RuntimeException
.
To handle these properly, we can use a type pattern rule:
io_handler(e) => IOException : e.message * : raise(e)
In this example, we're catching any instance of IOException
and returning the
message. For all other kinds, we rethrow the exception. The wildcard rule will catch any
subtype of java.lang.Exception
including all RuntimeException
s.
Loop will correctly generate the Java bytecode to catch only the exceptions specified by type pattern rules, in the order they are specified. So if you want to you can even write:
io_handler(e) => FileNotFoundException : raise(e) IOException : e.message
This handler will rethrow the exception if a file is not found, otherwise it simply returns
the error message, catching the exception. However, note that since we have only specified
two exception types, anything that's thrown of a different type, say
NumberFormatException
, won't be caught or handled.
Loop is smart enough to catch only the specific exception types--note that there is no catch/analyze/rethrow penalty, as the correct bytecode is generated underneath.
Please also remember that the wildcard rule will only catch Exceptions
, not
java.lang.Error
s or other types of java.lang.Throwable
. If you wish
to catch these types, you must specify them explicitly:
io_handler(e) => FileNotFoundException : raise(e) ~OutOfMemoryError : exit(1)~ * : e.message
Classes and objects
In addition to working with Java classes and objects, Loop also provides its own, more ideal type system. Classes in Loop are simple graphs of data that have no private state and no methods. Formally, they define a grouping of properties identified with a common purpose or model. All properties are publicly accessible by all functions. You can think of Loop classes like structs in C or C#, or as a formalization of the map data structure we encountered earlier.
Constructors
Loop objects are simply instances of these classes. They are created using the new
keyword and all have implicit constructors.
class Star -> name mass new Star()
This will create an empty object, which, is no different from the empty map:
new Star() == {:} => true
Like maps and Java objects, reading the properties of a Loop object is done via dot-syntax:
star: new Star() ~star.name~ => Nothing
Every Loop type automatically has a dynamic set of constructors for providing its initial set of properties:
class Star -> name mass new Star(name: 'Proxima Centauri', mass: 0.123)
Note that these are constructors that take named arguments. Each argument is tagged with the name of the property for which you are specifying the value. This makes it clear what you're passing in when you have large constructors.
Of course, if you don't know a value at construction time, you can simply leave it out:
class Star -> name mass new Star(name: 'Proxima Centauri')
This object will return nothing when you query its mass
property, but its
name
has been set.
class Star -> name mass star: new Star(name: 'Proxima Centauri') star.name ~star.mass~ => Proxima Centauri => Nothing
Setting (or re-setting) a property is done with the assign operator:
class Star -> name ~mass~ star: new Star(name: 'Proxima Centauri') ~star.name: 'Errai'~ star.name => Errai
Defaults
Loop has no explicit constructor function, instead if you want to specify certain default values that are preset on all instances of a class, you can directly specify it in the class definition:
class Star -> name ~mass: 0.123~ star: new Star(name: 'Proxima Centauri') star.mass => 0.123
Any expressions specified in the class definition will be evaluated along with the constructor and stored in the appropriate property when the object is instantiated. You are free to invoke methods or specify any other kind of expression here. The only restriction is that you may not refer to other properties of the same object.
class Star -> name default_mass: 0.123 \mass: default_mass\ => \#error\
If you want to share a value like this, you must specify it explicitly in a constructor.
While Loop objects resemble freeform maps, there are some important restrictions:
- properties cannot be deleted
- you cannot set functions in properties
- you cannot set closures in properties
Immutability
Immutability, or the idea of an object whose structural data cannot change once created, is a very important precept in any language that allows concurrency (multi-threaded programming). Loop is no exception. In fact, since Loop has a strong focus on concurrency, immutability is integrated into the core of the language itself.
Threads in Loop are not allowed to share mutable state (except via memory transactions, which we'll see later). In order to allow them to share fixed data, however, Loop provides a special construct called immutable types.
immutable class Star -> name mass star: new Star(name: 'Proxima Centauri', mass: 0.123) star.name \star.name: 'Errai'\ => Proxima Centauri => \#error\
Note the addition of a new keyword to the class definition: immutable
. This tells
Loop that instances of class Star
may not be altered after they are created. So
any properties must be set via the constructor.
In this case, an attempt to mutate the value name
resulted in a
mutation error. This ensures that we can share objects like this between threads, safely,
without worrying about their data becoming corrupt or inconsistent. We'll see how this is
done in much greater detail, in the section on Concurrency.
Duck typing
We've briefly seen that objects in Loop can be compared with maps in a compatible fashion. This is part of a much broader feature known as duck typing. It involves the idea that an object whose structure resembles that of another is equivalent to it.
Let's say we have a function that prints the name and age of a Star:
name_age(star) -> print("@{star.name} is @{star.age} years old") name_age(new Star(name: 'Proxima Centauri', age: 40000000)) => Proxima Centauri is 40000000 years old
Now, this function expects an argument called star
, but really it expects any
object that has two properties: name
and age
. So we can easily write:
class Person -> name age name_age(new Person(name: 'Nikola Tesla', age : 142)) => Nikola Tesla is 142 years old
This is completely legal and works as expected because of duck typing. In other words:
if it walks, talks and quacks like a duck, then it is in fact, a duck.
Java interop
As said earlier, Loop interoperates well with Java. So far we've seen how to call instance methods on Java objects seamlessly; and create new Java objects by calling their constructors. Now let's take a quick look at some other conveniences.
Static methods
Loop has a special syntax for referring to static methods of Java classes:
~`java.lang.System`~.currentTimeMillis() => 1337221827894
Note the backticks - ``
around the type reference to java.lang.System
.
This tells loop that we are referring to a Java type and must resolve any method calls as a
static member of the class. The function currentTimeMillis()
returns the current
system clock time in number of milliseconds since the beginning of the epoch.
Static fields
Class member (or static) fields are similarly resolved on Java types, but must be specified using a special operator:
~`java.lang.System::out`~.println('hi') => hi
Notice the ::
operator which designates the name of the static field (in our case,
out
). The entire field reference must appear inside the backticks and the
resolved field left to be evaluated against whatever expression you put around it. In this
example, I'm calling the method println()
which simply outputs a string to the
console. In fact, this is exactly how Loop's own print()
function works internally.
Imports
This code can be written shorter with a type import from java-space:
require java.lang.System ~System::println('hi')~ => hi
Type references
It is sometimes useful to resolve a Java class itself. Some libraries take classes as arguments, or you just may want to ensure that a Java class is available in the current process. As you might guess, you can refer to a Java type using the same backticks syntax we've seen so far:
print(`java.util.Date`) => java.util.Date
This is the equivalent of the following Java code fragment:
System.out.println(java.util.Date.class)
Modules
Modules in Loop are ways of packaging and shipping utility programs for use with other Loop programs. Modules may also be used as a simple way of organizing a large project and are usually composed of cohesive groups of functions and classes.
The process of making such a module available for use in your program is called importing
.
In Loop, you use the require
keyword to import a module by name:
require file print(read('file.txt'))
Here, the module file
is imported for use by our program. It provides the
function read()
, and we can thus use it without declaring it specially. This
function reads the contents of a file named autobots.txt
and returns a string.
require file print(~read('autobots.txt')~)
Go ahead and create yourself a text file (in any editor) with a list of autobots and run the program. For me this prints out:
Hot Rod Arcee Ironhide Perceptor Optimus Prime
Nice! We didn't have to call any Java code or define any additional functions. The file
module did all the work for us. This module ships with loop, and is part of its core library.
It provides many other functions useful for manipulating and working with files.
Now, all the functions in the file module are available in your program. However, this can present
a problem if we already have functions with the same names. Let's take read
as an
example:
require file ~read(val)~ => /[0-9]+/ : val.to_integer() * : val print(read('autobots.txt')) => \autobots.txt\
Now we have declared a function that converts strings to integers that is also called
read()
. When we run the program, we dont get any errors, rather we get the
wrong behavior. One way to fix this problem is to rename our function to something like
@read()
, but this is not a great solution. Both functions are legitimately called
read()
and there is no reason why one should have to give up the name to the
other.
Loop provides a feature known as import aliasing to solve this problem:
require file as f read(val) => /[0-9]+/ : val.to_integer() * : val print(f.read('autobots.txt'))
In this example, we have added a suffix to the require declaration, as f
. This
tells Loop to box all the functionality in module file into a namespace called
f
(you can call this whatever you like, even file
). Now, when we use
functions from the file
module, we prefix the function call with the alias:
require file as ~f~ read(val) => /[0-9]+/ : val.to_integer() * : val print(~f.read~('autobots.txt')) => Hot Rod => Arcee => Ironhide => Perceptor => Optimus Prime
Now when we run the program, Loop correctly resolves the read()
function against
module file
and we get our desired list of autobots.
First module
So far, all the programs we've written have not been in any particular module. By default,
Loop puts your program into a module called _default
(inside the Loop shell, this
is the _shell
module). This is a convenience that works well for a small script,
but isn't really practical once you have an application consisting of several interacting
loop scripts.
To name your module, place a module declaration at the very top of the script:
~module botreader~ require file as f read(val) => /[0-9]+/ : val.to_integer() * : val run -> print(f.read('autobots.txt'))
Here, we've taken the last example and added a module declaration. To make this work properly,
you must save this file as botreader.loop
. The module name must match the
file name in most cases (there are some exceptions which we will look at later).
Now we can use the botreader
module from other loop programs, easily:
require botreader run() => Hot Rod => Arcee => Ironhide => Perceptor => Optimus Prime
Note that this only works if both loop scripts are in the same directory. Modules bundled
with loop (such as file
) or in packages do not need to be in the
same directory. We'll see what this means in the next section.
Packaging
To be documented.
Prelude module
In a lot of these examples, we've used functions like print()
and last()
quite liberally. These are not declared anywhere, nor are we explicitly importing them from
any module with a require directive. How then are they popping up in our code?
The answer is the prelude module. All Loop programs implicitly import a built-in module
called prelude
, which provides a bunch of common functionality for convenience.
These are some of the functions that are part of prelude:
General functions
- print() - writes a value to the console
- alert() - shows a popup dialog with given text
- to_integer() - converts a string with numbers to an integer
- embiggen() - converts numbers to greater precision
- exit() - terminates the program execution
List & String functions
- last() - returns the last item in a list or string
- push() - adds a value to the end of a list
- pop() - removes and returns the last item in a list
- head() - returns the first item in a list
- tail() - returns a slice of the list with all items after the head
- reverse() - returns a copy of the list with items in reverse order
You can inspect the full source code of the prelude module here: https://github.com/dhanji/loop/blob/master/resources/loop/lang/prelude.loop
Just as we saw with the file module and our custom read()
function, you can also
have collisions with the prelude module. While you can't prevent the import of prelude, you
can definitely alias it to prevent function name collisions.
require prelude as ~pre~ print(val) -> ~pre.print~(val.toUpperCase()) print('hi there') => HI THERE
Hierarchical modules
To be documented.
Importing jars
We've seen how to refer to Java classes, constructors and static methods in the previous sections. These suffice for any of the APIs provided with the JDK itself, however if you want to import third-party Java libraries, you need to provide them to your programs first.
If using the loop
shell script bundled with the launcher, this is very simple.
First, create yourself a directory called lib
under the directory where your
Loop program files are (this can be anywhere in your system, and can be completely independent
of where you unzipped the Loop distribution). Now, place any third-party .jar files in
the lib
directory and they will automatically be loaded when running your program.
$ ls lib/ joda-time-2.0.jar
In the above example, I have placed the third-party jar for joda-time
in my lib
directory. Now when I run any Loop program that refers to joda-time classes, it will load them
from the provided jar file. Here's a sample program (called time.loop
):
require `org.joda.time.DateTime` print(new DateTime())
To run it, use the same command line as you are familiar with (but make sure to do it from
the same directory as the time.loop
program file):
$ loop time.loop ~2012-05-31T21:40:44.554+10:00~
The output is a string representation of joda-time's DateTime
object, holding the
current system time. You can add any number of jar files this way, to create a library for your
Loop program.
Classpath
If for some reason you don't want to use the lib
directory or want to import jars
in another location, you can directly launch the required jars using a java command line as
follows:
$ java -cp /path/to/loop.jar:[deps] loop.Loop time.loop
Note that the deps must be separated by a :
and remember to replace
/path/to/
with the real path to your unzipped loop distribution.
Concurrency
Much of Loop's design is based around the idea that programs are increasingly running on multiple CPUs (or cores) and that there is a pressing need for robust and simple tools to create concurrent programs. Loop takes the position that these tools are best provided by the language itself, and that the nitty-gritty of particular low-level concurrency constructs (mutexes, semaphores, CAS instructions, etc.) should not intrude on everyday programming.
This is not to say that you should not familiarize yourself with these concepts, and know them in reasonable detail, but that you should not have to work with them for most practical uses. One analogy might be that while an astronaut should be reasonably familiar with the physics of space flight, she should not have to work out the Newtonian propulsion equations to ignite each stage of a rocket during flight. A lever or button would suffice.
There are two principal constructs in loop for working with concurrency: channels and memory (or cell) transactions. Both are well-known, and well-established tools for writing concurrent programs, but Loop takes them a step further and integrates them deeply into the language itself, and with each other.
Channels and message-passing
You may be familiar with message-passing as a framework for coordination between multiple concurrent processes. Many languages and libraries provide something like this. The theory is that if concurrent processes only ever communicate by sending messages to each other, then there is no shared mutable state, and limited potential for memory corruption, deadlocks and starvation.
Channels
Channels are an abstraction over message passing and thread pools that allows you to work in terms of events and event handling functions. A channel consists of an event queue and a handler function, backed by a pool of threads. Let's take a look:
require channels print_message(msg) => -1 : @shutdown * : print(msg) main -> channel(@printer, print_message, {:}), @printer.send(i) for i in [1..10], @printer.send(-1)
The first thing you'll notice is that we're importing a module called channels
;
this is a built-in module that ships with loop to provide all the concurrency tools you'll
need for working with channels.
Second, we have a pattern-matching function, print_message()
that takes a single
argument and in the general case, just prints it out. If this argument is equal to
-1
, however, the function returns a symbol named @shutdown
. This is
a special symbol that the channels API recognizes and responds by shutting down the channel.
Finally we have a few lines in main()
to establish a channel and send it some
events to process:
main -> ~channel(@printer, print_message, {:}),~ @printer.send(i) for i in [1..10], @printer.send(-1)
Calling the function channel()
establishes a channel called printer, whose
event function is print_message()
. Any events received by this channel will be
dispatched to this function. Lastly, the empty map - {:}
is used to say that we
want the default channel options (we'll see how to use some of these options later).
More generally, this function has the following signature:
channel( < name >, < function >, < options > )
- name - a symbol naming the channel
- function - reference to a function to handle events
- options - a map of options for the new channel
Finally, to send events to the channel we've just created, we use the send()
function. This function takes two arguments, the name of the channel and a message to send.
This message can be any object, but it must be immutable. Loop will complain if you try
to send a mutable object to a channel.
main -> channel(@printer, print_message, {:}), ~@printer.send(i) for i in [1..10]~, @printer.send(-1)
When the messages arrive, they are dispatched as individual events to the event function. This is done in a fair manner over a thread pool that is shared between all channels. Note that in this example, I'm sending a sequence of numbers from 1 to 10, individually.
This produces the following output:
6 4 3 2 9 5 7 8 10 1
The numbers appear in a random order, which might be surprising, but in fact this is channel fairness in action. When we send the 10 numbers, they get pumped into the channel's event queue, and then the channel's event function goes hell for leather trying to process the queue. Since these events are being processed in parallel, the order of output is unpredictable (and random).
Serialized channels
If we take the previous example, and make one small change to the channel options:
require channels print_msg(msg) => -1 : @shutdown * : print(msg) main -> channel(@printer, print_msg, { @serialize : true }), @printer.send(i) for i in [1..10], @printer.send(-1)
Then we get the following output instead:
1 2 3 4 5 6 7 8 9 10
Perfectly ordered. This is because we specified that the channel is a serialized channel, meaning that events are dispatched exactly one at a time. Even if there are many threads available in the worker pool, a serialized channel will only use a maximum of one thread ensuring that all events in its queue are processed serially.
This is an extremely useful design pattern, particularly when you are dealing with large numbers of users, for example. Since channels are lightweight (much lighter-weight than threads), you can allocate one channel per user and fire off events as they arrive. You don't have to worry about synchronizing code for a single user, because the serialized channel ensures that it happens in sequence.
Channels can be shutdown and re-established as many times as you like. But it is recommended that you hold a channel open for the lifetime of the application. Coordinating their orderly shutdown and restart in the middle of a program's life can be tricky, so only do it if you really have no other way.
Channel memory
Serialized channels also provide the ability to keep some data around for the next event that arrives. This allows you to accumulate state incrementally as each event arrives and use this for internal processing. Since this state is mutable (i.e. you can modify it), it is only visible inside the channel. Here's an example of a simple counter using serialized channels and memory:
require channels increment(msg) => -1 : @shutdown @print : print(mem[@count]) * : mem[@count] = mem[@count] + 10 where mem : ~channel_memory()~ main -> channel(@printer, increment, { @serialize : true }), @printer.send(i) for i in [1..10], @printer.send(@print), @printer.send(-1)
Notice the function channel_memory()
, which obtains a reference to the current
channel's memory. We save this to a local reference, mem
for easy access.
Now, we've changed the printer slightly from the previous iteration, instead of merely
printing the message, we increment a counter (a slot in mem
called
@count
) by 10 each time.
Finally, before shutting down, we send a special message @print
, which just dumps
the value of @count
in the channel memory to the console. Running this function
produces:
100
You can store anything you like in channel memory, it acts just like a map. And the data will
stick around until you either set it to Nothing
or shut down the channel. But
remember that this will not be accessible in other channels or even in main()
:
require channels increment(msg) => -1 : @shutdown @print : print(mem[@count]) * : mem[@count] = mem[@count] + 10 where mem : channel_memory() main -> channel(@printer, increment, { @serialize : true }), @printer.send(i) for i in [1..10], print(\channel_memory()\.count) @printer.send(-1) => \#error: illegal shared memory request\
Of course, each serialized channel gets its own channel memory, so whatever data you set in one serialized channel won't be available in another serialized channel's memory either.
Concurrent channels
Let's go back to non-serialized (or concurrent) channels. These run as fast as possible and in parallel with one another. It is important to remember they have no channel memory. If you want to keep state around and modify it in parallel you'll have to use cell transactions, which we'll look at later.
Now, all channels run on the same shared thread pool. This thread pool starts with a small number of threads and expands with the rate of events being sent to all channels. If a particular (concurrent) channel is receiving too many messages and using up the majority of threads, the Loop runtime will attempt to balance the workload by making busy channels yield some time.
However, it is not always desirable to rely on this--the yielding only occurs between
events, and you may wish to have a more rigid (or dedicated) balance of threads per channel.
Loop allows any concurrent channel to have its own dedicated worker thread pool by specifying
the workers
configuration option:
require channels print_msg(msg) => -1 : @shutdown * : print(msg) main -> channel(@printer, print_msg, { @workers : 4 }), @printer.send(i) for i in [1..10], @printer.send(-1)
Here, I'm ensuring that channel printer
gets its own dedicated thread pool
consisting of 4 worker threads. All events sent to this channel will then be dispatched only
on this thread pool. Note that you can simulate a serialized channel by specifying only 1
worker:
require channels print_msg(msg) => -1 : @shutdown * : print(msg) main -> channel(@printer, print_msg, { ~@workers : 1~ }), @printer.send(i) for i in [1..10], @printer.send(-1)
Then the output produced is:
1 2 3 4 5 6 7 8 9 10
This incidentally ensures that only one event gets processed at a time, which is similar to what we saw with serialized channels. Of course serialized channels are much more sophisticated than this--they have channel memory, and consume less resources, balancing fairly on the global thread pool, but it is still an interesting similarity.
Cells and memory transactions
To be done. This feature is still a WIP and will be documented when complete.