Thread overview
Language Idea #6892: in array ops, enable mixing slices and random access ranges
Feb 05, 2018
Guillaume Piolat
Feb 06, 2018
Meta
Feb 06, 2018
Simen Kjærås
Feb 06, 2018
Guillaume Piolat
Feb 06, 2018
Simen Kjærås
Feb 06, 2018
Guillaume Piolat
February 05, 2018
General idea
============

Currently arrays ops express loops over slices.

    a[] = b[] * 2 + c[]

It would be nice if one could mix a random access range into such an expression.
The compiler would have builtin support for random access range.


Example
=======

------------------------------>3---------------------------------------

import std.algorithm;
import std.array;
import std.range;

void main()
{
	int[] A = [1, 2, 3];
	
	// arrays ops only work with slices
	A[] += iota(3).array[];
	
	
	// Check that iota is a random access range
	auto myRange = iota(3);
	static assert( isRandomAccessRange!(typeof(myRange)) );
	
	
	// Doesn't work, array ops can't mix random access ranges and slices
        // NEW
	A[] += myRange[]; // whatever syntax could help the compiler
}

------------------------------>3---------------------------------------


How it could work
=================

    A[] += myRange[]; // or another syntax for "myRange as an array op operand"

would be rewritten to:

    foreach(i; 0..A.length)
        A[i] += myRange[i];


myRange should not be a range without "length".


Why?
====

Bridges a gap between lazy generation and array ops, now that array ops are reliable.
Allow arrays ops to take slice-like objects.


What do you think?
February 06, 2018
On Monday, 5 February 2018 at 17:35:45 UTC, Guillaume Piolat wrote:
> General idea
> ============
>
> Currently arrays ops express loops over slices.
>
>     a[] = b[] * 2 + c[]
>
> It would be nice if one could mix a random access range into such an expression.
> The compiler would have builtin support for random access range.
>
>
> Example
> =======
>
> ------------------------------>3---------------------------------------
>
> import std.algorithm;
> import std.array;
> import std.range;
>
> void main()
> {
> 	int[] A = [1, 2, 3];
> 	
> 	// arrays ops only work with slices
> 	A[] += iota(3).array[];
> 	
> 	
> 	// Check that iota is a random access range
> 	auto myRange = iota(3);
> 	static assert( isRandomAccessRange!(typeof(myRange)) );
> 	
> 	
> 	// Doesn't work, array ops can't mix random access ranges and slices
>         // NEW
> 	A[] += myRange[]; // whatever syntax could help the compiler
> }
>
> ------------------------------>3---------------------------------------
>
>
> How it could work
> =================
>
>     A[] += myRange[]; // or another syntax for "myRange as an array op operand"
>
> would be rewritten to:
>
>     foreach(i; 0..A.length)
>         A[i] += myRange[i];
>
>
> myRange should not be a range without "length".
>
>
> Why?
> ====
>
> Bridges a gap between lazy generation and array ops, now that array ops are reliable.
> Allow arrays ops to take slice-like objects.
>
>
> What do you think?

It's already possible, with only very slightly worse aesthetics:

struct VecOp(T)
{
    T[] arr;

    pragma(inline, true)
    T[] opOpAssign(string op: "+", Range)(Range r)
    {
        int i;
        foreach (e; r)
        {
            arr[i] += e;
            i++;
        }

        return arr;
    }
}

pragma(inline, true)
VecOp!E vecOp(E)(return E[] arr)
{
    return typeof(return)(arr);
}

void main()
{
    import std.range: iota;

    int[] a = [1, 2, 3];
    a.vecOp += iota(3);

    assert(a == [1, 3, 5]);
}

I'm not very good at reading assembly, so I have no idea whether it's comparable to doing `a[] += [0, 1, 2]`.
February 06, 2018
On Tuesday, 6 February 2018 at 02:14:35 UTC, Meta wrote:
>> What do you think?
>
> It's already possible, with only very slightly worse aesthetics:
>
[Good stuff]

We can do better than that, though:

import std.range, std.array, std.algorithm;

struct Vec(Range)
if (isInputRange!Range)
{
    Range rng;

    this(Range value)
    {
        rng = value;
    }

    static if (!isInfinite!Range)
    {
        auto get()
        {
            return rng.array;
        }
        alias get this;
    }

    auto getRange(R2)(R2 other)
    {
        static if (isInputRange!R2)
            return other;
        else static if (isVec!R2)
            return other.rng;
        else
            return repeat(other);
    }

    auto opBinary(string op, R2)(R2 rhs)
    {
        return vec(rng.zip(getRange(rhs)).map!("a[0] "~op~" a[1]"));
    }

    auto opBinaryRight(string op, R2)(R2 lhs)
    {
        return vec(getRange(lhs).zip(rng).map!("a[0] "~op~" a[1]"));
    }

    auto opOpAssign(string op, R2)(R2 rhs)
    if (isForwardRange!Range)
    {
        auto rhsR = getRange(rhs);

        auto r = rng.save;
        foreach (ref e; r)
        {
            if (rhsR.empty) break;
            mixin("e "~op~"= rhsR.front;");
            rhsR.popFront();
        }
        return this;
    }
}

auto vec(Range)(Range r)
if (isInputRange!Range)
{
    return Vec!Range(r);
}

enum isVec(T) = is(T == Vec!U, U);

unittest
{
    import std.stdio;
    auto a = [1,2,3,4,5,6,7,8];
    auto b = a.length.iota;
    auto c = recurrence!("a[n-1] + a[n-2]")(1, 1);

    uint[] result = a + b * c.vec;
    writeln(result);
    result.vec += result;
}

--
  Simen
February 06, 2018
On Tuesday, 6 February 2018 at 09:02:46 UTC, Simen Kjærås wrote:
> On Tuesday, 6 February 2018 at 02:14:35 UTC, Meta wrote:
>>> What do you think?
>>
>> It's already possible, with only very slightly worse aesthetics:
>>
> [Good stuff]
>
> We can do better than that, though:
>
> [More good stuff]

The problem in your sample is that you turn a range into a new slice with .array (which is "alias this" also), and this allocates.

Meta's solution brings slices into the range world instead (rather than ranges to array ops like originally proposed).

February 06, 2018
On Tuesday, 6 February 2018 at 02:14:35 UTC, Meta wrote:
> It's already possible, with only very slightly worse aesthetics:
>
> struct VecOp(T)
> {
>     T[] arr;
>
>     pragma(inline, true)
>     T[] opOpAssign(string op: "+", Range)(Range r)
>     {
>         int i;
>         foreach (e; r)
>         {
>             arr[i] += e;
>             i++;
>         }
>
>         return arr;
>     }
> }
>
> pragma(inline, true)
> VecOp!E vecOp(E)(return E[] arr)
> {
>     return typeof(return)(arr);
> }


I'm not sure if this is equivalent to what I asked, but in the event it is, here is a thought.

A fully featured VecOp could be part of druntime, and slices as array ops operands could be replaced by slice.vecOp by the compiler.

If this vectorize well, this allow to remove array ops from the language. (However it seems to me array ops are recognized at the operator level but well)
February 06, 2018
On Tuesday, 6 February 2018 at 12:13:22 UTC, Guillaume Piolat wrote:
> On Tuesday, 6 February 2018 at 09:02:46 UTC, Simen Kjærås wrote:
>> On Tuesday, 6 February 2018 at 02:14:35 UTC, Meta wrote:
>>>> What do you think?
>>>
>>> It's already possible, with only very slightly worse aesthetics:
>>>
>> [Good stuff]
>>
>> We can do better than that, though:
>>
>> [More good stuff]
>
> The problem in your sample is that you turn a range into a new slice with .array (which is "alias this" also), and this allocates.

Only if you assign it to an array. While it's not very well indicated in the above example, this also works, and does not allocate:

unittest
{
    auto a = [1,2,3,4,5,6,7,8];
    auto b = a.length.iota;

    a.vec += b;
}

--
  Simen