Friday, December 28, 2012

Dart Constant Constructors

‹prev | My Chain | next›

One of the many aspects of Dart that I do not cover in Dart for Hipsters is constant constructors. I briefly played with them a while back, but never really understood them. I will give them another try tonight.

A "regular" Dart cookie class (the yummy kind, not the browser obnoxious kind) might look and get used something like the following:
class Cookie {
  var number_of_chips;
  Cookie({this.number_of_chips});
}

main() {
  var cookie = new Cookie(number_of_chips: 12);

  print("An ordinary cookie has ${cookie.number_of_chips} chips");
}
Running this code would result in the following output:
An ordinary cookie has 12 chips
Constant constructors, as the name implies, involve instance variables that cannot be changed. In Dart, this is generally what the final keyword does—it creates variables (instance or otherwise) that cannot be updated.

Bearing that in mind, I create a PerfectCookie class, which is almost identical to my "regular" Cookie class, except that the number of chips is declared as a final value of 42:
class PerfectCookie {
  final number_of_chips = 42;
  const PerfectCookie();
}
Updating the main() entry point to use a PerfectCookie as well, I have:
main() {
  var cookie = new Cookie(number_of_chips: 12);
  var perfect_cookie = new PerfectCookie();

  print("An ordinary cookie has ${cookie.number_of_chips} chips");
  print("The perfect cookie has ${perfect_cookie.number_of_chips} chips");
}
The resulting output, as expected, now claims that 42 is the ideal number of chips in a cookie:
An ordinary cookie has 12 chips
The perfect cookie has 42 chips
Running this code through dart_analyzer generates no comments, so this seems a legitimate use-case for constant constructors.

Based on the description in the spec, I can also declare number_of_chips as final, but without assigning an actual value:
class PerfectCookie {
  final number_of_chips;
  const PerfectCookie({this.number_of_chips});
}
I can then use this class identically to the "regular" Cookie class:
main() {
  var cookie = new Cookie(number_of_chips: 12);
  var perfect_cookie = new PerfectCookie(number_of_chips: 42);

  print("An ordinary cookie has ${cookie.number_of_chips} chips");
  print("The perfect cookie has ${perfect_cookie.number_of_chips} chips");
}
I still get the same output:
An ordinary cookie has 12 chips
The perfect cookie has 42 chips
Most interestingly, the code still passes dart_analyzer. I had really expected dart_analyzer to complain that I was assigning a non-final value to number_of_chips in my constant constructor, but this seems fine.

What does not work (and is prohibited per the spec) is updating the number of chips in the object generated by the constant constructor:
main() {
  var cookie = new Cookie(number_of_chips: 12);
  var perfect_cookie = new PerfectCookie(number_of_chips: 42);

  cookie.number_of_chips = 13;
  perfect_cookie.number_of_chips = 43;

  print("An ordinary cookie has ${cookie.number_of_chips} chips");
  print("The perfect cookie has ${perfect_cookie.number_of_chips} chips");
}
I do not need dart_analyzer for this code. It produced a compile-time error:
Unhandled exception:
NoSuchMethodError : method not found: 'number_of_chips='
Receiver: Instance of 'PerfectCookie'
Arguments: [43]
#0      Object._noSuchMethod (dart:core-patch:1354:3)
#1      Object.noSuchMethod (dart:core-patch:1355:25)
#2      main (file:///Code/classes/constant_class.dart:16:18)
In this fashion, constant constructors seem a cheap way of creating objects with getters and no corresponding setter. It feels a little strange declaring this in the constructor rather than at the class name level. Is it possible to define a class with a constant constructor and non-constant constructor? And if so, to what end? Those minor worries aside, I can definitely see using these as some point—even if not often.


Day #613

2 comments:

  1. This is a fine explanation of final fields, but it doesn't really explain const constructors.
    Nothing in these examples actually use that the constructors are const constructors. Any class can have final fields, const constructors or not.

    A field in Dart is really an anonymous storage location combined with an automatically created getter and setter that reads and updates the storage, and it can also be initialized in a constructor's initializer list.

    A final field is the same, just without the setter, so the only way to set its value is in the constructor initializer list, and there is no way to change the value after that - hence the "final".

    The point of const constructors is not to initialize final fields, any generative constructor can do that. The point is to create compile-time constant values: Objects where the all field values are known already at compile time, without executing any statements.

    That puts some restrictions on the class and constructor. A const constructor can't have a body (no statements executed!) and its class must not have any non-final fields (the value we "know" at compile time must not be able to change later). The initializer list must also only initialze fields to other compile-time constants, so the right-hand sides are limited to "compile-time constant expressions"[1]. And it must be prefixed with "const" - otherwise you just get a normal constructor that happens to satisfy those requirements. That is perfectly fine, it's just not a const constructor.

    In order to use a const constructor to actually create a compile-time constant object, you then replace "new" with "const" in a "new"-expression.
    You can still use "new" with a const-constructor, and it will still create an object, but it will just be a normal new object, not a compile-time constant value. That is: A const constructor can also be used as a normal constructor to create objects at runtime, as well as creating compile-time constant objects at compilation time.

    So, as an example:
    class Point {
    static final Point ORIGIN = const Point(0, 0);
    final int x;
    final int y;
    const Point(this.x, this.y);
    Point.clone(Point other): x = other.x, y = other.y; //[2]
    }

    main() {
    // Assign compile-time constant to p0.
    Point p0 = Point.ORIGIN;
    // Create new point using const constructor.
    Point p1 = new Point(0, 0);
    // Create new point using non-const constructor.
    Point p2 = new Point.clone(p0);
    // Assign (the same) compile-time constant to p3.
    Point p3 = const Point(0, 0);
    print(identical(p0, p1)); // false
    print(identical(p0, p2)); // false
    print(identical(p0, p3)); // true!
    }

    Compile-time constants are canonicalized. That means the no matter how many times you write "const Point(0,0)", you only create one object. That may be useful - but not as much as it would seem, since you can just make a const variable to hold the value and use the variable instead.

    So, what are compile-time constants good for anyway?

    * They are useful for enums.
    * You can use compile-time constant values in switch cases.
    * They are used as annotations.

    Compile-time constants used to be more important before Dart switched to lazily initializing variables. Before that, you could only declare an initialized global variable like "var x = foo;" if "foo" was a compile-time constant. Without that requrirement, most programs can be written without using any const objects

    So, short summary: Const constructors are just for creating compile-time constant values.

    /L
    [1] Or really: "Potentially compile-time constant expressions" because it may also refer to the constructor parameters.
    [2] So yes, a class can have both const and non-const constructors at the same time.

    ReplyDelete
    Replies
    1. Wow. Thanks for that excellent explanation. That really does a great job of clearing things up in my mind — much appreciated!

      Delete