Thread overview
Possible improvement of phobos map?
Jul 04, 2022
JG
Jul 04, 2022
JG
Jul 04, 2022
JG
Jul 04, 2022
Paul Backus
Jul 05, 2022
JG
Jul 06, 2022
H. S. Teoh
Jul 06, 2022
JG
Jul 06, 2022
H. S. Teoh
Jul 06, 2022
Paul Backus
Jul 06, 2022
Timon Gehr
July 04, 2022

Hi,

There was a recent discussion regarding suggesting some special algorithms for associative arrays, which made we wonder if the problem isn't actually that it is a slightly annoying when working with ranges of tuples or structs.

For instance you might have code like this

  auto a = ["a":5, "b":6];
  a.byPair.map!(t=>format!"%s=%s"(t[0],t[1])).writeln;

or

  auto r = iota(1,10);
  auto gcds = cartesianProduct(r,r).map!(t=>gcd(t[0],t[1]));

which while not too bad isn't as readable as it could be, and it gets worse when the chain gets longer.

With some slight changes to map (which wouldn't be needed if pattern matching exists), one can do this:

import std;

struct MapPrototype(alias f, R) {
    R r;
    bool empty() { return r.empty; }
    auto front() { return f(r.front.tupleof); }
    void popFront() { r.popFront; }
    auto save() { return typeof(this)(r.save); }
}

auto mapPrototype(alias f, R)(R r) {
    return MapPrototype!(f,R)(r);
}

struct Point {
    int x;
    int y;
}

void main() {
    auto r = iota(1,10);
    auto gcds = cartesianProduct(r,r).mapPrototype!(gcd);
    gcds.writeln;
    auto a = ["a":5, "b":6];
    a.byPair.mapPrototype!((name,value)=>format!"%s=%s"(name,value)).writeln;
    [Point(1,2),Point(3,4)].mapPrototype!((x,y)=>x*x+y*y).writeln;
}

Thoughts?

July 04, 2022

On Monday, 4 July 2022 at 19:44:11 UTC, JG wrote:

>

Hi,

There was a recent discussion regarding suggesting some special algorithms for associative arrays, which made we wonder if the problem isn't actually that it is a slightly annoying when working with ranges of tuples or structs.

For instance you might have code like this

  auto a = ["a":5, "b":6];
  a.byPair.map!(t=>format!"%s=%s"(t[0],t[1])).writeln;

or

  auto r = iota(1,10);
  auto gcds = cartesianProduct(r,r).map!(t=>gcd(t[0],t[1]));

which while not too bad isn't as readable as it could be, and it gets worse when the chain gets longer.

With some slight changes to map (which wouldn't be needed if pattern matching exists), one can do this:

import std;

struct MapPrototype(alias f, R) {
    R r;
    bool empty() { return r.empty; }
    auto front() { return f(r.front.tupleof); }
    void popFront() { r.popFront; }
    auto save() { return typeof(this)(r.save); }
}

auto mapPrototype(alias f, R)(R r) {
    return MapPrototype!(f,R)(r);
}

struct Point {
    int x;
    int y;
}

void main() {
    auto r = iota(1,10);
    auto gcds = cartesianProduct(r,r).mapPrototype!(gcd);
    gcds.writeln;
    auto a = ["a":5, "b":6];
    a.byPair.mapPrototype!((name,value)=>format!"%s=%s"(name,value)).writeln;
    [Point(1,2),Point(3,4)].mapPrototype!((x,y)=>x*x+y*y).writeln;
}

Thoughts?

I should have added, this is incomplete code one needs to check which behavior is wanted decomposition like above or the old behavior.

July 04, 2022

On Monday, 4 July 2022 at 19:51:01 UTC, JG wrote:

>

On Monday, 4 July 2022 at 19:44:11 UTC, JG wrote:

>

Hi,

There was a recent discussion regarding suggesting some special algorithms for associative arrays, which made we wonder if the problem isn't actually that it is a slightly annoying when working with ranges of tuples or structs.

For instance you might have code like this

  auto a = ["a":5, "b":6];
  a.byPair.map!(t=>format!"%s=%s"(t[0],t[1])).writeln;

or

  auto r = iota(1,10);
  auto gcds = cartesianProduct(r,r).map!(t=>gcd(t[0],t[1]));

which while not too bad isn't as readable as it could be, and it gets worse when the chain gets longer.

With some slight changes to map (which wouldn't be needed if pattern matching exists), one can do this:

import std;

struct MapPrototype(alias f, R) {
    R r;
    bool empty() { return r.empty; }
    auto front() { return f(r.front.tupleof); }
    void popFront() { r.popFront; }
    auto save() { return typeof(this)(r.save); }
}

auto mapPrototype(alias f, R)(R r) {
    return MapPrototype!(f,R)(r);
}

struct Point {
    int x;
    int y;
}

void main() {
    auto r = iota(1,10);
    auto gcds = cartesianProduct(r,r).mapPrototype!(gcd);
    gcds.writeln;
    auto a = ["a":5, "b":6];
    a.byPair.mapPrototype!((name,value)=>format!"%s=%s"(name,value)).writeln;
    [Point(1,2),Point(3,4)].mapPrototype!((x,y)=>x*x+y*y).writeln;
}

Thoughts?

I should have added, this is incomplete code one needs to check which behavior is wanted decomposition like above or the old behavior.

To be clear I mean something like this:

import std;

struct MapPrototype(alias f, R) {
    R r;
    bool empty() { return r.empty; }
    auto front() {
        static if(__traits(compiles,f(R.init.front))) {
            return f(r.front);
        } else {
          return f(r.front.tupleof);
        }
    }
    void popFront() { r.popFront; }
    auto save() { return typeof(this)(r.save); }
}

auto mapPrototype(alias f, R)(R r) {
    return MapPrototype!(f,R)(r);
}

struct Point {
    int x;
    int y;
}

void main() {
    auto r = iota(1,10);
    auto gcds = cartesianProduct(r,r).mapPrototype!(gcd);
    gcds.writeln;
    auto a = ["a":5, "b":6];
    a.byPair.mapPrototype!((name,value)=>format!"%s=%s"(name,value)).writeln;
    [Point(1,2),Point(3,4)].mapPrototype!((x,y)=>x*x+y*y).writeln;
    [Point(1,2),Point(3,4)].mapPrototype!(p=>p.x).writeln;
}
July 04, 2022

On Monday, 4 July 2022 at 19:44:11 UTC, JG wrote:

>

Hi,

There was a recent discussion regarding suggesting some special algorithms for associative arrays, which made we wonder if the problem isn't actually that it is a slightly annoying when working with ranges of tuples or structs.

For instance you might have code like this

  auto a = ["a":5, "b":6];
  a.byPair.map!(t=>format!"%s=%s"(t[0],t[1])).writeln;

or

  auto r = iota(1,10);
  auto gcds = cartesianProduct(r,r).map!(t=>gcd(t[0],t[1]));

which while not too bad isn't as readable as it could be, and it gets worse when the chain gets longer.

You can clean these up using the recently-added std.functional.bind:

    a.byPair.map!(bind!((k, v) => format!"%s=%s"(k, v))).writeln;
    auto gcds = cartesianProduct(r,r).map!(bind!gcd);
July 05, 2022

On Monday, 4 July 2022 at 19:55:21 UTC, Paul Backus wrote:

>

On Monday, 4 July 2022 at 19:44:11 UTC, JG wrote:

>

Hi,

There was a recent discussion regarding suggesting some special algorithms for associative arrays, which made we wonder if the problem isn't actually that it is a slightly annoying when working with ranges of tuples or structs.

For instance you might have code like this

  auto a = ["a":5, "b":6];
  a.byPair.map!(t=>format!"%s=%s"(t[0],t[1])).writeln;

or

  auto r = iota(1,10);
  auto gcds = cartesianProduct(r,r).map!(t=>gcd(t[0],t[1]));

which while not too bad isn't as readable as it could be, and it gets worse when the chain gets longer.

You can clean these up using the recently-added std.functional.bind:

    a.byPair.map!(bind!((k, v) => format!"%s=%s"(k, v))).writeln;
    auto gcds = cartesianProduct(r,r).map!(bind!gcd);

Thanks that is cleaner. I am not sure if it as clean as if map implicitly did it, but perhaps it could also be confusing (and plenty of other range based algorithms would need similar changes).

July 05, 2022
On Mon, Jul 04, 2022 at 07:44:11PM +0000, JG via Digitalmars-d wrote: [...]
> ```d
>   auto a = ["a":5, "b":6];
>   a.byPair.map!(t=>format!"%s=%s"(t[0],t[1])).writeln;
> ```

Yeah, I found this annoying too.


[...]
> With some slight changes to map (which wouldn't be needed if pattern matching exists), one can do this:
> 
> ```d
[...]
> a.byPair.mapPrototype!((name,value)=>format!"%s=%s"(name,value)).writeln;
>     [Point(1,2),Point(3,4)].mapPrototype!((x,y)=>x*x+y*y).writeln;
[..]
> ```
> 
> Thoughts?

This is definitely much more pleasant to write.

In fact, I wonder if the current map couldn't be extended to handle this. We just need .map to detect if the lambda has multiple arguments, and expand the mapped element accordingly. A lambda argument that takes two parameters cannot be confused with a lambda argument that takes only one, so this wouldn't affect existing code. Something like this:

	auto map(alias fun, R)(R range) {
		static if (arity!fun == 1) {
			... // current implementation
		} else {
			... // new implementation, expand element with .tupleof
			    // possibly also check if .tupleof.length
			    // matches arity!fun.
		}
	}


T

-- 
The irony is that Bill Gates claims to be making a stable operating system and Linus Torvalds claims to be trying to take over the world. -- Anonymous
July 06, 2022
On Wednesday, 6 July 2022 at 01:28:32 UTC, H. S. Teoh wrote:

> In fact, I wonder if the current map couldn't be extended to handle this.

Yes, it definitely could (my second version does this). I guess one downside is that several other range algorithms could do with the same change (rather than using bind which requires no change).
July 06, 2022
On Wed, Jul 06, 2022 at 09:00:59AM +0000, JG via Digitalmars-d wrote:
> On Wednesday, 6 July 2022 at 01:28:32 UTC, H. S. Teoh wrote:
> 
> > In fact, I wonder if the current map couldn't be extended to handle this.
> 
> Yes, it definitely could (my second version does this). I guess one
> downside is that several other range algorithms could do with the same
> change (rather than using bind which requires no change).

IMO it's worth it. Or maybe use bind internally in Phobos so that we don't have to repeat the implementation across different Phobos functions.  But the user-facing API should be as smooth as possible.


T

-- 
People tell me that I'm paranoid, but they're just out to get me.
July 06, 2022
On Wednesday, 6 July 2022 at 17:21:29 UTC, H. S. Teoh wrote:
> On Wed, Jul 06, 2022 at 09:00:59AM +0000, JG via Digitalmars-d wrote:
>> On Wednesday, 6 July 2022 at 01:28:32 UTC, H. S. Teoh wrote:
>> 
>> > In fact, I wonder if the current map couldn't be extended to handle this.
>> 
>> Yes, it definitely could (my second version does this). I guess one
>> downside is that several other range algorithms could do with the same
>> change (rather than using bind which requires no change).
>
> IMO it's worth it. Or maybe use bind internally in Phobos so that we don't have to repeat the implementation across different Phobos functions.  But the user-facing API should be as smooth as possible.

Disagree. This would be a textbook example of generality creep [1]. map currently (sensibly) requires that the function given to it accept a range element as an argument. Having it go out of its way to try and accept any function that can be cajoled into accepting a range element, if you apply some kind of transformation first (like adding .tupleof), would ultimately make its behavior more complicated and harder to understand.

[1] https://forum.dlang.org/thread/q6plhj$1l9$1@digitalmars.com
July 06, 2022
On 04.07.22 21:44, JG wrote:
> Hi,
> 
> There was a recent discussion regarding suggesting some special algorithms for associative arrays, which made we wonder if the problem isn't actually that it is a slightly annoying when working with ranges of tuples or structs.
> 
> For instance you might have code like this
> 
> ```d
>    auto a = ["a":5, "b":6];
>    a.byPair.map!(t=>format!"%s=%s"(t[0],t[1])).writeln;
> ```
> or
> ```d
>    auto r = iota(1,10);
>    auto gcds = cartesianProduct(r,r).map!(t=>gcd(t[0],t[1]));
> 
> ```
> which while not too bad isn't as readable as it could be, and it gets worse when the chain gets longer.
> 
> With some slight changes to map (which wouldn't be needed if pattern matching exists), one can do this:
> 
> ```d
> import std;
> 
> struct MapPrototype(alias f, R) {
>      R r;
>      bool empty() { return r.empty; }
>      auto front() { return f(r.front.tupleof); }
>      void popFront() { r.popFront; }
>      auto save() { return typeof(this)(r.save); }
> }
> 
> auto mapPrototype(alias f, R)(R r) {
>      return MapPrototype!(f,R)(r);
> }
> 
> struct Point {
>      int x;
>      int y;
> }
> 
> void main() {
>      auto r = iota(1,10);
>      auto gcds = cartesianProduct(r,r).mapPrototype!(gcd);
>      gcds.writeln;
>      auto a = ["a":5, "b":6];
> a.byPair.mapPrototype!((name,value)=>format!"%s=%s"(name,value)).writeln;
>      [Point(1,2),Point(3,4)].mapPrototype!((x,y)=>x*x+y*y).writeln;
> }
> ```
> 
> Thoughts?
> 
> 

I want this as well, but it is probably better to improve the language than to add additional behavior to Phobos.

The fundamental issue is that there is a difference between functions with multiple arguments and functions that accept a single tuple argument. Argument lists are basically tuples, but we need to make them first-class values. Unfortunately, this is not so easy to retrofit into existing language semantics.