July 27, 2015
On Sunday, 26 July 2015 at 23:21:50 UTC, Walter Bright wrote:
> If the template constraint is 'isInputRange', and you pass it an 'InputRange' that is nothing beyond an input range, and it compiles, it is JUST AS GOOD as Rust traits, without needing to add 'isInputRange' to every template up the call tree.
>
> In fact, I do just this in my D dev projects. I have a set of mock ranges that match each of the range types.
>

That is not enough, it has been covered. It is clear that this is what needs to be done for good testing. It is also clear and explained in this thread that this is not enough.

>> The problem is the exact same as for dynamic typing and unitests. A dynamically
>> typed function that expect a string can be test exhaustively with warious string
>> passed as arguments. Still, none knows what happen when passed an int, float,
>> array, object or whatever. Worse, it is either going to blow up in some
>> unexpected way or not explode and do random stuff.
>
> Flatly no, it is not at all the same. Dynamic typing systems do not have constraints. Furthermore, dynamic typed languages tend to do random s**t when presented with the wrong type rather than fail. (Such as concatenating strings when the code was intended to do an arithmetic add.) D does not, it presents the user with a compilation error.
>

That is blatantly false and show more ignorance than anything else. For the precise example, concatenate with + is know to be bad in dynamic typing for this very reason. Even PHP got around that trap and pretty much only javascript got into that road. You'll find noone defneding this outside the Node.js crowd, but once you start thinking that a monothreaded event loop is the definition of scalable, you are already lost for science anyway.

But more generally there is a point. Dynamic languages tends to do random shit rather than failing. Hopefully, some won't. For instance python will fail hard rather than do random stuff when passed the wrong type.

If that was the problem faced, then using a type system would be pointless. Using something like python is sufficient.

>> The same way, instantiate your template with something it doesn't expect and you
>> get absurdly complex errors (and it is getting worse as phobos get more and more
>> "enterprise" with Foo forwarding to FooImpl and 25 different helpers).
>
> This is incorrect. In my D project development, when I send the wrong thing, I get a list of the template instantiation stack.

Which is often 3 pages long.

> The bottom gives the method not found, and the stack gives how it got there. I find it adequate in that it doesn't take much effort to figure out where things went wrong.
>

That is learned helplessness. This is bad and we should feel bad to propose such a user experience. There are reasons why template are frowned upon by many devs, and this is one of them.

I even heard Andrei suggest at a conf to pipe the output of the compiler to head (he was talking about C++, but D suffer from the same problem in that regard). Yes it works, and yes you can eventually make sens of the error, but that is terrible user experience.

> BTW, when you discover that a constraint is wrong on a Phobos template, please file a bug report on it.
>

It is not just phobos, but everybody's code. Why all this need to be done manually when the compiler could do it for us ?

Isn't it why we use computer in the first place ?

If I follow your post, one have to maintain a set of minimal mock to test instantiations, a constraint for the template and a body, the 3 of them requiring to be kept in synch over time while nothing checks it is.

That is completely unmaintainable.

>> The problem is the same, will it fail at call site/instanciation site with "I
>> expected X you gave me Y" or will it fail randomly somewhere down the road in
>> some unrelated part of the code, or worse, not fail at all when it should have ?
>
> If the constraint is InputRange, and the body assumes ForwardRange, and I pass it a ForwardRange, and it works, how is that 'worse'?

Maybe now my complexity is quadratic ? Maybe it doesn't do what it is supposed to do anymore, but else ?

July 27, 2015
"Jonathan M Davis" <jmdavisProg@gmx.com> wrote:
> On a related note, while I'd noticed it on some level, I don't think that it had ever clicked for me how restrictive interfaces are before this discussion. The simple fact that you can't ask for two of them at once really reduces how reusable your code can be. So, templatizing those checks rather than using interfaces is huge. And DbI is an extension of that. There's likely a lot of unplumbed depth there.

One big improvement of traits over interfaces is, that you can implement traits for types after they are defined, even for types that you didn't define yourself.

So in Rust, if your function needs two unrelated interfaces (trait objects == dynamic polymorphism) A and B, you can easily define a new trait C that depends on A and B and implement C for all types that also implement A and B:

trait A {...}
trait B {...}

trait C : A,B { }

impl<T: A+B> C for T { }

fn myFunction(c: C) {...}

For generics you don't even need that:

fn myFunction<T: A+B>(t: T) {...}

Tobi
July 27, 2015
On 7/26/2015 12:51 AM, Alix Pexton wrote:
> On 25/07/2015 9:48 PM, Walter Bright wrote:
>
>> Unfortunately, Bruce Eckel's seminal article on it
>> http://www.mindview.net/Etc/Discussions/CheckedExceptions has
>> disappeared. Eckel is not a Java code monkey, he wrote the book Thinking
>> In Java
>> http://www.amazon.com/gp/product/0131002872/
>>
>
>
> https://web.archive.org/web/20150515072240/http://www.mindview.net/Etc/Discussions/CheckedExceptions
>

That's it. Thanks for finding the link.
July 27, 2015
On Monday, 27 July 2015 at 19:11:53 UTC, deadalnix wrote:
>
> That is completely unmaintainable.
>

I really don't get how the mess of unittests, mock data types, template constraints, and type interfaces that are just convention(ranges are just a convention, they don't exist anywhere) is supposed to some how be more maintainable than a single system that can serve the function of all of them and forces them all to be in sync 100% of the time. Seriously...
July 27, 2015
On Monday, 27 July 2015 at 01:59:49 UTC, Jonathan M Davis wrote:
> On Sunday, 26 July 2015 at 22:59:09 UTC, deadalnix wrote:
>> On Sunday, 26 July 2015 at 03:42:22 UTC, Walter Bright wrote:
>>> On 7/25/2015 3:28 PM, deadalnix wrote:
>>>> Also, argument from ignorance is hard to maintain when the thread is an actual
>>>> feedback from experience.
>>>
>>> You say that interfaces do the same thing. So please show how it's done with the example I gave:
>>>
>>>     int foo(T: hasPrefix)(T t) {
>>>        t.prefix();    // ok
>>>        bar(t);        // error, hasColor was not specified for T
>>>     }
>>>
>>>     void bar(T: hasColor)(T t) {
>>>        t.color();
>>>     }
>>
>> I'm not sure what is the problem here.
>
> The problem here is that if you're dealing with traits or concepts or whatever that are essentially interfaces, and foo uses the Prefix interface/trait/concept and then wants to use bar which has the Color interface/trait/concept, it has to then require that what it's given implements both the Prefix and Color interfaces/traits/concepts.
>

So, if I translate to regular D, here is what I get :

int foo(T)(T t) if (hasPrefix!T) {
    t.prefix();    // ok
    bar(t);        // ok, nothign is checked
}

void bar(T)(T t) if (hasColor!T) {
    t.color(); // error, color is not specified on an object of type XXX
}

It changes nothing.

> In the case of actual interfaces, this really doesn't work well, because you're forced to basically have an interface that's derived from both - e.g. PrefixAndColor. Otherwise, you'd be forced to do nonsense like have foo take a Prefix and then cast it to Color to pass to bar and throw an exception if the type it was given didn't actually implement both interfaces. And it's downright ugly. In reality, the code would likely simply end up not being that generic and would require some specific type that you were using in your code which implemented both Prefix and Color, and foo just wouldn't work with anything else, making it far less reusable. So, with actual interfaces, it becomes very difficult to write generic code.
>

We do not have to make the same limitation (especially if trait are implicitly implemented).

Still, even with this limitation, using type is considered superior and using unitests is not considered to be not sufficient.

Also, your message is kind of weird as you seem to assume in that part that what is discussed here is the same as passing argument, when you get back to the checked exception position lower.

> With traits or concepts, presumably, you could say that foo required a type that implemented both Prefix and Color, which fixes the problem of how you're able to accept something that takes both generically without coming up with something like PrefixAndColor (though if you can only list one trait/concept as required, you're just as screwed as you are with interfaces). But even if you can list multiple traits/concepts as required by a function, you quickly end up with a proliferation of traits/concepts that need to be covered by a higher level function like foo, because not only would foo need to list all of the traits/concepts that the functions it uses directly require, but it has to list all of the traits/concepts that it even indirectly requires (potentially from far down the call stack). So, any change to a function that even gets called indirectly could break foo, because it didn't have the right traits/concepts listed in its requirements. And all of the functions in the call chain would have to have their list of required traits/concepts updated any time there was any tweak to any of the underlying functions, even if those functions would have actually worked fine with most of the code that was calling them, because the types being passed in had the new requirements already (it's just that the functions higher up the stack didn't list the updated requirements yet).
>
> By having foo list all of the traits/concepts that would be required anywhere in its call stack, you're doing something very similar to what happens with checked exceptions where the exceptions have to be listed clear up the chain. It's not quite the same, and there's no equivalent to "throws Exception" (since that would be equivalent to somehow having a trait/concept that said that you didn't care what the type given to foo implemented). Rather, you're basically being forced to list each trait/concept individually up the chain - but it's still a similar problem to checked exceptions. It doesn't scale well. And if a function is required to list all of the traits/concepts that are required - even indirectly - then changing the requirements of a function - even slightly - results in code breakage similar to that of checked exceptions when you change which exceptions a function throws, and "throws Exception" wasn't being used. And even if you're not worried about breaking other people's code, it's a maintenance problem to maintain that list clear up the chain.
>

I'm doing something similar to checked exception. Yes. I'm passing argument down. There are similar indeed, and this is why people though checked exception were a good idea.

The main way in which the differs is that checked Exception expose implementation, while typed argument provide a contract for the caller.

Consider it that way. Template are a meta language within the language. With template you write code that write code.

At this meta level, types are values. Instantiating a template is the same as calling a function (function that will generate code). That is the reason why static if works so well, and that is the reason why static foreach is asked for. This is the reason why Andrei's approach to present compile time arguments in TDPL rather than templates works.

For some reason, unitests cannot replace types for code that connect to database, database code itself, code that render pages, code that do scientific computation, code that do rendering, code that crunches numbers, code that do GUI, code for command line utilities, code that do whatever, but for code that wirte code, yeah, they trully do the trick !

> Unfortunately, we _do_ have a similar problem with template constraints if we insist on putting all of the function's requirements in its template constraint rather than just its immediate requirements. But at least with template constraints, if the top-level constraint is missing something that a function somewhere deeper in the stack requires, then you get a compilation error only when the type being passed in doesn't pass the constraint on the function deeper in the stack. So, if you adjust a template constraint, it will only break code that doesn't work with the new constraint - even code that uses that function indirectly (possibly even quite deeply in a call stack, far from their own code) won't break due to the change, unless the type being used doesn't pass the new constraint. And when it does fail, the errors may not be pretty, but they do tell you exactly what's required to figure out what's wrong when you look at the source code. Whereas the traits/concepts solution would break _all_ code that used the function that was adjusted (even indirectly), not just the code that wouldn't work with the new requirements.
>
> I discussed this quite a bit more elsewhere in this thread: http://forum.dlang.org/post/lsxidsyweczhojoucnsw@forum.dlang.org
>
> - Jonathan M Davis

Will read that soon.
July 27, 2015
On 07/27/2015 01:29 AM, Walter Bright wrote:
> On 7/26/2015 3:44 PM, deadalnix wrote:
>> or template code (which will blow up at instanciation time, or worse,
>> do random
>> shit).
>
> Um, all Rust traits do is test for a method signature match, so it
> compiles. It is NOT a defense against a random method that just happens
> to match and does some other unrelated random shit.

You are describing Go interfaces, not Rust traits.
July 27, 2015
On 7/27/15 3:35 PM, Tofu Ninja wrote:
> On Monday, 27 July 2015 at 19:11:53 UTC, deadalnix wrote:
>>
>> That is completely unmaintainable.
>>
>
> I really don't get how the mess of unittests, mock data types, template
> constraints, and type interfaces that are just convention(ranges are
> just a convention, they don't exist anywhere) is supposed to some how be
> more maintainable than a single system that can serve the function of
> all of them and forces them all to be in sync 100% of the time.
> Seriously...

Clearly adding more language features to D would have certain payoff. But we should not just go ahead and pull today's hot topic, which really does change very often.

As the language's main proponents, we should be more inclined toward using the language that we have creatively for solving interesting tasks, instead of keeping on wishing that just one more feature would make it perfect. The lure of the eternally-open design stage is very powerful (I basked in its glow a number of times), but it inevitably becomes an adversary to productivity - instead of getting work done, there's always contemplating how the language design could be tweaked to better accommodate the task at hand.

The withdrawal is unpleasant, I know. But we must acknowledge that the design of D is done. The large stones and pillars we have in place are there to stay, and D shall not be radically different five years from now. We need to acknowledge that a lot of grass seems greener on the other side (and some really is), but we need to make do with the gardening tools we got.

I'll do my best to limit my participation in emotional debates, and suggest other D luminaries to do the same. We should put strong emphasis on finalizing the definition and implementation of the language we have (those imprecisions and fuzzy corners are really counterproductive), forge forward to do great work with D, and show others how it's done.


Thanks,

Andrei

July 27, 2015
On Monday, 27 July 2015 at 19:21:10 UTC, Tobias Müller wrote:
> trait A {...}
> trait B {...}
>
> trait C : A,B { }
>
> impl<T: A+B> C for T { }
>
> fn myFunction(c: C) {...}
>
> Tobi

Has to be:
    fn my_function(c: &C) { ... }
actually, because trait objects can only be passed by reference/borrowed-pointer.
July 27, 2015
On 7/27/2015 12:53 PM, deadalnix wrote:
> So, if I translate to regular D, here is what I get :

I asked how you'd solve the problem with interfaces.

July 27, 2015
On Monday, 27 July 2015 at 19:21:10 UTC, Tobias Müller wrote:
> "Jonathan M Davis" <jmdavisProg@gmx.com> wrote:
>> On a related note, while I'd noticed it on some level, I don't think that it had ever clicked for me how restrictive interfaces are before this discussion. The simple fact that you can't ask for two of them at once really reduces how reusable your code can be. So, templatizing those checks rather than using interfaces is huge. And DbI is an extension of that. There's likely a lot of unplumbed depth there.
>
> One big improvement of traits over interfaces is, that you can implement traits for types after they are defined, even for types that you didn't define yourself.
>
> So in Rust, if your function needs two unrelated interfaces (trait objects == dynamic polymorphism) A and B, you can easily define a new trait C that depends on A and B and implement C for all types that also implement A and B:
>
> trait A {...}
> trait B {...}
>
> trait C : A,B { }

How is that any different from interfaces? You can do exactly the same thing with them.

> impl<T: A+B> C for T { }
>
> fn myFunction(c: C) {...}
>
> For generics you don't even need that:
>
> fn myFunction<T: A+B>(t: T) {...}

As long as you can list the two interfaces/traits/concepts that you require separately, then you're okay. But as soon as you have to create a new one that combines two or more interfaces/traits/concepts, then that doesn't scale. interfaces force that. I wouldn't expect traits or concepts to, because they're compile-time constructs, but that would depend on how the language defines them.

It might make sense to create combined traits/concepts for the cases where the operations in question are often going to be required together, but in general, it's going to scale far better to require them separately rather than require a combined trait/concept. Otherwise, you get a combinatorial explosion of traits/concepts as you combine them to create new traits/concepts - either that, or you code doesn't end up being very generic, because it's frequently using traits/concepts that require more operations than it actually uses, meaning that it will work with fewer types than it would otherwise.

In general, templates shouldn't be requiring more operations than they actually use, or they won't be as reusable as they could/should be. And that implies that the list of required operations should be kept to what's actually required rather than using traits/concepts that require those operations plus others.

- Jonathan M Davis