May 12, 2021
On Wednesday, 12 May 2021 at 23:31:21 UTC, Paul Backus wrote:
> This *does* work as expected: https://run.dlang.io/is/Ru9phk
>
> The issue with `format` is that it takes an alias parameter, not a value parameter--and the reason it does *that* is to support string, wstring, and dstring with a single overload.

Yes, so we are getting at the root of this.

I know these thing work, this is why I stated that SomeEnumString is a subtype of string to begin with, it has all the properties. If that wasn't working, then I would have been mistaken when making such assertions.

It is working in the simple case, it is expected to work from the caller's standpoint due to the LSP, but it doesn't work in practice due to some obscure implementation detail that is of little concern to the user.

Pushing this on the user is not the way to go.

If the library writer desire to bundle string/dstring/wstring in the same implementation, this doesn't change the fact that it ought to work with subtypes. Choosing to break this is what "flies in the face of the LSP".

I would also like to see people think what make respecting the LSP challenging in such case, and see what can be done at a systemic level. It's kind of a bummer that the path of least resistance is to break the LSP when going for more genericity in another dimension.
May 13, 2021
On Wednesday, 12 May 2021 at 23:42:11 UTC, deadalnix wrote:
> It is working in the simple case, it is expected to work from the caller's standpoint due to the LSP, but it doesn't work in practice due to some obscure implementation detail that is of little concern to the user.
>
> Pushing this on the user is not the way to go.
>
> If the library writer desire to bundle string/dstring/wstring in the same implementation, this doesn't change the fact that it ought to work with subtypes. Choosing to break this is what "flies in the face of the LSP".

Well, no, it doesn't--because, again, the LSP doesn't apply here in the first place, and never has. Flies in the face of user expectations, perhaps--though even then, if the user looks at the documentation and see `isSomeString!(typeof(fmt))`, is it really reasonable for them to expect that a non-string type will be accepted?

I think it's a reasonable API design decision to support any type that implicitly converts to a string type, but it's not the *only* reasonable decision, and we ought to acknowledge the costs as well as the benefits.

Personally, my inclination is to err on the side of making the standard library a little more complex so that user code can be simpler, but Andrei makes a convincing argument that this tendency has gotten us into trouble before [1]. How do we decide where to draw the line? There has to be some principle here beyond just "users expect it" and "respect the LSP."

[1] https://forum.dlang.org/thread/q6plhj$1l9$1@digitalmars.com

> I would also like to see people think what make respecting the LSP challenging in such case, and see what can be done at a systemic level. It's kind of a bummer that the path of least resistance is to break the LSP when going for more genericity in another dimension.

IMO this is all downstream of D's choice to use untyped templates as opposed to typed generics (a tradeoff that goes all the way back to Lisp vs. ML). It's a fun thought experiment to imagine a version of D that took the other path, but there's not much we can do about it now.
May 12, 2021
On 5/12/21 6:00 PM, Paul Backus wrote:
> On Monday, 10 May 2021 at 21:55:54 UTC, deadalnix wrote:
>> If you think that invalidate the LSP, I'm afraid there is a big misunderstanding about the LSP. Not all operation on a subtype have to return said subtype. It is made clearer if you consider the slicing operationa s a member function on an object instead - as I seems classes and inheritance is the only way OPP is understood these days.
>>
>> class A {
>>    A slice(int start, int end) { ... }
>> }
>>
>> class B : A {}
>>
>> Where is it implied that B's version of the slice operation must return an A? Nowhere, the LSP absolutely doesn't mandate that. It mandate that you can pass a B to something that expects an A, and that thing will behave the way you'd expect.
>>
>> And it does!
>>
>> If your code needs an A, then you mark it as accepting an A as input. If I have a B and want to pass it to your code, I can too, transparently. You do not need to even know about the existence of B when your wrote your code. This is what the LSP is at its core.
>>
>> Back to our string example, the code should accept string (A), with zero knowledge of the existence of any enum string (B). You should be able to pass a B to that code and have everything work as expected.
> 
> I concede the points that enum strings do not violate the LSP, and that they are subtypes of string. You're right, and I was wrong.

I was all over run.dlang.org like "Sure that's not going to work... wait a second, it does! But that other thing's not going to work... what, that works too!" I didn't know D's enums are _that_ odd.

It seems you can do almost everything with an enum that you can do with its base type. Keyword being "almost". For example,

x ~= "asd";

works whether x is a string or an enum based on string. However,

x = x ~ "asd";

works if x is a string and does not work if x is an enum derived from string. Therefore, a function using that expression works for strings but not for enum strings.

Similarly:

x += 3;

works for int and enums derived from int. However,

x = x + 3;

does not. So you can't transparently substitute enums for their base type. I suspect there'd be other cases, too.
May 13, 2021
On Thursday, 13 May 2021 at 01:16:42 UTC, Andrei Alexandrescu wrote:
> It seems you can do almost everything with an enum that you can do with its base type. Keyword being "almost". For example,
>
> x ~= "asd";
>
> works whether x is a string or an enum based on string. However,
>
> x = x ~ "asd";
>
> works if x is a string and does not work if x is an enum derived from string. Therefore, a function using that expression works for strings but not for enum strings.

A template function, you mean? Because (as the rest of the post you quoted demonstrates) the LSP does not and has never applied (in D) to substitutions that involve different instantiations of the same template. If you explicitly instantiate `func!string`, then it will work exactly as the LSP dictates, but if you substitute `func!string(x)` with `func!E(x)`, you have no guarantee.

Granted, the fact that `x ~= "asd"` works and `x = x ~ "asd"` doesn't is definitely a bug.
May 12, 2021
On Wednesday, May 12, 2021 7:16:42 PM MDT Andrei Alexandrescu via Digitalmars- d wrote:
> I was all over run.dlang.org like "Sure that's not going to work... wait a second, it does! But that other thing's not going to work... what, that works too!" I didn't know D's enums are _that_ odd.
>
> It seems you can do almost everything with an enum that you can do with its base type. Keyword being "almost".

Yeah, if enums are supposed to only have a fixed set of values, then they're completely broken. The language does almost nothing to guarantee it. One result of that is that you have to be _very_ careful about how you use something like final switch - especially since it's not checked with -release.

Of course, if enums are just named values without caring about whether it's possible to have an enum with a different value than the ones listed, then the fact that the enum is even treated differently from the base type causes other problems. So, ultimately, I think that D enums are pretty schizophrenic and not particularly well-designed.

I've argued in the past that the language should disallow all operations on enums (aside from casts) which aren't guaranteed to result in a valid value for that enum type, but not everyone agrees with that stance.

- Jonathan M Davis



May 12, 2021
On Wednesday, May 12, 2021 8:13:04 PM MDT Jonathan M Davis via Digitalmars-d wrote:
> I've argued in the past that the language should disallow all operations on enums (aside from casts) which aren't guaranteed to result in a valid value for that enum type, but not everyone agrees with that stance.

Or more accurately, all operations on an enum which are not guaranteed to result in a valid enum value should result in the base type (and thus not be assignable to a variable of that enum type without a cast), and operations which mutate the enum should not be allowed unless they're guaranteed to result in a valid enum value. But regardless, the point is that ideally, unless a cast is used, it should be impossible to have something typed as an enum without it being guaranteed that the value be one of the enumerated values for that enum type. But that's definitely not how D enums work...

- Jonathan M Davis



May 13, 2021
On 5/12/21 9:41 PM, Paul Backus wrote:
> On Thursday, 13 May 2021 at 01:16:42 UTC, Andrei Alexandrescu wrote:
>> It seems you can do almost everything with an enum that you can do with its base type. Keyword being "almost". For example,
>>
>> x ~= "asd";
>>
>> works whether x is a string or an enum based on string. However,
>>
>> x = x ~ "asd";
>>
>> works if x is a string and does not work if x is an enum derived from string. Therefore, a function using that expression works for strings but not for enum strings.
> 
> A template function, you mean? Because (as the rest of the post you quoted demonstrates) the LSP does not and has never applied (in D) to substitutions that involve different instantiations of the same template. If you explicitly instantiate `func!string`, then it will work exactly as the LSP dictates, but if you substitute `func!string(x)` with `func!E(x)`, you have no guarantee.
> 
> Granted, the fact that `x ~= "asd"` works and `x = x ~ "asd"` doesn't is definitely a bug.

Well the problem is that the choice of covariance of results for operations on enums vs their "base" is quite arbitrary.

For strings, the result of "~" is not covariant but the result of "~=" is - not only it works, but it returns a reference to the enum type, not the base type.

However, for enums derived from integrals the result of "+" is not covariant when adding an enum with an integral, but covariant when two enums are added together. Same goes for "-", "/", "*", but oddly not for "^^". I suspect nobody thought of trying to raise an enum to the power of an enum.

The plot thickens when considering enums derived from user-defined types:

void main() {
    import std;
    struct S {
        void fun() { writefln("%s", &this); }
        int min = -1;
    }
    enum X : S { x = S() }
    X x;
    x.fun;
    (cast(S*) &x).fun;
    writeln(x.min);
}

The two addresses are the same, meaning the enum value gets to call the base member's function, in a subtyping manner. However, the last line doesn't compile, which breaks subtyping.

On the face of it, enums are defined by the language, so whatever choices are made are... there. I understand the practicality of some choices, but overall the entire enum algebra is quirky and difficult to maneuver around in generic code. Which harkens back to the opener of this thread - Phobos should not go out of its way to support enumerated types everywhere, when a trivial recourse exists on the caller side - pass value.representation instead of value.

A much stronger argument could be made against supporting convertibility (to e.g. strings or ranges) by means of alias this. Callers should convert to the needed type prior to calling into the standard library.
May 13, 2021
On Thursday, 13 May 2021 at 02:43:41 UTC, Jonathan M Davis wrote:
> On Wednesday, May 12, 2021 8:13:04 PM MDT Jonathan M Davis via Digitalmars-d wrote:
> Or more accurately, all operations on an enum which are not guaranteed to result in a valid enum value should result in the base type (and thus not be assignable to a variable of that enum type without a cast), and operations which mutate the enum should not be allowed unless they're guaranteed to result in a valid enum value. But regardless, the point is that ideally, unless a cast is used, it should be impossible to have something typed as an enum without it being guaranteed that the value be one of the enumerated values for that enum type. But that's definitely not how D enums work...
>
> - Jonathan M Davis


So basically enum should implicitly be declared to be immutable right?
May 13, 2021
On Thursday, 13 May 2021 at 01:03:19 UTC, Paul Backus wrote:
> On Wednesday, 12 May 2021 at 23:42:11 UTC, deadalnix wrote:
>> It is working in the simple case, it is expected to work from the caller's standpoint due to the LSP, but it doesn't work in practice due to some obscure implementation detail that is of little concern to the user.
>>
>> Pushing this on the user is not the way to go.
>>
>> If the library writer desire to bundle string/dstring/wstring in the same implementation, this doesn't change the fact that it ought to work with subtypes. Choosing to break this is what "flies in the face of the LSP".
>
> Well, no, it doesn't--because, again, the LSP doesn't apply here in the first place, and never has. Flies in the face of user expectations, perhaps--though even then, if the user looks at the documentation and see `isSomeString!(typeof(fmt))`, is it really reasonable for them to expect that a non-string type will be accepted?
>
> I think it's a reasonable API design decision to support any type that implicitly converts to a string type, but it's not the *only* reasonable decision, and we ought to acknowledge the costs as well as the benefits.
>
> Personally, my inclination is to err on the side of making the standard library a little more complex so that user code can be simpler, but Andrei makes a convincing argument that this tendency has gotten us into trouble before [1]. How do we decide where to draw the line? There has to be some principle here beyond just "users expect it" and "respect the LSP."
>

While what you say is correct, I'm not convinced it is right.

We established before that effectively, we should expect the LSP to hold when values are passed down, but not when types are. Which i think we both agree is the reasonable thing to do here, because B being a subtype of A doesn't say anything about meta_typeof(B) being a subtype of meta_typeof(A), and therefore there is no expectation that the LSP holds.

So it is correct to assert that if format takes the type as a parameter, then there is no expectation that the LSP holds. It is also correct to say that the documentation describes things accurately.

But I strongly disagree with the fact that it is right.

To use an analogy, I could make a car where the gaz and break pedal are swapped, and explain as much in the user manual, yet, I fully expect people would crash such cars at a higher rate than the alternative.

In the case of format, we need to ask ourselves what does the user expect, to pass a value down or to pass a type (plus possibly a value) down? Because if it the first, then it reasonable from the user standpoint that the LSP works and if it is the second, then there isn't such an expectation.

The fact that we see people trying to do format!SomeEnumString , but not something like format!42 provides a good answer to that question. Format's parameter is expected to be a string, not any random type. And if that is the case, then it is reasonable to expect to LSP to hold.

Now, the matter of cost is an interesting one. But I argue that doing what the user expect ought to be cheap, if not the cheapest option available. This is simply the difference between a language that helps its users and a language that gets in the way. So if the cost is high, then we need to consider this high cost a serious problem to solve.

May 13, 2021
On Thursday, 13 May 2021 at 02:43:41 UTC, Jonathan M Davis wrote:
> On Wednesday, May 12, 2021 8:13:04 PM MDT Jonathan M Davis via Digitalmars-d wrote:
>> I've argued in the past that the language should disallow all operations on enums (aside from casts) which aren't guaranteed to result in a valid value for that enum type, but not everyone agrees with that stance.
>
> Or more accurately, all operations on an enum which are not guaranteed to result in a valid enum value should result in the base type (and thus not be assignable to a variable of that enum type without a cast), and operations which mutate the enum should not be allowed unless they're guaranteed to result in a valid enum value. But regardless, the point is that ideally, unless a cast is used, it should be impossible to have something typed as an enum without it being guaranteed that the value be one of the enumerated values for that enum type. But that's definitely not how D enums work...
>
> - Jonathan M Davis

YES!
7 8 9 10 11 12 13 14 15 16 17
Next ›   Last »