Dart at a Glance — A Language a Day, Advent Calendar 2019 Day 8/24

About this ‘A Language a Day’ Advent Calendar 2019

Welcome to Day 8 of this year’s A Language a Day Advent Calendar. Today’s topic is introduction to the Dart programming language.

Facts about the language

Some facts about Dart:

  • Object-oriented
  • Compiled to native code or to JavaScript
  • Developed by Google
  • Appeared in 2011
  • Website: dart.dev

Installing and running Dart

You can try Dart online or install a command-line complier. To run a program from command line, type:

$ dart hello-world.dart

Compiling

You can also compile your Dart program to the binary code or to the JavaScript source. There are two utilities for doing that:

To create an executable file (the default extension is exe even on a non-Windows platform):

$ dart2native hello-world.dart
$ ./hello-world.exe

To compile to JavaScript:

$ dart2js hello-world.dart
$ node out.js

Hello, World!

Here is the program to print the message.

void main() {
    print("Hello, World!");
}

As you can immediately see, you need the main function and semicolons at the end of the statement. The program also works if you omit void.

Variables

Dart has a very concise syntax of allowing you to use both typed and not typed variables. To create just a variable, use the var keyword. But the compiler deduces its type and forces you to be consistent. You cannot assign another value that has a different type. For example, the following program leads to a compile-time error if you uncomment the second assignment:

main() {
    var name = "John";
    // name = 22; // Error: A value of type 'int' 
                  // can't be assigned to a variable of type 'String'.

    print("Hello, $name!");
}

For such variable transformations (no idea why you may need it), use the dynamic keyword:

main() {
    dynamic name = "John";
    name = 32;
    print("Hello, $name!");
}

To specify the type, name it before the variable. I really enjoy the fact that you don’t need a separate keyword for creating a variable in this case:

main() {
    String name = "John";
    int age = 42;
    print("$name is $age."); // John is 42.
}

Strings are kept as a sequence of UTF-16 (not UTF-8) code units. There are two properties that you can use to traverse a string: codeUnits and runes. They both return integer numbers, but if your string contains the characters with codepoints beyond the limit of the UTF-16 representation, you will have two units, and you’d better treat this character as a rune. Compare:

main() {
    String s = "abc\u{1f555}";
    for (var u in s.codeUnits) print(u); // 97 98 99 55357 56661
    for (var r in s.runes) print(r); // 97 98 99 128341
}

Functions

We already have seen the examples of a simple function with no arguments and no return value, but before continuing with functions, let us immediately examine the example of computing the factorial of positive integers.

A Factorial example

Here is the code.

int factorial(int n) => n < 2 ? 1 : n * factorial(n - 1);

main() {
    print(factorial(1));
    print(factorial(2));

    print(factorial(5));
    print(factorial(6));
}

In this example, the factorial function is a one-liner, and in such cases there is a shortened form of defining function bodies with the help of the => arrow. The calculated value is the return value of the function.

For bigger functions, the code takes a more traditional shape:

int factorial(int n) {
    return n < 2 ? 1 : n * factorial(n - 1);
}

Actually, you can omit the types in the function header, but that is not recommended:

factorial(n) {
    return n < 2 ? 1 : n * factorial(n - 1);
}

Optional and named parameters

Surround your optional parameters with a pair of square brackets and check if they are passed by comparing them to null:

String greet([String name]) {
    return name == null ? "Hello, dear!" : "Hello, $name!";
}

main() {
    print(greet());       // Hello, dear!
    print(greet("Alla")); // Hello, Alla!
}

Named parameters are also possible in Dart. The syntax of both definition and usage is shown in the below example:

String greet({String name, String salutation}) {
    print("Dear $salutation $name,");
}

main() {
    greet(salutation: "Mr.", name: "Johnson"); // Dear Mr. Johnson,
    greet(salutation: "Mrs.", name: "Marple"); // Dear Mrs. Marple,
}

Lambdas

The following example demonstrates how you can create a simple anonymous function and define it where it is used:

main() {
    var data = ["alpha", "beta", "gamma"];
    data.forEach(
        (x) => print(x)
    );
}

Classes

If you have experience with other C-like programming languages, you will immediately be able to understand the basics of object-oriented features in Dart. Use the class keyword and define the corresponding constructor (otherwise only the constructor with no parameters will be generated).

Here is the class describing a person with the name and age instance variables and a single method, info(). There are three ways of creating constructors. In the simplest case, you do not need to program its body if you only want to initialise objects’s data. The other two options are shown in the comments:

class Person {
    String name;
    int age;

    /*Person(String _name, int _age) {
        name = _name;
        age = _age;
    }*/

    /*Person(String name, int age) {
        this.name = name;
        this.age = age;
    }*/

    Person(String this.name, int this.age);

    info() {
        return "$name is $age.";
    }
}

main() {
    var p = Person("John", 20);
    print(p.info()); // John is 20.
}

Cascade notaion

In Dart, an interesting way of calling more than one method on the same object is implemented. This somehow resembles the way jQuery programmers stack the calls, but the methods do not need to return the object.

For example, here is a modified Person class that can increment the age:

class Person {
    String name;
    int age;

    Person(String this.name, int this.age);

    inc_age() {
        age++;
    }

    info() {
        print("$name is $age.");
    }
}

Now we want to call the info() method, then inc_age(), and then info() again. To avoid repeating the object name, you can use the following cascade notation:

main() {
    var person = Person("John", 20);
    person
        ..info()    // John is 20.
        ..inc_age()
        ..info();   // John is 21.
}

Factory constructors

In Dart, constructors can be marked factory, in which case they do not always need to create a new object. In the following example, the Person class has a factory constructor that creates only one object per given name. If you are trying to create a new object with the name which is already known, you will get one of the previously created objects instead.

The program flow may be a bit difficult to ad hoc writing. Remember that the factory constructor has no access to this.

class Person {
    String name;
    int id;

    static int count = 0;
    static Map<String, Person> cache = {};

    Person.init(String name) {
        this.name = name;
        this.id = ++count;
    }

    factory Person(String name) {
        if (cache[name] == null)
            cache[name] = Person.init(name);

        return cache[name];
    }

    info() {
        print("$name is #$id.");
    }
}

main() {
    var john1 = Person("John");
    var hella = Person("Alla");
    var john2 = Person("John");
    
    john1.info(); // John is #1.
    hella.info(); // Alla is #2.
    john2.info(); // John is #1.
}

The class counts the objects and keeps track of the objects that were created. They are saved in the static (thus, class-level) variable cache. It is a Map that connects the name to the existing object of the Person type. When the name is seen for the first time, the factory Person constructor creates a new object an puts it to the cache. When the name is mentioned again, an existing cached object is returned. You can see this behaviour by looking at the person’s id value.

A Polymorphic example

Let us implement our standard zoo example, where the cats and the dogs are animal in the object-oriented sense.

abstract class Animal {
    String info();
}

class Cat implements Animal {
    String info() => "Cat";
}

class Dog implements Animal {
    String info() => "Dog";
}

main() {
    var zoo = [Dog(), Dog(), Cat(), Cat()];
    for (var x in zoo) print(x.info());
}

The code looks very concise and clean. Animal is an abstract class, and both Cat and Dog should implement the info() method returning a string.

With such a hierarchy, calling the info() method on the object is correctly dispatched to the method defined in either Cat or Dog.

Notice that if you remove the base class from the example (and the implements traits), the code will not compile. In that case, the classes will be direct descendants of Object, and you will get an error message: The method 'info' isn't defined for the class 'Object'.

Asynchronous programming

In Dart, there are a few methods of organising asynchronous code. If you are interested, you should investigate on the following search words: async and await, Future and Stream (and generators), and isolates (instead of threads).

Let me restrict the topic to the Sleep sort example here.

The Sleep Sort example

Our Dart version of the Sleep Sort algorithm uses futures — the objects of the Future objects.

void main() async {
    var data = [10, 4, 2, 6, 2, 7, 1, 3];

    for (var n in data)
        Future.delayed(
            Duration(seconds: n),
            () => print(n)
        );
}

The Future.delayed method (see the API documentation) is a factory constructor (see the corresponding subsection about classes earlier in this article). The first argument sets a delay proportional to the current number. The second argument is a lambda function that prints the result.

Get more

I found Dart a nice language with a relatively simple syntax (if you are familiar with C++, for example) and good performance. Enjoy the rest of the language by using the resources from its official website:

The source codes are located on GitHub.

Next: Day 9. Hack

Leave a Reply