Delight

Aug 2009, Thomas Leonard

Delight Syntax

Delight drops the punctuation-heavy style that D inherits from C. Instead, its syntax is based on Python's.

  • A colon (:) introduces a new, indented, block.
  • Statements end at the end of a line. Semi-colons are not required.
  • Parenthesis are not needed with if, while statements, etc.
  • Comments start with # and continue to the end of the line.

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: ([{}])

Tabs vs Spaces

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:

  • it looks ugly and distracting,
  • depending on how wide you set your tab stop, adjacent lines may appear to be at different indentation levels,
  • text editors often convert lines to their configured format during editing, leading to unnecessary edits appearing in the version control system, and possibly merge conflicts.

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:

  • No need to reformat code when moving between files or projects.
  • No edit wars in version control (Monday: Alice's text editor converts 10,000 lines of code to Tabs. Tuesday: Bob's text editor converts them back again, etc).
  • No need to reconfigure your text editor when moving between projects. Tell your editor to enable Tabs for all .dlt files.

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 ;-)

Quick Reference

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"

Imports

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()

Loops

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.

Expressions

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

If Statements

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.

Functions and Methods

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.

Classes and Interfaces

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

Getters and Setters

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)

Exceptions

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)

Assertions and Unit-tests

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

Protection Levels

There are four protection levels available:

public
no restrictions
private
only accessible by code in the same source file (doesn't have to be in the same class)
protected
like private, but also accessible to sub-classes
package
only accessible by code in the same package (source directory)

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

Further Reading

Since Delight is based on D, most of the D syntax rules either apply directly, or have an obvious translation into Delight's syntax.

Next Step

Try the Types page for more syntax...