March 06
On Wednesday, March 6, 2024 1:23:52 PM MST Meta via Digitalmars-d wrote:
> Couldn't we somehow fit RefRange into the
> current range hierarchy somewhere, and provide primitives to make
> it more difficult to shoot yourself in the foot?

RefRange was a huge mistake (and it was my mistake). It's basically trying to force reference semantics on something that has value semantics, and it causes problems as a result. I don't think that it can actually be made to work properly - at least not with forward ranges.

IMHO, we need to be able to rely on the semantics of the basic range operations, and most of the problems with the current range API come from when operations are under-specified or outright unspecified - be it because we're trying to support too much (e.g. both structs and classes for ranges), or because we just didn't actually think to specify what the behavior should be. And in some cases, we though of it but too late to actually require it (e.g. you can't actually rely on $ working with random-access ranges, because we didn't think to require it up front, and too many random-access ranges had been written that didn't support it by the time we realized our mistake).

- Jonathan M Davis



March 06
On Wednesday, March 6, 2024 1:53:55 PM MST Dukc via Digitalmars-d wrote:
> The gripe I have with `std.range.interface` is lack of attributes
> in the virtual functions. I don't think any simple attribute set
> alone would solve the problem well. Granted, there should rarely
> be reason to have a dynamic `@system` range type, but `pure` and
> `nothrow` are attributes you sometimes really want, sometimes you
> really don't. Therefore we need the ability to pick, meaning the
> interfaces will have to be templated, along the lines of:
> ```D
> interface ForwardRange(uint attrs) : InputRange!attrs
> {
>     // ...
> }
> ```
>
> Plus there should probably be function to convert, for example, a
> ```
> BidirectionalRange!(FunctionAttribute!safe |
> FunctionAttribute!pure_ | FunctionAttribute!nothrow_)
> ```
> to
> ```
> BidirectionalRange!(FunctionAttribute!safe |
> FunctionAttribute!nothrow_)
> ```
> .

This is a fundamental problem with classes in general. Attributes just do not play nicely with them. Either you're forced to require attributes that potentially won't work for some code, or you're forced to disallow attributes that some code might require.

And what you're describing results in a proliferation of interfaces, which to an extent, goes against the point of using interfaces. So, we might do something like that, but it does cause a different set of problems.

Regardless, we will have to look over exactly what we want to do with the replacement for std.range.interfaces. I think that it was mostly thrown together because it was thought that it was needed and not because it really had been needed much at that point, and we should definitely look at what needs reworking based on what we've learned in the interim. Classes in general are kind of terrible with generic code in D though, particularly because of the issues with attributes.

- Jonathan M Davis



March 06
On Wed, Mar 06, 2024 at 08:53:55PM +0000, Dukc via Digitalmars-d wrote:
> On Wednesday, 6 March 2024 at 17:57:36 UTC, H. S. Teoh wrote:
[...]
> > This makes .choose essentially useless outside of trivial cases.
> 
> It's far from useless, it's only like taking an enum argument and `switch`ing on it instead of taking a delerate argument. Certainly works but means you have to list all your cases in the function, which may or may not be what you want.

Well yes, so it's very limited in what it can do. In the cases where you can't list all cases in the call to .choose, or where you don't want to for code cleanliness reasons, it's useless.


> The gripe I have with `std.range.interface` is lack of attributes in the virtual functions. I don't think any simple attribute set alone would solve the problem well.
[...]

The problem is that the base class cannot predict which restrictive attributes will be applied to a derived class override of a method. You cannot override a @safe base class method with a @system derived class method, for example. So it must settle for the most permissive attributes.  But that means the code that receives a base class reference must handle the most permissive attributes as well; a @safe function would not be able to invoke a @safe method in a derived class if the only reference it has to the derived object is a base class reference.  There's no way around this except to get rid of (restrictive) attributes altogether.


T

-- 
EMACS = Extremely Massive And Cumbersome System
March 06
On Wednesday, 6 March 2024 at 21:17:37 UTC, Jonathan M Davis wrote:
> On Wednesday, March 6, 2024 1:23:52 PM MST Meta via Digitalmars-d wrote:
>> Couldn't we somehow fit RefRange into the
>> current range hierarchy somewhere, and provide primitives to make
>> it more difficult to shoot yourself in the foot?
>
> RefRange was a huge mistake (and it was my mistake). It's basically trying to force reference semantics on something that has value semantics, and it causes problems as a result. I don't think that it can actually be made to work properly - at least not with forward ranges.
>
> IMHO, we need to be able to rely on the semantics of the basic range operations, and most of the problems with the current range API come from when operations are under-specified or outright unspecified - be it because we're trying to support too much (e.g. both structs and classes for ranges), or because we just didn't actually think to specify what the behavior should be. And in some cases, we though of it but too late to actually require it (e.g. you can't actually rely on $ working with random-access ranges, because we didn't think to require it up front, and too many random-access ranges had been written that didn't support it by the time we realized our mistake).
>
> - Jonathan M Davis

I don't mean the type RefRange; I mean expanding the range hierarchy to include the concept of a "RefRange" (just like InputRange, BidirectionalRange, etc.) that explicitly has reference semantics instead of value semantics. Then you can specialize algorithms on whether they're a ref range or not (implying that ranges with reference semantics should no longer pass the isInputRange et al. checks - no idea how you'd do that OTOH), and then you can account for that fact in the implementation of the algorithm, rather than all range algorithms currently carrying an implicit assumption that they're working with a value range.
March 06

On Wednesday, 6 March 2024 at 21:24:59 UTC, Jonathan M Davis wrote:

>

This is a fundamental problem with classes in general. Attributes just do not play nicely with them. Either you're forced to require attributes that potentially won't work for some code, or you're forced to disallow attributes that some code might require.

It's not a problem with classes, it's a decision one has to make when designing an interface with runtime variance regardless of the mechanism. You either have to allow the unknown code you're calling to execute side effects in which case it's impure, or you must execute the side effects it requests on it's behalf which means the unknown code must use your interfaces instead of doing their io themselves. It would be the same with any other mechanism.

>

And what you're describing results in a proliferation of interfaces, which to an extent, goes against the point of using interfaces.

I don't think so. Say, you're designing an interpreter, and decide the user-defined functions can't execute io directly or break D type system, but can throw exceptions. Therefore the D type for a range in your client langauge will be ForwardRange!(ClientLangValue, FunctionAttribute.safe | FunctionAttribute.pure_). You need only one template instance, same as presently. How is this exactly proliferation?

March 06
On Wednesday, March 6, 2024 3:54:54 PM MST Dukc via Digitalmars-d wrote:
> On Wednesday, 6 March 2024 at 21:24:59 UTC, Jonathan M Davis
>
> wrote:
> > This is a fundamental problem with classes in general. Attributes just do not play nicely with them. Either you're forced to require attributes that potentially won't work for some code, or you're forced to disallow attributes that some code might require.
>
> It's not a problem with classes, it's a decision one has to make when designing an interface with runtime variance regardless of the mechanism. You either have to allow the unknown code you're calling to execute side effects in which case it's impure, or you must execute the side effects it requests on it's behalf which means the unknown code must use your interfaces instead of doing their io themselves. It would be the same with any other mechanism.

Yes, it's a problem with non-templated code in general, but because you can't templatize virtual functions, it's a problem with classes that you can't really fix, whereas with most other code, you can usually fix it by templatizing the code and inferring the attributes based on the arguments.

> > And what you're describing results in a proliferation of interfaces, which to an extent, goes against the point of using interfaces.
>
> I don't think so. Say, you're designing an interpreter, and decide the user-defined functions can't execute io directly or break D type system, but can throw exceptions. Therefore the D type for a range in your client langauge will be `ForwardRange!(ClientLangValue, FunctionAttribute.safe | FunctionAttribute.pure_)`. You need only one template instance, same as presently. How is this exactly proliferation?

It's a proliferation, because you're declaring a bunch of different interfaces. Instead of having ForwardRange, you have a whole bunch of variants of ForwardRange. Now, that might not end up being a problem in practice (particularly since you do already have to templatize ForwardRange on the element type), because you may only need one variation in your codebase, but it does further complicate the interfaces, and it does mean that interfaces with the same element type won't necessarily be compatible, whereas with the current design, that isn't a problem.

So, it may ultimately be what we want to do, but it's not without its downsides.

- Jonathan M Davis



March 07

On Wednesday, 6 March 2024 at 13:26:14 UTC, Paul Backus wrote:

>

On Wednesday, 6 March 2024 at 12:20:40 UTC, Atila Neves wrote:

>

I like T.init being empty. I thought I'd found a clever way to make this work for classes but apparently this crashes, much to my surprise:

class Class {
    bool empty() @safe @nogc pure nothrow scope const {
        return this is null;
    }
}

Class c;
assert(c.empty);

It crashes because it's attempting to access Class's vtable. If you make the method final, it works:

class Class {
    final bool empty() {
        return this is null;
    }
}

void main()
{
    Class c;
    assert(c.empty); // ok
}

Of course! I write classes so seldom I forget my own rule to add final unless I can't.

That would severely restrict class ranges though, since presumably the whole point is to have dynamic dispatch.

March 07
On Thursday, March 7, 2024 2:11:29 AM MST Atila Neves via Digitalmars-d wrote:
> Of course! I write classes so seldom I forget my own rule to add `final` unless I can't.
>
> That would severely restrict class ranges though, since presumably the whole point is to have dynamic dispatch.

The solution that I'm currently planning to go with is to wrap them:

https://forum.dlang.org/post/mailman.1091.1709755456.3719.digitalmars-d@puremagic.com

- Jonathan M Davis



March 07

On Wednesday, 6 March 2024 at 19:03:39 UTC, Jonathan M Davis wrote:

>

The issue is whether the function is virtual. In D, class member functions are virtual by default, so you get a crash when the program attempts to use the vtable to look the function up (and therefore attempts to dereference null), whereas in C++, you don't, because class member functions are non-virtual by default, and so it doesn't do anything with the vtable. If you mark the function as final in D (without it overriding anything), then there shouldn't be a crash, and if you mark the C++ function as virtual, then there will be.

Virtual or not, GCC assumes that this cannot be null:

#include <cassert>

class C {
public:
    bool empty() const {
         return this == nullptr; // warning: `nonnull` argument `this` compared to NULL
    }
};

int main()
{
    assert(((C*)nullptr)->empty()); // warning: `this` pointer is null
}
March 07

On Wednesday, 6 March 2024 at 17:32:17 UTC, H. S. Teoh wrote:

>

On Wed, Mar 06, 2024 at 04:47:02PM +0000, Steven Schveighoffer via Digitalmars-d wrote: [...]

>

The only tricky aspect is ranges that are references (classes/pointers). Neither of those to me should be supported IMO, you can always wrap such a thing in a range harness.
[...]

Every time this topic comes up, class-based ranges become the whipping boy of range design woes. Actually, they serve an extremely important role: type erasure, which is critical when you have code like this:

As Jonathan already responded, you can have a class range by wrapping it in a struct. Which is what I meant when I said:

"you can always wrap such a thing in a range harness"

-Steve