Overview | Comparisons | Discuss |
Examples | GTK | Injection |
Install | Library | Logging |
Nulls | Plans | Syntax |
Types |
Delight drops the punctuation-heavy style that D inherits from C. Instead, its syntax is based on Python's.
For example, this function takes a number and converts it to a string:
# Converts a number to a string
string formatNumber(int x):
if x == 1:
return "One"
else if x == 2:
return "Two"
else:
assert x > 2
return "Many"
Indentation is ignored inside all kinds of brackets: ([{}])
Some people prefer to indent with tabs, others with spaces (some use 2 spaces, some 4, some 8). Though it doesn't really matter which style is used, it is a problem if they are mixed:
Any programmer working in a large team, or on multiple projects, will have to deal with a mixture of different styles. Each time they edit a file, they need to check that project's rules and reconfigure their text editor.
Delight therefore enforces a single style: one Tab character per indent level. Anything else and the compiler will reject it. This means:
See Tab characters considered harmful for more reasons why mixing tabs and spaces is bad (the author is arguing for spaces, but the main point is consistency). There's not much to choose between either: using spaces means different programmers can't use different indent sizes, while using tabs apparently causes some problems for Emacs users trying to line up function arguments. Since the second is a software problem, and therefore fixable, I went with tabs.
Note: I have no interest in discussing the merits of different indentation systems. It's obvious that there are many large and successful projects using all styles and it makes no real difference which is used. If turning off your editor's expandtabs option pushes you too far out of your comfort zone, you probably won't like the rest of the language either ;-)
A source file starts with a module line. The module example.foo should be in a file named example/foo.dlt. If the file isn't in a sub-directory then the module line can be omitted.
Each file can contain imports, typedefs, aliases and test-cases, and declare classes, constants, functions and templates at the top-level:
module example.foo
# Import symbols from other modules
import dlt.io: Printer
# "number" and "int" can be used interchangeably
alias int number
# "handle" is a new opaque type, which happens
# to be stored as an "int"
typedef int handle
# Define a class
class Main:
# An argument that must be passed
# when constructing this Class
in Printer stdout
void main():
stdout.write(addNewline(greeting))
# Constants can go at the top level (but not variables)
const string greeting = "Hello"
# A top-level function
string addNewline(string str):
return str ~ "\n"
# A template (generic) function
# (T is a type chosen by the caller)
T notnull(T)(T? maybe):
if T obj = maybe:
return obj
else:
throw new NullPointerException()
# An enumeration type
enum Colour: Red, Green, Blue
# Tests for this module
unittest:
assert addNewline("a") == "a\n"
There are various ways to import symbols from another module.
# Import a module, accessed by its full name.
import dlt.io
void sayHi(dlt.io.Printer p): ...
# Import just "Time" and "Date" from dlt.time
# Make them publicly available to people who
# import us, too
public import dlt.time: Clock, Date
Date read(Clock clock): ...
# Import the math module, but call it "m"
import m = dlt.math
m.sin(0)
# Import "bar" and "baz" from example.foo,
# but call them "a" and "b"
import example.foo: a = bar, b = baz
a()
# Import all symbols into our namespace
import example.utils: *
trim()
There are various different constructs for loops:
# Iterate over a sequence
for x in ["red", "green", "blue"]:
stdout.formatln("Next is {}", x)
# Iterate over a sequence backwards
for x in ["red", "green", "blue"] reversed:
stdout.formatln("Next backwards is {}", x)
# Get the index as well as the value
for i, x in ["red", "green", "blue"]:
stdout.formatln("Item #{} = {}", i, x)
# Get all key/value pairs from an associative array
for key, value in dictionary:
stdout.formatln("{} -> {}", key, value)
# A C-style for loop
for (int i = 1; i < 10; i++):
stdout.formatln("i = {}", i)
# A while loop
int j = 10
while j > 0:
stdout.formatln("j = {}", j)
j--
The loop variables (x, i, key, value and j) are only defined within the loop.
Generator functions also work (as in D). The sequence object used in the for statement must be an object with this method, or a function with the same signature:
int opApply(int delegate(ref TypeA, ...) dg):
To yield a value, call dg with the value to provide to the loop. It returns non-zero if the loop does a break; you should stop and return the value in that case. If the delegate has multiple arguments, you can assign to multiple loop variables at once.
These mainly work as in C, C++ and D, except for the following changes to Python syntax:
# Short-circuit operators are "and" and "or", not && and ||
# ! is written "not"
if testsEnabled and not testsPass():
throw new Exception("Tests failed!")
# Python-style ternary if
return "Pass" if score >= 70 else "Fail"
assert foo is not null
These are similar to Python, except that elif is written else if. As in D, the result of the condition can be assigned to a variable, but only if the variable is declared in the statement:
if score < 40:
return "Fail"
else if score < 70:
return "OK"
else:
return "Good"
# Compile-time error (no declaration)
if score = 100:
...
# OK, not an assignment
if score == 100:
...
# OK, includes declaration
if Details details = lookupDetails(name):
return details.toString()
This last form is useful for dealing with maybe types. See NullPointerException.
These work as in D. Parameters can be in, out, or ref. Variadic functions have several forms, but the simplest constructs an object of the given type by passing the function arguments to the type's constructor:
int sum(int[] values...):
int total = 0
for x in values:
total += x
return total
unittest:
assert sum(1, 2, 3) == 6
As in Python, anonymous functions are expressions, not statements, with an implied return. e.g.
int[] result = map(delegate(int x): x + 1, [1, 2, 3])
assert result == [2, 3, 4]
The return type is inferred automatically.
Anonymous functions can access variables in their containing function, even after the function has returned.
An interface describes a set of methods. A class that implements an interface must provide implementations of all of those methods.
An interface can extend other interfaces. The extended interface is the union of all the methods from all the interfaces.
A class can extend another class to inherit its implementation.
interface Reader:
string read(int nBytes)
interface TextReader extends Reader:
string[] readLines(int nLines)
class FileReader implements Reader:
string read(int nBytes):
...
class FileTextReader extends FileReader implements TextReader:
string[] readLines(int nLines):
...
A class can have any number of constructors, which are defined using the keyword this. A constructor can use super to call its parent class's constructor (if it doesn't contain a call to super, then one is added to the start automatically).
To replace the definition of a function in a sub-class, the new method must be marked as override. It can call the original method using super.method(...) but no call is added automatically:
class Base:
this():
log_warning("Base class constructor")
void foo():
log_warning("Base foo")
class Subclass extends Base:
this():
super()
log_warning("Subclass constructor")
override void foo():
super.foo()
log_warning("Subclass foo")
class Main:
void main():
auto sub = new Subclass()
sub.foo()
This prints:
2008-09-22 10:54:29,391 Warn hi.Base - Base class constructor 2008-09-22 10:54:29,391 Warn hi.Subclass - Subclass constructor 2008-09-22 10:54:29,391 Warn hi.Base - Base foo 2008-09-22 10:54:29,391 Warn hi.Subclass - Subclass foo
A method which takes a single argument and returns void can be used as a setter. A method which takes no arguments and returns something can be used as a getter.
class Interval:
int days
void weeks(int w):
days = w * 7
int weeks():
return days / 7
These can either be called in the normal way, or using the property syntax:
class Main:
void main(Printer stdout):
auto i = new Interval()
i.weeks = 3
stdout.formatln("{} weeks is {} days", i.weeks, i.days)
If an exception is thrown in the try block then control jumps to the first matching catch block, if any. Whether an exception is thrown or not, the finally block is always executed.
class Main:
in FileSystem fs
in Printer stdout
void main():
string motd
try:
motd = fs.path("/etc/motd").loadContents()
catch FileException ex:
motd = "Can't read message of the day: " ~ str(ex)
finally:
stdout.formatln("Finally!")
stdout.formatln("Got:\n{}", motd)
When compiling a module with -funittest, any unittest block is executed. Use this to check that your module works correctly.
The assert statement evaluates an expression and checks that it is true. If not, it throws an exception. An else part can be added to give a more useful error message:
unittest:
auto i = new Interval()
i.weeks = 3
assert i.weeks == 3
assert i.days == 21 else "Conversation factor is wrong!"
As in D, a class may contain an invariant function. This is called after the constructor and before and after any public method is called.
class Lives:
int lives = 3
void die():
lives--
invariant():
assert lives >= 0
There are four protection levels available:
By default, everything is public (no restrictions) except for imports, which are private.
Example:
class Foo:
private int a
protected int b
package int c
public int d
Since Delight is based on D, most of the D syntax rules either apply directly, or have an obvious translation into Delight's syntax.
Try the Types page for more syntax...
Overview | Comparisons | Discuss |
Examples | GTK | Injection |
Install | Library | Logging |
Nulls | Plans | Syntax |
Types |