This is a bunch code samples in the Droid language. It has been put in an order that may help introduce the language and it's strengths to newcomers. Here's the grammar.
If you prefer, you can take a look at the core language. That description is much more technical, but also more precise. It is mostly meant as a help for compiler and tool implementors. It will be even less informal later on.
The language presents few original concepts, but combines concepts that are currently not widely used outside academia and not widely available in mainstream languages with more widespread features. The core is somewhat minimalistic.
The package system is being designed separately, and this page contains the information. Dependency management is currently planned to be part of the package system.
The classic, but boring, first impression of a language.
object Hello do Console.writeLine("Hello, world!") end
This isn't exactly BASIC's print "Hello, world!"
, but at least there's no main
method to implement. The syntax is chosen to avoid having all sorts of stuff in the global namespace. Only programs as small as Hello
will suffer from it. In the following examples, we will omit the scaffolding and concentrate on the code you can put in the do ... end
block. The top level constructs will be explained last.
I can't think of very many imperative languages that don't have all of these. However, their syntax and semantics are a tiny bit different.
if a > b do # what to do if a is greater than b elseif a < b do # what to do if it's less else # what do do if they're equal end
You can have as many elseifs
as you like (even none) and the else
part is optional. However, it also returns the value it evaluates:
val x = if hot do 30 else 10 end
In the above, x
is 30 if hot is true and 10 otherwise. If the else
part is missing, the return type is Void
.
var x = 10 while x > 0 do x -= 1 # subtract one from x end
The above while loop counts repeatedly subtracts one from x
until it becomes zero. Surprised? Didn't think so!
One of the most common kinds of loops is the one that iterates over a collection and examines each element in it. Most modern imperative languages have a construct for this called "foreach". Droid takes it a bit further.
val names = ["Jordan", "Michelle", "Peter"] for name in names do Console.writeLine(name) end
This is the standard foreach loop behavior. It prints:
Jordan Michelle Peter
There are certain uses of the foreach loop that occur very frequently in any given program. One of them is this:
val xs = Array[Int](ys.Size) for y in ys do xs.push(foo(y)) end val result = xs
Where foo
can be more or less involved. This is commonly called mapping in the functional programming world. The foreach loop in Droid actually returns a list of the result of each of it's iterations. This means that this is equivalent to the above (except it returns a List
, not an Array
):
val result = for y in ys do foo(y) end
Sometimes you only want some of the elements. Removing those elements that are of no interest to you is often called filtering or selecting .
val result = for y in ys where y > 100 do foo(y) end
The above selects only those elements that are greater than 100, and applies foo(y) to each of them before returning them in a list. It is similar to a list comprehension (as in Python, Haskell and math), and to the (pseudo) SQL statment SELECT foo(y) FROM ys AS y WHERE y > 100
.
A foreach loop works on everything that implements the Iterable
interface:
for x in 5 do Console.writeLine(x) end
Integers are iterable. The iterator for a given integer N will iterate from 0 to N - 1 (both inclusive, and empty if N <= 0). The above will print:
0 1 2 3 4
This is especially useful if you want to iterate over array indices:
val fruits = ["bannana", "apple", "orange"].toArray() for i in fruits.Size do Console.writeLine("Fruit #" + i + " is " + fruits(i)) end
Which prints:
Fruit #0 is bannana Fruit #1 is apple Fruit #2 is orange
When iterating over a map/dictionary of some kind, you get an object back that that has both the key and the value. You can use pattern matching to pull out the values:
for (Key: k, Value: v) in myDictionary do Console.writeLine("" + k + " => " + v) end
Garbage collection takes care of freeing memory and other resources that you don't need. However, sometimes you don't want to wait for the garbage collector to come around and clean up for you. When you have acquired sparse resources such as files and mutex locks, you'd better hurry up and release them as soon as you no longer need them, so that other parts of your program (or indeed, other programs) may use them.
C++ has a neat solution: RAII (Resource Acquisition Is Initialization). The weird name aside, it means that you allocate the instance that holds the resource on the stack, so that the destructor is called immediately, when you exit the function in which it was allocated. That way you only have to care about the resource management in one place (at construction time).
In Droid there is a similar construct, although it does not have anything to do with stack allocation. It is called a scoped resource :
fun printLines(fileName: String) do val file = scope File.readText(fileName) for line in file.Lines do Console.writeLine(line) end end
The above example defines a function printLines
that prints all the lines of a text file. Experienced coders will notice that it doesn't explicitly close the file. Don't be alarmed - the scope
keyword where the file is opened ensures that the file is closed immediately when the function returns. If an exception is thrown that isn't catched inside the function, it will also be closed immediately.
In other words, when you allocate a sparse resource, you can just scope
it and forget about further management of it. Just make sure the scope that you're currently in corresponds roughly to it's optimal lifetime. Sometimes that lifetime is smaller than the full extend of the enclosing scope. You can define a smaller scope with the scope do ... end
construct:
scope do scope myMutex.Lock # modify or read data protected by the mutex end
In fact, all block constructs have their own scope. That includes if
statements, while
loops and so on. As a rule of thump, every time you indent you start a scope, and every time you unindent you end a scope.
All objects that can act as scoped resources implement the Disposable
interface. It has one method dispose
that should dispose of the sparse resource. Calling that method after disposing it should have no effect (it will sometimes be called twice by the runtime system).
If you don't scope the resource, it will still be automatically disposed when the object is garbage collected. And in between, you can simply call dispose
yourself.
Exceptions are used to signal that an unusual thing has happened, such as if you try to open a file that does not exist. In such cases we throw an exception, which unwinds all calls you have made until it finds a catch block that can handle it. If no catch blocks are found, the entire call stack is unwinded and when we reach the top level, the program (or thread, or process) stops and the exception is shown to the user.
fun foo() do val r = file.readText("some-file-that-doesn't-exist.txt") catch FileNotFoundException do Console.writeLine("Whoops, it looks like the file does not exist!") end end
The above function prints the "Whoops, ..." message when called. What happens is that the call to readText
calls some other function to open the file, which throws a FileNotFoundException
since the file does not exist. It then propagates upwards until it lands in the foo
function, where there is a catch block that matches this specific exception. The handler for this exception is then invoked, which outputs the "Whoops, ..." message.
Any block that can contain code can have a catch
block after it, like above. Compared with the usual try...catch
you find in most languages, it saves you from an extra indentation around the main code of the block.
The catch
block above contains a pattern and an action for each of the exceptions it can handle. In that case, it can only handle one kind of exception, but it could just as well have had other exception patterns below it, like one for FileAccessDeniedException
. These are very simple patterns that only mention a class name. But patterns are useful for much more than simple type detection. Consider the switch
statement:
switch myNumber do 0 do "Zero" end 1 do "One" end 2 do "Two" end end
This code converts a number x (between 0 and 2, inclusive) to it's textual name. That is, if x is 0, it returns "Zero", if x is 1, it returns "One", and if x is 2, it returns "Two". 0, 1, and 2 are very simple patterns, each matching the obvious corresponding number. But patterns can be more complex:
switch myList do [] do "This is an empty list." end [e] do "This is a list with a single element: " ~ e ~ "." end [a, b] @ r do "This is a list with multiple elements, " ~ "the first two of which is " ~ a ~ " and " ~ b ~ ", and after it comes " ~ r.Size ~ " elements." end end
The first pattern matches any empty list. The second pattern matches a list with exactly one element, and binds e
to the element's value, so that it can be used inside the action. The third pattern matches any list with at least two elements, and binds the two first elements to a
and b
, and the rest of the list to r
.
There are different kinds of patterns, including ones that match classes, lists, strings, integers and wildcards (e, a, and b above). Patterns can be nested, as shown above where wildcard patterns are nested inside list patterns.
Regular expressions are another kind of pattern matching which is more specific. Namely, they are designed to match sequences inside strings. Here's a good reference. The syntax for them is tilde, slash, regular expression, slash, options, plus various escape sequences, including \/
which is a literal slash.
val re = ~/([0-9]+)/ switch re.match("I've got 75 cents.") do Some(m) do Console.writeLine("The string contained the integer: " ~ m(1)) end None do Console.writeLine("The string contained no integers.") end end
This regular expression matches integers in a string. Or more precisely, it matches one or more characters in the character range from "0" to "9" inclusive. The parenthesis mark that whatever this matches is going to be stored into group 1. The output of this snippet is "The string contained the integer: 75".
Most languages have some convenient way to represent that there is no interesting result, or a value is "missing". It's been called null, nil, none
in different languages. However, they are also the source of one of the most common runtime errors, the NullPointerException.
Since we're no fan of runtime errors, we've removed it from the language and told the compiler to guarantee that it cannot occur instead. Instead of pretending that every reference can be null, you are required to mark a reference if it can refer to "nothing".
val x: Int? = 23
The above says that x
refers either to Some[Int]
or to None.
When assigned directly to an integer like above, it is autoboxed so as to be equivalent to Some(23).
This kind of value is called an Option,
because it may or may not refer to an interesting value. The most secure way to handle both cases is with a switch, as seen in the example for regular expressions, where there is a case for Some
and one for None.
If you know that the value cannot be None,
you can get the value directly with the following syntax:
val y: Int = x() # Returns the integer contained in x, # or throws OptionNoneException if x is None.
Of course if you use the above example instead of a switch, you're not better off than with a null pointer, so we recommend using the switch approach by default. Remember, it's going to be rare since "nullable" references are not needed that often. If you forget to check for None,
the compiler will tell you. For example, y = x
would generate an error stating that you cannot assign values of type "Option[Int]" to "Int".
References that can also refer to "nothing" are much rarer than references that always refer to a valid object. Although the secure approach is slightly more verbose in the rare case, you avoid performing implicit, dynamic casts to a not-nullable reference every time you call a method on a reference (which is what happens in Java and C#... in C++ with pointers it just blows up).
You have already seen how to declare a local function (the fun name(args) do ... end
syntax). You can also create anonymous functions:
button.onClick(fun(event) do Console.writeLine("The button was clicked!") end)
Here we create an anonymous function that writes "This button was clicked!" when called with an event. We pass it to a method that accepts such a function, here button's onClick method, which would call the function each time the button was clicked. Functions remember the variables that was in scope where they were created, which can be used as below:
var timesClicked = 0; button.onClick(fun(event) do timesClicked += 1 Console.writeLine("Click #" ~ timesClicked) end)
If you then click the button three times, the output is
Click #1 Click #2 Click #3
That is, the timesClicked variable is kept alive as long as the function which captures it is alive, even if timesClicked goes out of it's original scope. Multiple functions can capture a variable and share state that way.
You may notice that we didn't provide types for the event argument or function return type. That's because onClick only takes functions that take ClickEvent
and returns Void,
so it's type is already obvious to the compiler and other programmers.
In summary you can pass functions as arguments to other functions, store them in variables and return them, just like any other value in the language. They remember their scope and their type can often be inferred from the expected type.
The arithmetic and comparison operators can be defined for user defined types. These operators are actually just syntactic sugar for method calls. If you implement the methods used in the expansion of an operator, you can use the operator on your types.
If the numbers to the left seem cryptic, you can chose ignore them. They just explain how grouping works, and it's pretty much like in any other ordinary language (and math). You might want to pay attention to the last four lines of this table, since they contain some unusual syntactic sugar.
P.A |
Syntax |
Expansion |
|
10.L |
|
|
|
10.L |
|
|
|
10.L |
|
|
|
10.L |
|
|
|
10.L |
|
|
|
10.L |
|
|
|
11.N |
|
|
|
12.L |
|
|
|
13.L |
|
|
|
13.L |
|
|
|
14.L |
|
|
|
14.L |
|
|
|
15.N |
|
|
|
16.N |
|
|
|
30.L |
|
|
(where |
1.N |
|
|
(where |
31.L |
|
|
(the name |
1.N |
|
|
(the name |
If you have an expression like a + b * c
then it's equivalent to a + (b * c)
because *
has a higher precedence (P) than +.
If you have an expression like a + b + c
then it's equivalent to (a + b) + c
because +
is left-associative. All operators are either left-associative (L) or non-associative (N).
Please note that the comparison operators work like in math when associativity is invoked, ie. a < b < c == d
is almost equivalent to a < b and b < c and c == d.
The difference is that each operand will only be evaluated once.
These are (some of the) operators you can't redefine.
P.A |
Syntax |
Explanation |
1.N |
|
Assigns b to a. Returns Void. |
1.N |
|
Where |
1.N |
|
Expands to |
2.L |
|
Expands to |
3.L |
|
Evaluates |
4.L |
|
Evaluates |
5.N |
|
Evaluates |
20.N |
|
Makes sure that |
30.L |
|
Calls the |
31.L |
|
Gets the functional value of the method |
Any other operators in the language have lower precedence, and have no associativity (since their syntax isn't infix).
The following constructs must appear at the top level of a file, that is, outside any do ... end
pair.
Enumarations are used to implement some of the core features of the language. For example, booleans are implemented as:
enum Bool do True False end
This says that any value of type Bool
is either True
or False.
But enumerations can also have parameters. This kind of construct is often called a variant:
enum Option do Some(value: Int) None end
This means that any value of type Option
is either None
or Some(value),
where value
is any value of type Int.
We saw earlier that you can use switch
to extract value.
The Option
we have defined so far only support Int
values. If we needed support for String
and Bool
we could provide separate option types for these, but that's too much work. Instead, we can make Option
generic:
enum Option[T] do Some(value: T) None end
This says that for any type T,
there exists an Option
whose Some
constructor takes a value of type T.
In other words, if I write Option[String],
I get an Option
where the T
type is replaced with String.
Earlier we saw that we could "promise" the compiler that a value wasn't None
by using the syntax x().
Enums can have methods:
enum Option[T] do Some(value: T) None methods fun get() do switch this do Some(v) do v end None do throw OptionNoneException() end end end end
That is, when x.get()
is called, we switch on x
to extract the value, or throw an exception if it's None.
The syntax x()
expands to x.get(),
which means we have obtained the syntax we saw earlier.
The constructors are accessed like Option_Some
and Option_None.
If the expected type is already Option,
you can omit the prefix and write Some
or None.
For the built in types, you never need a prefix though, since they're aliased to true, false
and none,
and T
is autoboxed to Some[T]
when the expected type is Option[T].
Functions, classes, enums and interfaces can be pure. For functions, this is enforced by using the keyword pure
instead of fun,
and for the rest, pure
must be put in front of the normal keyword. Examples:
pure class Foo(x: Int) do var mutableX = x; methods pure getX(): Int do x end # fun setX(value: Int): Void do mutableX = value end end
The commented code violates multiple rules. Code inside pure functions or pure constructors cannot:
Use variables declared with var
from the parent scope(s).
Call impure functions (this implies that you cannot construct instances of impure classes).
And pure classes, enums and interfaces can only contain pure functions. However, pure code can:
Use variables declared with var
locally.
Call pure methods on impure values.
Declare and return impure functions.
This allows a subset of Droid programs to be purely functional. Such functions are easier to reason about because side effects are removed from the equation, so all inputs and outputs can be seen just by looking at the function in isolation.
Every type has a pure version, which only contains it's pure methods (of which there may be none). The pure version behaves as a subtype of the impure version (if the original version is pure, the types are equal).
Enums, classes, interfaces, generics, packages and importing, and operators. I have it all in my head, and most of it can be copied from older documents. Generics are the hardest part, since they are the most advanced typing scheme in the language (with constraints and variance, using a MSR proposal for C#).
Multitasking won't get but a brief mention. A shared-all model (often called 'threads') really messes with the semantics of the language, and is hard to use. We use a lightweight shared-nothing model (often called 'processes') with message passing (possibly with built-in support for distribution). When creating a new process from a function, all state it captures is copied (and unserializable captured state will cause an exception). The same happens when sending a function. The language will support serialization and matching. The rest will be in a library.
Note: when there is no overloading, implementing the same interface twice but with different generic parameters will cause an error. Is this acceptable?