Field | Value |
---|---|
DIP: | 1xxx |
Review Count: | 0 |
Author: | Biotronic |
This document describes a method for implicitly converting const data structures to head-mutable when types are inferred for temporary variables. This already happens for built-in pointers and arrays, but user-defined types do not enjoy the same benefit. This document argues for a solution where the existing alias this
feature is pressed into service to create temporaries with the desired head-mutability when the aliased value has the right type.
This feature is of particular interest for implementers of ranges, but will be of benefit when implementing smart pointers, algebraic data types, and many other types.
Description of library-only implementation
The use of const
is extolled in D. Its use is encouraged on member functions, on method parameters, and on 'variables' - invariable though they may be. However, since D's const
is transitive, this does lead to some problems. The most obvious may be the fact that you cannot iterate over a const
range - popFront()
needs to mutate the range in order to iterate, so it's a no-go from the start. For arrays, this is fixed by having const(T[])
automatically decay to const(T)[]
whenever a temporary is required. For user-defined types, this conversion is not so straightforward - what is the head-mutable version of const(Foo!(int, string[], "a < b"))
? We don't know, of course, so there must be a way to tell the compiler what exactly it should be.
No grammar will change as an effect of this DIP. The only difference is the semantics of a type with alias this
when it is passed as an argument to a function.
We propose that when a value of a source type defining alias this
is passed as an argument to a function, and the type is marked as const
, immutable
, or inout
, the compiler examine the type that alias this
would return. If that type has the same head-mutable layout as the source type, the return value of alias this
is used in the original value's place.
We need to define 'head-mutable layout'. For simple types containing no pointers (int
, char
, float
, and structs that don't contain pointers, dynamic arrays, associative arrays, or class references), this is simply their type without const
, immutable
, or inout
.
This will change the inferred type of arguments, which might lead to breakage where the exact type of an argument is expected.
Essentially the simplest type to meaningfully implement the behavior described here is something like this:
struct S(T) {
T[] payload;
auto headMutable(this This)() inout {
import std.traits : CopyTypeQualifiers;
return S!(CopyTypeQualifiers!(This, T))(arr);
}
alias headMutable this;
}
auto decay(T)(T value) { return value; }
unittest {
const S!int a = S!int([1,2,3]);
// Assigning to an 'auto' variable does not change the type:
auto b = a;
assert(is(typeof(a) == typeof(b)));
// But when it is passed to a function, one layer of const is stripped:
auto c = decay(a);
assert(is(typeof(c) == S!(const(int))));
}
If the types in the above example are replaced with int[]
and const(int)[]
, the asserts will pass in current D.
Given the above type, this code:
unittest {
const S!int a = S!int([1,2,3]);
decay(a);
}
is lowered to this code:
unittest {
const S!int a = S!int([1,2,3]);
S!(const int) __tmp = a; // alias this handles this conversion.
decay(__tmp);
}
A more complete example showing how it will work in practice:
import std.range;
import std.array;
auto map(alias Fn, R)(R range) if (isInputRange!R && is(typeof(Fn(range.front)))) {
return MapResult!(Fn, R)(range);
}
struct MapResult(alias Fn, R) {
R range;
this(R _range) {
range = _range;
}
void popFront() {
range.popFront();
}
@property
auto ref front() {
return Fn(range.front);
}
@property
bool empty() {
return range.empty;
}
static if (isBidirectionalRange!R) {
@property
auto ref back() {
return Fn(range.back);
}
void popBack() {
range.popBack();
}
}
static if (hasLength!R) {
@property
auto length() {
return range.length;
}
alias opDollar = length;
}
static if (isRandomAccessRange!R) {
auto ref opIndex(size_t idx) {
return Fn(range[idx]);
}
}
static if (isForwardRange!R) {
@property
auto save() {
return MapResult(range.save);
}
}
static if (hasSlicing!R) {
auto opSlice(size_t from, size_t to) {
return MapResult(range[from..to]);
}
}
// This is all that is new.
auto headMutable(this This)() const {
return map!Fn(range);
}
alias headMutable this;
}
unittest {
const arr = [1,2,3];
const squares = arr.map!(a => a*a);
const squaresPlusTwo = squares.map!(a => a+2);
assert(equal(squaresPlusTwo, [3, 6, 11]));
}
Copyright (c) 2018 by the D Language Foundation
Licensed under Creative Commons Zero 1.0
Will contain comments / requests from language authors once review is complete, filled out by the DIP manager - can be both inline and linking to external document.