Book Image

D Cookbook

By : Adam Ruppe
Book Image

D Cookbook

By: Adam Ruppe

Overview of this book

Table of Contents (21 chapters)
D Cookbook
Credits
Foreword
About the Author
About the Reviewers
www.PacktPub.com
Preface
Index

Understanding immutability


Here, you'll look at how to use immutability in your functions and data types. Immutability helps us to write code that is easier to understand and maintain because it limits the places where things can change.

Getting ready

First, write a function. Then, look at it and determine what it needs to do. Does it just look at the data passed to it? Does it store or return a reference to data passed in? We'll use these facts about how the function uses its arguments to determine the best-fit qualifiers.

How to do it…

The use of const and immutable is slightly different on free functions and object methods.

Writing functions

If you are accepting a value type, const and immutable aren't very important.

If you are borrowing a value—going to look at it, but not store it nor modify it—use the in keyword and, if it is a character string, use char[] instead of string (string is an alias for immutable(char)[]):

void foo(in char[] lookAtThis) { /* inspect lookAtThis */ }

If you are going to store a reference, it is best to take immutable data, if possible as follows:

void foo(immutable(ubyte)[] data) { stored = data; }

If you are going to modify the data, but not store it, use scope, but not const (in is shorthand for scope const), as follows:

void foo(scope char[] changeTheContents) { /* change it */ }

If you are not going to modify or store the contents, but will return a reference to it, use inout as follows:

inout(char)[] substring(inout(char)[] haystack, size_t start, size_t end) {
    return haystack[start .. end];
}

If you are going to change the value itself (not just the contents it references), use ref as follows:

void foo(ref string foo) { /* change foo itself */ }

Writing object methods

When writing object methods, all of the preceding functions still apply, in addition to putting a qualifier on the this parameter. The qualifier for this goes either before or after the function as follows:

int foo() const { return this.member; } /* this is const */
const int foo() { return this.member; } /* same as above */

Since the second form can be easily confused with returning a const value (the correct syntax for that is const(int) foo() { …}), the first form is preferred. Put qualifiers on this at the end of the function.

How it works…

D's const qualifiers is different than that of C++ in two key ways: D has immutable qualifiers, which means the data will never change, and D's const and immutable qualifiers are transitive, that is, everything reachable through a const/immutable reference is also const/immutable. There is no escape like the mutable keyword of C++.

These two differences result in a stronger guarantee, which is useful, especially when storing data.

When storing data, you generally want either immutable or mutable data—const usually isn't very useful on a member variable; although it prevents your class from modifying it, it doesn't prevent other functions from modifying it. Immutable means nobody will ever modify it. You can store that with confidence that it won't change unexpectedly. Of course, mutable member data is always useful to hold the object's own private state.

The guarantee that the data will never change is the strength of immutable data. You can get all the benefits of a private copy, knowing that nobody else can change it, without the cost of actually making a copy. The const and immutable qualifiers are most useful on reference types such as pointers, arrays, and classes. They have relatively little benefit on value types such as scalars (int, float, and so on) or structs because these are copied when passed to functions anyway.

When inspecting data, however, you don't need such a strong guarantee. That's where const comes in. The const qualifier means you will not modify the data, without insisting that nobody else can modify it. The in keyword is a shorthand that expands to scope const. The scope parameters aren't fully implemented as of the time of this writing, but it is a useful concept nonetheless. A scope parameter is a parameter where you promise that no reference to it will escape. You'll look at the data, but not store a reference anywhere. When combined with const, you have a perfect combination for input data that you'll look at. Other than that you have the short and convenient in keyword.

When you do return a reference to const data, it is important that the constancy is preserved, and this should be easy. This is where D's inout keyword is used. Consider the standard C function strstr:

char *strstr(const char *haystack, const char *needle);

This function returns a pointer to haystack where it finds needle, or null if needle is not found. The problem with this prototype is that the const character attached to haystack is lost on the return value. It is possible to write to constant data through the pointer returned by strstr, breaking the type system.

In C++, the solution to this is often to duplicate the function, one version that uses const, and one version that does not. D aims to fix the system, keeping the strong constancy guarantee that C loses and avoiding the duplication that C++ requires. The appropriate definition for a strstr style function in D will be as follows:

inout(char)* strstr(inout(char)* haystack, in char* needle);

The inout method is used on the return value, in place of const, and is also attached to one or more parameters, or the this reference. Inside the function, the inout(T) data is the same as const(T) data. In the signature, it serves as a wildcard that changes based on the input data. If you pass a mutable haystack, it will return a mutable pointer. A const haystack returns a const pointer. Also, an immutable haystack will return an immutable pointer. One function, three uses.

D also has the ref function parameters. These give a reference to the variable itself, as shown in the following code:

void foo(int a) { a = 10; }
void bar(ref int a) { a = 10; }
int test = 0;
foo(test);
assert(test == 0);
bar(test);
assert(test == 10);

In this example, the variable test is passed to foo normally. Changes to a inside the function is not seen outside the function.

Note

If a was a pointer, changes to a will not be seen, but changes to *a will be visible. That's why const and immutable are useful there.

With the function bar, on the other hand, it takes the parameter by reference. Here, the changes made to a inside the function are seen at the call site; test becomes 10.

Tip

Some guides recommend passing structs to a function by ref for performance reasons rather than because they want changes to be seen at the call site. Personally, I do not recommend this unless you have profiled your code and have identified the struct copy as a performance problem. Also, you cannot pass a struct literal as ref, because there is no outer variable for it to update. So, ref limits your options too.