www.digitalmars.com         C & C++   DMDScript  

digitalmars.D - Possible improvement of phobos map?

reply JG <someone somewhere.com> writes:
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?
Jul 04
next sibling parent reply JG <someone somewhere.com> writes:
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

 ```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 should have added, this is incomplete code one needs to check which behavior is wanted decomposition like above or the old behavior.
Jul 04
parent JG <someone somewhere.com> writes:
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

 ```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 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: ```d 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; } ```
Jul 04
prev sibling next sibling parent reply Paul Backus <snarwin gmail.com> writes:
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

 ```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.
You can clean these up using the recently-added [`std.functional.bind`][1]: ```d a.byPair.map!(bind!((k, v) => format!"%s=%s"(k, v))).writeln; ``` ```d auto gcds = cartesianProduct(r,r).map!(bind!gcd); ``` [1]: https://phobos.dpldocs.info/std.functional.bind.html
Jul 04
parent JG <someone simewhere.com> writes:
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

 ```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.
You can clean these up using the recently-added [`std.functional.bind`][1]: ```d a.byPair.map!(bind!((k, v) => format!"%s=%s"(k, v))).writeln; ``` ```d auto gcds = cartesianProduct(r,r).map!(bind!gcd); ``` [1]: https://phobos.dpldocs.info/std.functional.bind.html
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).
Jul 05
prev sibling next sibling parent reply "H. S. Teoh" <hsteoh qfbox.info> writes:
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
Jul 05
parent reply JG <someone somewhere.com> writes:
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).
Jul 06
parent reply "H. S. Teoh" <hsteoh qfbox.info> writes:
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.
Jul 06
parent Paul Backus <snarwin gmail.com> writes:
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
Jul 06
prev sibling parent Timon Gehr <timon.gehr gmx.ch> writes:
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.
Jul 06