Jump to page: 1 2
Thread overview
Implementing tail-const in D
Jan 23, 2018
Simen Kjærås
Jan 23, 2018
Nicholas Wilson
Jan 23, 2018
Simen Kjærås
Jan 23, 2018
Andrea Fontana
Jan 23, 2018
Simen Kjærås
Jan 25, 2018
Simen Kjærås
Jan 23, 2018
sarn
Jan 24, 2018
Jacob Carlborg
Jan 24, 2018
Nick Treleaven
Jan 24, 2018
Simen Kjærås
Jan 25, 2018
H. S. Teoh
Jan 25, 2018
Simen Kjærås
Jan 29, 2018
Simen Kjærås
Mar 10, 2018
sclytrack
January 23, 2018
Since tail-const (more correctly called head-mutable) was mentioned here lately (in the 'I closed a very old bug!'[1] thread), I've been racking my brain to figure out what needs doing to make a viable solution.

Unqual is the standard way today to get a head-mutable version of something. For dynamic arrays, static arrays, pointers and value types, including structs without aliasing, thi works. For AAs, classes, and structs with aliasing, Unqual is the wrong tool, but it's the tool we have, so it's what we use.

Unqual has other uses, so HeadMutable!T should be a separate template. This means parts of Phobos will need to be reworked to support types not currently supported. However, given these types are not currently supported, this should not break any existing code.

While it is generally desirable for T to be implicitly castable to HeadMutable!T (just like const(int[]) is implicitly castable to const(int)[]), the rules for such implicit casting in the language today are inconsistent[2] and incompatible with alias this[3], opDispatch, opDot, subclassing, and constructors.

Instead of implicit casting, I therefore propose we use a method headMutable(), which will attempt to call the appropriate functions to do the conversion. With these two building blocks, we have what we need for tail-const (head-mutable) ranges and other constructs.

What does your code need to do to support HeadMutable? If you have a templated struct that holds an array or pointer, the type of which depends on a template parameter, you can define a function opHeadMutable that returns a head-mutable version. That's it.

If you use HeadMutable!T anywhere, you almost definitely should use headMutable() when assigning to it, since T might not be implicitly castable to HeadMutable!T.

So what does all of this look like? An example templated struct with opHeadMutable hook:

struct R(T) {
    T[] arr;
    auto opHeadMutable(this This)() {
        import std.traits : CopyTypeQualifiers;
        return R!(CopyTypeQualifiers!(This, T))(arr);
    }
}

This is the code you will need to write to ensure your types can be converted to head-mutable. opHeadMutable provides both a method for conversion, and a way for the HeadMutable!T template to extract the correct type.

The actual implementation of HeadMutable!T and headMutable is available here:
https://gist.github.com/Biotronic/67bebfe97f17e73cc610d9bcd119adfb


My current issues with this:
1) I don't like the names much. I called them Decay, decay and opDecay for a while. Name suggestions are welcome.
2) As mentioned above, implicit conversions would be nice, but that'd require an entirely new type of implicit conversion in addition to alias this, opDispatch, opDot and interfaces/base classes. This would require some pretty darn good reasons, and I don't think a call to headMutable() is that much of a problem.

Questions:
Is a DIP required for this? Should I create a PR implementing this for the range types in Phobos? What other types would benefit from this?

I welcome any and all... feck it. Destroy!

--
  Simen

[1]: https://forum.dlang.org/post/egpcfhpediicvkjuklwo@forum.dlang.org
[2]: https://issues.dlang.org/show_bug.cgi?id=18268
[3]: Alias this is too eager, and allows for calling mutating methods on the temporary value it returns. If alias this was used to allow const(int[]) to convert to const(int)[], isInputRange!(const(int[])) would return true.
January 23, 2018
On Tuesday, 23 January 2018 at 09:36:03 UTC, Simen Kjærås wrote:
> Questions: Is a DIP required for this?

A DIP is required for language changes. So yes.
January 23, 2018
On Tuesday, 23 January 2018 at 12:12:42 UTC, Nicholas Wilson wrote:
> On Tuesday, 23 January 2018 at 09:36:03 UTC, Simen Kjærås wrote:
>> Questions: Is a DIP required for this?
>
> A DIP is required for language changes. So yes.

No language changes are proposed - this is all library code.

--
  Simen
January 23, 2018
On Tuesday, 23 January 2018 at 12:39:12 UTC, Simen Kjærås wrote:
> On Tuesday, 23 January 2018 at 12:12:42 UTC, Nicholas Wilson wrote:
>> On Tuesday, 23 January 2018 at 09:36:03 UTC, Simen Kjærås wrote:
>>> Questions: Is a DIP required for this?
>>
>> A DIP is required for language changes. So yes.
>
> No language changes are proposed - this is all library code.
>
> --
>   Simen

It would be useful to have one or more short examples. Just to see what actually change in a common scenario.

Andrea
January 23, 2018
On Tuesday, 23 January 2018 at 14:17:26 UTC, Andrea Fontana wrote:
> On Tuesday, 23 January 2018 at 12:39:12 UTC, Simen Kjærås wrote:
>> On Tuesday, 23 January 2018 at 12:12:42 UTC, Nicholas Wilson wrote:
>>> On Tuesday, 23 January 2018 at 09:36:03 UTC, Simen Kjærås wrote:
>>>> Questions: Is a DIP required for this?
>>>
>>> A DIP is required for language changes. So yes.
>>
>> No language changes are proposed - this is all library code.
>>
>> --
>>   Simen
>
> It would be useful to have one or more short examples. Just to see what actually change in a common scenario.
>
> Andrea

Your wish is my command. For the most part, the changes will require that instead of storing Unqual!Ts, use HeadMutable!Ts, and when assigning to a HeadMutable!T, remember to assign headMutable(rhs).

Here's a somewhat simplistic map function. As you can see, not a whole lot is changed - map passes head-mutable versions of its arguments to MapResult, and MapResult implements opHeadMutable(), otherwise everything is exactly as you'd expect.

import std.range;

auto map(alias fn, R)(R r) if (isInputRange!(HeadMutable!R))
{
    // Pass head-mutable versions to MapResult.
    return MapResult!(fn, HeadMutable!R)(headMutable(r));
}

struct MapResult(alias fn, R) if (isInputRange!R)
{
    R range;

    this(R rng)
    {
        range = rng;
    }

    @property
    auto front()
    {
        return fn(range.front);
    }

    void popFront()
    {
        range.popFront();
    }

    @property
    bool empty()
    {
        return range.empty;
    }

    // The only change to MapResult:
    auto opHeadMutable(this This)()
    {
        import std.traits : CopyTypeQualifiers;
        return MapResult!(fn, HeadMutable!(CopyTypeQualifiers!(This, R)))(range);
    }
}


unittest
{
    import std.algorithm : equal;

    const a = [1,2,3,4].map!(v => v*2);
    assert(!isInputRange!(typeof(a)));

    // Here, std.algorithm.map gives up, since a const MapResult is not
    // an input range, and calling Unqual on it doesn't give a sensible
    // result.
    // HeadMutable makes this work, since the type system now knows how
    // to make a head-mutable version of the type.
    auto b = a.map!(v => v/2);

    assert(equal([1,2,3,4], b));
}

--
  Simen
January 23, 2018
On Tuesday, 23 January 2018 at 09:36:03 UTC, Simen Kjærås wrote:
> Since tail-const (more correctly called head-mutable) was mentioned here lately (in the 'I closed a very old bug!'[1] thread), I've been racking my brain to figure out what needs doing to make a viable solution.

Have you seen Rebindable in Phobos?  I know it's not the same thing as what you're talking about, but it's relevant.
https://dlang.org/library/std/typecons/rebindable.html
January 24, 2018
On 2018-01-24 00:10, sarn wrote:

> Have you seen Rebindable in Phobos?  I know it's not the same thing as what you're talking about, but it's relevant.
> https://dlang.org/library/std/typecons/rebindable.html

I'm pretty sure he has since it's use in the implementation [1] ;)

[1] https://gist.github.com/Biotronic/67bebfe97f17e73cc610d9bcd119adfb#file-headmutable-d-L10

-- 
/Jacob Carlborg
January 24, 2018
On Tuesday, 23 January 2018 at 09:36:03 UTC, Simen Kjærås wrote:
> Unqual is the standard way today to get a head-mutable version of something. For dynamic arrays, static arrays, pointers and value types, including structs without aliasing, thi works. For AAs, classes, and structs with aliasing, Unqual is the wrong tool, but it's the tool we have, so it's what we use.

I made an old PR for a Rebindable that works with const/immutable structs with aliasing:
https://github.com/dlang/phobos/pull/4363

I didn't have time to get it merged though. I didn't think about AAs, but we already have Rebindable for classes, and Rebindable could probably easily support the other types. Then we could swap those uses of Unqual for Rebindable.
January 24, 2018
On Wednesday, 24 January 2018 at 11:21:59 UTC, Nick Treleaven wrote:
> On Tuesday, 23 January 2018 at 09:36:03 UTC, Simen Kjærås wrote:
>> Unqual is the standard way today to get a head-mutable version of something. For dynamic arrays, static arrays, pointers and value types, including structs without aliasing, thi works. For AAs, classes, and structs with aliasing, Unqual is the wrong tool, but it's the tool we have, so it's what we use.
>
> I made an old PR for a Rebindable that works with const/immutable structs with aliasing:
> https://github.com/dlang/phobos/pull/4363

Nice. I guess HeadMutable should use Rebindable in those cases, if you get this merged. It addresses a somewhat orthogonal issue though. We need to be able to call mutating methods on the resulting value, e.g. popFront(), without mutating aliased values. For that to work, the type must be able to specify how to make a head-mutable version of itself. Rebindable cannot offer this kind of access (and shouldn't).

--
  Simen
January 25, 2018
On Tuesday, 23 January 2018 at 14:55:39 UTC, Simen Kjærås wrote:
> auto map(alias fn, R)(R r) if (isInputRange!(HeadMutable!R))
> {
>     // Pass head-mutable versions to MapResult.
>     return MapResult!(fn, HeadMutable!R)(headMutable(r));
> }

Another thing that I didn't think of when writing the above code was how this works with a mutating lambda:

unittest {
    const a = [1,2,3,4].map!((ref v) => v*=2);
    HeadMutable!(typeof(a)) b; // static assert  "No head-mutable type for const(MapResult!(__lambda1, int[]))"
}

Which makes sense - there is no way to make a head-mutable version of a from itself - it would break the constness guarantees.

--
  Biotronic
« First   ‹ Prev
1 2