Skip to content

Instantly share code, notes, and snippets.

@Biotronic
Created March 12, 2018 07:57
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Biotronic/940f553fa1b4cbf80a98afe3d1970c6d to your computer and use it in GitHub Desktop.
Save Biotronic/940f553fa1b4cbf80a98afe3d1970c6d to your computer and use it in GitHub Desktop.
Implicit Head-mutable Conversion for Temporaries

Implicit Head-mutable Conversion for Temporaries

Field Value
DIP: 1xxx
Review Count: 0
Author: Biotronic

Abstract

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.

Links

Description of library-only implementation

Rationale

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.

Description

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.

Breaking changes / deprecation process

This will change the inferred type of arguments, which might lead to breakage where the exact type of an argument is expected.

Examples

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 & License

Copyright (c) 2018 by the D Language Foundation

Licensed under Creative Commons Zero 1.0

Review

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment