Implementing the value object pattern in D
Asked Answered
L

1

6

I want to implement the value object pattern in D. That is, I want to have mutable reference variables to immutable objects. T variables should be assignable, but T objects should never change their state.

I am confused about the difference between const and immutable in D. Let me illustrate my doubts with a skeleton Rational class:

class Rational
{
    int num;
    int den;

Should I declare num and den as const or immutable? Is there a difference for integers?

    invariant()
    {
        assert(den > 0);
        assert(gcd(abs(num), den) == 1);
    }

Should I declare invariant as const or immutable? Marking it as immutable results in a compile-time error, but that may be due to other members not being marked immutable.

    this(int numerator, int denominator) { ... }

Should I declare the constructor as const or immutable? What would that mean?

    string toString()
    {
        return std.string.format("(%s / %s)", num, den);
    }
}

Should I declare toString as const or immutable?

Instead of marking individual members, it seems I can also mark the entire class:

class Rational
const class Rational
immutable class Rational

Which of these make the most sense for the value object pattern?

What about pure? In the value object pattern, the methods should be free of side effects, so does it make sense to declare every member as pure? Marking toString as pure does not compile, unfortunately, because std.string.format is not pure; is there any particular reason for that?

It seems I can also declare the class itself as pure, but that does not seem to have any effect, because the compiler does not complain about toString calling an impure function anymore.

What does it mean to declare a class as pure then? Is it simply ignored?

Legged answered 8/8, 2012 at 10:29 Comment(2)
As already stated below by jA_cOp, ValueObject pattern emerged from Java world because the Java language only deals with reference types... All you need in D is a struct.Overlooker
And as I already mentioned, structs don't work if you want to share representation between variables and value objects.Legged
O
15

The D Struct

The value object pattern is best represented in D by simply using a struct and its in-built value semantics.

To my understanding, the value object pattern is usually employed in Java due to Java's current lack of in-built aggregates with value semantics.

D's structs work similarly to structs in C and C#, as well as structs and classes in C++. The comparison is perhaps best for the latter, as D structs have constructors and destructors, but with one important exception: there's no inheritance and virtual functions; those features are delegated to classes, which work much like classes in Java and C# (they are implicit reference types, hence they never exhibit the slicing problem).

struct Rational
{
    int num;
    int den;

    /* your methods here */
}

Instances of Rational are then always passed by value (unless the parameter explicitly specifies otherwise, see ref and out) to functions and copied on assignment.

Purity

Pure functions cannot read or write to any global state. Pure functions are allowed to mutate explicit parameters as well as the implicit this parameter for methods; methods on Rational are thus probably always pure.

std.string.format not being pure is a problem with its current implementation. It will use a different implementation in the future that is pure.

Const and Immutable

If you want to express that the method is pure and also doesn't mutate its own state, you can make it both pure and const.

Both mutable (Rational) and immutable (immutable(Rational)) instances can be implicitly converted to const(Rational), hence const is the best choice when you don't need the immutable guarantee but you still don't mutate any members.

In general, struct methods that don't need to mutate member fields should be const. For classes, the same applies but you also have to think about any derived methods that may override the method - they are bound by the same restriction.

Putting const or immutable on a struct or class declaration is equivalent of marking all its members (including methods) const or immutable respectively.

Immutable Constructors

If all your constructor does is assign the num and den fields to their respective constructor parameters, then this functionality is already present on structs by default:

struct S { int foo, bar; }

auto s = S(1, 2);
assert(s.foo == 1);
assert(s.bar == 2);

const on a constructor doesn't make a lot of sense because any constructor regardless of constancy can construct a const instance since everything is implicitly convertible to const.

immutable on a constructor does make sense and is sometimes the only way to construct an immutable instance of a struct or class. A mutable constructor could create aliases for the this reference through which the instance could later be mutated, so its result cannot always be implicitly converted to immutable.

However, an immutable constructor is not needed in your case because Rational does not have any indirection, so a mutable constructor can be used and the result copied over. In other words, types with no mutable indirection are implicitly convertible to immutable. This includes primitive types like int and float as well as structs satisfying the same condition.

Attributes with no Effect

Attributes put on declarations where they don't have any effect are ignored by all current compilers. This can make sense, because attributes can be applied to multiple declarations at once, with the attribute { /* declarations */ } and attribute: /*declarations*/ syntaxes:

struct S
{
    immutable
    {
        int foo;
        int bar;
    }
}

struct S2
{
    immutable:
    int foo;
    int bar;
}

In both of the above examples, foo and bar are of type immutable(int).

Using a Class

Sometimes value semantics are not desired, such as for performance reasons associated with frequent copying of large structs. It's possible to explicitly pass structs by reference, such as using ref and out function parameters or by using pointers, but when value semantics are the default it's easy to make mistakes, and the syntactic overhead can be grinding. Pointers also have a number of other pitfalls.

Classes are reference types and it's impossible to treat them like values. They are typically instantiated with new, which always creates a GC-allocated instance of the class (overloading of new is deprecated). These two points make classes in D very similar to classes in Java and C# (another notable point is that there are interfaces instead of multiple inheritance). However, classes have the overhead of hidden fields (currently size_t.sizeof * 2 bytes for all classes) and the ABI of fields is not specified, but classes are also the only option when inheritance and virtual functions are desired.

Here's Rational implemented for the Value Object Pattern:

class Rational
{
    immutable int num;
    immutable int den;

    this(int num, int den)
    {
        this.num = num;
        this.den = den;
    }

    /* methods here */
}

This is the implementation most faithful to Java implementations. It uses immutable to prevent mutation of num and den regardless of the mutability of the instance itself. Methods should be const and typically pure as with the struct.

Since immutable constructors are not currently fully implemented (read: don't use them at all), the above constructor will actually allow you to create immutable instances of the class (e.g. new immutable(Rational)(1, 2)) even though the constructor is free to make mutable aliases of the this reference, breaking the immutable guarantee.

A slightly more D-like way would be to leave immutability decisions to user code, implementing it plainly like this:

class Rational
{
    int num;
    int den;

    this(int num, int den)
    {
        this.num = num;
        this.den = den;
    }

    /* immutable constructor overload would be here */

    /* methods here */
}

The user can then choose whether to use Rational or immutable(Rational). The latter can be safely passed between threads using the std.concurrency threading interface, while trying to send the former would be rejected at compile-time.

However, the latter has a glaring problem - because Rational is implicitly a reference type, there's no way to type a mutable reference to an immutable instance of Rational. The current solution to this problem is to use std.typecons.Rebindable. There is a proposed solution for fixing this in the language.

Overmeasure answered 8/8, 2012 at 11:22 Comment(4)
Structs work great for simple types like Rational. But what if I really want to share immutable objects between several variables, or share state between several objects like class SinglyLinkedList { SinglyLinkedList tail; /* ... */ }?Legged
@FredOverflow, make a class with immutable fields and assign them in an immutable constructor. Methods on it should be const and typically pure. You can also make an immutable instance of a struct and pass around mutable pointers to it (e.g. immutable(Rational)* - the pointer tail is mutable/reassingable). Do you think I should add a section on using a class, similar to how it'd be done in Java?Overmeasure
Yes, I definitely want to see the Java way translated to D :)Legged
@FredOverflow, added a section about using a class for this and its advantages and disadvantages.Overmeasure

© 2022 - 2024 — McMap. All rights reserved.