August 05, 2022
On Thursday, 4 August 2022 at 21:31:02 UTC, Paul Backus wrote:
> Strictly speaking, you don't even need that; all you need is a range that generates the natural numbers, and you can write
>
>     naturals.take(n)
>
> ...to get the equivalent of iota(n).
>
> That said, I don't think you have to aim for total minimalism to avoid generality creep here. You just need to know what iota's purpose for inclusion is, so that you can say "no, that's not iota's job; use generate/recurrence/something else instead" when someone proposes an overly-general enhancement.

I agree!  It's like rangeTo and downTo in Kotlin, right?  The simpler, the better!

SDB@79
August 05, 2022
On Friday, 5 August 2022 at 00:56:04 UTC, Paul Backus wrote:
> On Thursday, 4 August 2022 at 22:36:57 UTC, H. S. Teoh wrote:
>>
>> Perhaps.  But the question now is, where do we go from here?  How do we improve what we currently have?
>
> Phobos v2, I guess? Leave the problematic overloads behind, and tell people upgrading to use a different function if they need something fancier.
>
> Unless there's been a big policy reversal announced at DConf, my impression is that breaking changes to existing Phobos interfaces are off the table.

There is probably a balance here between simple low level constructions but at the same time a convenient high level API. As a user, I would just much rather be able to do something like `x.iota(a, b)` where `x` could be anything that it might make sense to call `iota` on (I like "Design by Introspection" with a bit of duck typing thrown in).

The issue with something like `iota(n).map!(i => MyType.startValue + i)` is that I might have to write that all the time as a user. From my perspective, I would rather put an `iota` overload in `MyType`'s module if I can't have it as part of `phobos`. From this perspective, `std.algorithm` should support built-in types and other types could define their own `iota` that call these.
August 05, 2022

On 8/4/22 8:12 PM, Steven Schveighoffer wrote:

>

On 8/4/22 3:06 PM, H. S. Teoh wrote:

>

For example, if you want a range that starts from 10 and ends at 20,
just write:

    iota(10).map!(i => i + 10)

If you want a range that starts at 3 and steps by 5 each time, just write:

    iota(n).map!(i => 3 + i*5)

For some reason, the forum.dlang.org software regarded the paragraph below as belonging to Teoh. It was something I wrote.

>

Sorry, I'd rather specify the start, end, and steps, then have to work backwards from the formula what it means. Clarity is important.

-Steve

August 05, 2022

On Friday, 5 August 2022 at 00:12:40 UTC, Steven Schveighoffer wrote:

>

As for the floating point thing, foreach(i; 0.5 .. 7.6) works, it should too for iota.

Argument by analogy like this is exactly what leads to generality creep in the first place. You can just as easily say, "for (auto i = start; i < end; i++) works for any type that overloads < and ++, so iota(start, end) should do the same", and then you end up with the exact problem OP describes.

There has to be some deeper principle that lets you decide which analogies are valid and which aren't. Like, what is our purpose for including iota in Phobos, when we already have more general facilities like recurrence, sequence, and generate; and does adding a particular feature to iota serve that purpose?

In this case I think your conclusion is correct, but the reasoning behind it is iffy.

August 05, 2022

On 8/5/22 11:20 AM, Paul Backus wrote:

>

On Friday, 5 August 2022 at 00:12:40 UTC, Steven Schveighoffer wrote:

>

As for the floating point thing, foreach(i; 0.5 .. 7.6) works, it should too for iota.

Argument by analogy like this is exactly what leads to generality creep in the first place. You can just as easily say, "for (auto i = start; i < end; i++) works for any type that overloads < and ++, so iota(start, end) should do the same", and then you end up with the exact problem OP describes.

This isn't "argument by analogy". We have facilities built into the language which support an interface of "x to y". If iota is going to be the range equivalent, it should at least support that for all types that are supported for built-in range expressions.

A consistent API is important.

As for working with arbitrary types, I don't see a problem with it. If you define ++ on your type or += 1, and it doesn't mean the same as every other type that defines that, then it's on you for not following the conventions. D is a language which uses the introspected abilities of things to define whether they are compatible.

-Steve

August 05, 2022

On Friday, 5 August 2022 at 15:20:12 UTC, Paul Backus wrote:

>

[snip]

There has to be some deeper principle that lets you decide which analogies are valid and which aren't. Like, what is our purpose for including iota in Phobos, when we already have more general facilities like recurrence, sequence, and generate; and does adding a particular feature to iota serve that purpose?

In this case I think your conclusion is correct, but the reasoning behind it is iffy.

This reminds me of Coase's "Nature of the Firm", which looks at the question of when firms should do things within the firm or contract them outside. If your language only has low level primitives that can build up everything, then there is an additional cost for users to figure out how those things combine. Higher level API (or more generality) can help reduce the cost that users face, but also have a trade-off by making the code base more difficult to maintain, etc. What you're suggesting puts all the cost on the user. I would argue that there is a trade-off, such that it makes sense in some places to offer higher level API for common use cases.

August 05, 2022

On Friday, 5 August 2022 at 17:03:35 UTC, jmh530 wrote:

>

If your language only has low level primitives that can build up everything, then there is an additional cost for users to figure out how those things combine. Higher level API (or more generality) can help reduce the cost that users face, but also have a trade-off by making the code base more difficult to maintain, etc. What you're suggesting puts all the cost on the user. I would argue that there is a trade-off, such that it makes sense in some places to offer higher level API for common use cases.

Yes, absolutely. I'm not trying to suggest that we shouldn't have high-level APIs. I'm trying to suggest that our high-level APIs should be the product of conscious design choices, rather than gradual accretion of special cases (a.k.a. "generality creep").

August 06, 2022

On Friday, 5 August 2022 at 15:31:18 UTC, Steven Schveighoffer wrote:

>

On 8/5/22 11:20 AM, Paul Backus wrote:

>

On Friday, 5 August 2022 at 00:12:40 UTC, Steven Schveighoffer wrote:

>

As for the floating point thing, foreach(i; 0.5 .. 7.6) works, it should too for iota.

Argument by analogy like this is exactly what leads to generality creep in the first place. You can just as easily say, "for (auto i = start; i < end; i++) works for any type that overloads < and ++, so iota(start, end) should do the same", and then you end up with the exact problem OP describes.

This isn't "argument by analogy". We have facilities built into the language which support an interface of "x to y". If iota is going to be the range equivalent, it should at least support that for all types that are supported for built-in range expressions.

A consistent API is important.

As for working with arbitrary types, I don't see a problem with it. If you define ++ on your type or += 1, and it doesn't mean the same as every other type that defines that, then it's on you for not following the conventions. D is a language which uses the introspected abilities of things to define whether they are compatible.

-Steve

I think that deserves to be a guiding principle that motivates the design rather than a hard constraint. Imo nothing particularly bad happens here if iota is slightly more limited. I don’t really have an overall opinion on the “what should iota take” topic though.

August 06, 2022

On Friday, 5 August 2022 at 15:31:18 UTC, Steven Schveighoffer wrote:

>

As for working with arbitrary types, I don't see a problem with it. If you define ++ on your type or += 1, and it doesn't mean the same as every other type that defines that, then it's on you for not following the conventions. D is a language which uses the introspected abilities of things to define whether they are compatible.

One of the problems with iota is that, because it was built up by accretion of special cases, it does not adhere to any kind of consistent structural interface.

For example, this works:

iota(BigInt(1), BigInt(10))

...but this does not:

iota(BigInt(1), BigInt(10), BigInt(1))

...because nobody ever bothered to add a (start, end, step) overload for custom types.

August 06, 2022
On Sat, Aug 06, 2022 at 03:11:28PM +0000, Paul Backus via Digitalmars-d wrote:
> On Friday, 5 August 2022 at 15:31:18 UTC, Steven Schveighoffer wrote:
> > As for working with arbitrary types, I don't see a problem with it. If you define ++ on your type or += 1, and it doesn't mean the same as every other type that defines that, then it's on you for not following the conventions. D is a language which uses the introspected abilities of things to define whether they are compatible.
> 
> One of the problems with `iota` is that, because it was built up by accretion of special cases, it does *not* adhere to any kind of consistent structural interface.
> 
> For example, this works:
> 
>     iota(BigInt(1), BigInt(10))
> 
> ...but this does not:
> 
>     iota(BigInt(1), BigInt(10), BigInt(1))
> 
> ...because nobody ever bothered to add a `(start, end, step)` overload
> for custom types.

Perhaps what we need is to sit down and come up with a consistent model for iota and implement that. A consistent API of what is expected of incoming types and how iota will behave in each case.

Here's a first stab at it. Let T be the incoming type (I'm leaning against separately parametrizing the start/end/increment types, I think that's just needlessly complex). Then:

0. T must at least support < and at least one of ++, +. If not, iota(T)
   is not supported.

1. If T supports ++, then iota(start,end) will return at least an input
   range.

2. If `T + int` is valid, then iota(start,end) will return at least a
   random-access range.

3. If `T + T` is valid, then iota(start,end,step) will return at least
   an input range.

4. If `T + int*T` is valid, then iota(start,end,step) will return a
   random-access range.


T

-- 
Leather is waterproof.  Ever see a cow with an umbrella?