Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

also in .Net but then you enter a generic hell and you start thinking vanilla es6 is better


C# is getting support for generic math in 11 with the addition of static abstract members (including operators) for interfaces.


AFAICT this is basically how numbers work in Haskell, and I see no particular problems with them. You usually don't need too many number types of different nature.


Generics are great when you need them, but they will cut you badly if you misuse them.

Generics are viral. When you make something generic, you often have to make the things that touch or contain it generic, too.

Generics also create tight coupling. When you change the definition of a generic interface/class, you'll need to update your usage across the codebase. As opposed to, say, adding a new field to a class, that can be safely ignored anywhere it isn't used.

When this component you're updating is highly connected to other parts of your code, perhaps add another generic parameter to it, it completely explodes and you have to jump all around your codebase adding generics.

The kicker is that you may be updating components which are themselves generic and highly connected, setting off secondary explosions. Pretty soon you're throwing that codebase out, starting over, and swearing to yourself that you'll never touch generics again.

My advice is to assume generics are a premature abstraction until you've exhausted what you can do with more concrete approaches.


>Generics are viral. When you make something generic, you often have to make the things that touch or contain it generic, too

Do you have an example of that? Can't you always "typedef" any particular generic type as a concrete type and work with that going forward?

>Generics also create tight coupling. When you change the definition of a generic interface/class, you'll need to update your usage across the codebase. As opposed to, say, adding a new field to a class, that can be safely ignored anywhere it isn't used.

I don't see how this is different from concrete types or interfaces. If you change a public API you may have to update callers. If you change internals you don't have to update callers. Perhaps you can show an example to clarify what you mean.

I'm not a huge fan of generics myself, but I think your claim is that generics force you to introduce unnecessary dependencies. I don't see how this is true on a logical level. Dependencies between compiled artifacts are a different matter, but that's an implementation issue and I don't think it's what you're talking about.


https://github.com/dotnet/csharplang/issues/1328

> Have you ever wanted an interface (or abstract type) to be parameterized on a polymorphic variable to its implementer, but not its users? Have you ever wanted to parameterize over internal state, without revealing what that state contains to your users? This is what existential types allow you to do.


I mentioned in a sibling comment, I just don't wanna write an example, sorry.

You can't typedef, or populate the generic in any way, until you've reached a point in your code where you have sufficient information to know what to populate it with. This can often be further from the point of the initial change than you might like, as I described. (ETA: I didn't realize viral implied that you can _never_ concretize, I only meant there's a tendency to pass it up the chain.)

Changing public APIs will cause secondary changes. Generics are coupled more tightly than some other kinds of changes. For example adding a parameter to a method in an interface does force you to update all those implementations, and then all the usages of those implementations, which is a lot of work and could potentially trigger a similar situation. But generally it doesn't bubble up as high. Because generics are more abstract, it's more difficult to populate that parameter - you're more likely to need to pass the buck by being generic over that yourself, which causes you to update more usages, etc.


Please show code then

I dont remember misusing C# generics even once and I struggle to see example of such a case


This is a reasonable request, but I don't really want to write a code example, that feels like a lot of work and I have 0 investment in changing anyone's mind about this (I didn't really expect this to be controversial - I was just sharing my experiences and expanding on Avlin67's comment about "generic hell"), but if you had a question I'd be happy to answer it.


The only example I can think of is the bifurcation non-generic collections vs. generic collections in the early days of C# when you had a non-generic collection and couldn't pass it to a method which expects a generic collection, but it was the consequence of the development history of C# and is not a problem of generics per se.


All of the things you said apply equally well to function parameters.


That's an interesting point. From a strictly theoretical perspective you're right. I think it's that generics are more abstract and have a higher blast radius. Eg, you add an argument to a method, you need to update usage of that method. You add a generic to a class, you need to update everywhere that class is used.

The fact that arguments are less abstract also I believe tends to prevent them from bubbling all the way to the top. Generics can often only be populated at the top-level usage. Function arguments I find don't usually bubble up that far.


> you add an argument to a method, you need to update usage of that method. You add a generic to a class, you need to update everywhere that class is used.

You're talking about two different things here, though. What if you add a generic parameter to a function? It'll often be inferred at existing call sites, but worst case, same as any other change to a function's signature.


The reason I'm relating them is that they're both changes to a class, that have different blast radii and different levels of abstraction.

In the worst case, adding a nongeneric parameter to a function could have the same impact. I've never heard of that happening though. I'm trying to express how I've observed things working in practice, not the theoretical boundaries of what could happen here.

So lets say you add a concrete parameter to a method. You update the usages. Somewhere the output gets stored in an existing class. So you add a new field to this class of the correct type. You're done.

Let's say you do the same with a generic. Now when you add that field to that class, it also has to be generic over that type. Now you need to update all the places where that class was used.

If your code is overly generic throughout, the likelihood of this having secondary or tertiary effects and having a runaway refactor becomes pretty darn high.

Being too abstract will always get you in trouble, and it'll probably look pretty similar. I'm just saying it's very easy to do with generics and harder to do with less abstract techniques.


> In the worst case, adding a nongeneric parameter to a function could have the same impact. I've never heard of that happening though.

Can you clarify this? I'm reading it as "I've never heard of anyone adding a parameter to a function" and that's so far from my experience that I'm either misreading or you work in a vastly different field than I do.

> Now when you add that field to that class, it also has to be generic over that type. Now you need to update all the places where that class was used.

Only if you need that to be generic too. If you change an int to a T, and you want to preserve the existing behavior for existing callers, they just call it as f<int>() instead of f(). Languages with good type inference will do that for you without changing the calling code as written.


Apologies, I mean that, if you came up with some kind of pathologically bad architecture, it could encounter the same failure mode when trying to add a parameter to a function (like, the callers need to add a parameter, and their callers, etc). But as you note, adding parameters to a function is routine, and I've never heard of this happening. I've definitely added parameters in a way that was tiresome and required me to go higher up the chain of callers than I would have liked, but not in a way that spun out of control.

I'm not really sure what to say at this point really. I think we're miscommunicating somehow. Would you agree that if we are too abstract with our architecture, we'll end up with a brittle and difficult to maintain architecture?


I do agree with that, and with the implication that one shouldn't add generics (or other abstraction) where they don't provide enough value for their costs.

But I'm confused because your example (adding a generic parameter to a function) seems to be an example of adding abstraction to code that did not previously have enough abstraction.


Yeah for sure. If we need more abstraction we need it, generics are just really, really abstract. I'm just saying generics should be a last resort. And if you find yourself with generics all over the place, you might take a step back and ask, did I make a bad architectural decision that will blow up in my face later? Can I do a medium sized refactor now to save myself a massive refactor later?

Coming from Python, I had a bad habit of premature abstraction. In Python, it's easy to be very generic at very little cost (not necessarily using generics - they exist in Python, but they're not "real" since Python is gradually typed). I thought of keeping things generic as "designing for expansion". Then I encountered the problems I've been describing, small refactors would turn into giant ones, and it was entirely unsustainable.

When I asked for advice about this, what I got was pretty much, "Oh yeah, that'll happen. Just don't use generics if you can get away with it." Initially that felt like a nonanswer to me, even a brush off. But as I matured in Rust I realized the advice was spot on, and that I had been abstracting prematurely.

I've seen techniques that can use generics well and actually make coupling looser, and that's awesome, and I don't mean to suggest that one should never use generics. I acknowledge I got into trouble by _misusing_ them. I'm just saying it's an unwieldy tool for special situations. It will rapidly expend your complexity budget.

The original context I was responding to was something like, someone says, generics are great until you get in generic hell, and then someone was like, generics seem fine to me. And I just wanted to explain how one gets into generic hell.


I am now fairly sure we agree and I just didn't like your example.


This is not the case with traits/type class approach seen in Rust/Haskell.


I'm afraid that it most certainly is, as Rust is the only language I have ever used generics in, and I had this problem.

Lifetimes suffer from the exact same problem as well, since they're really an exotic form of generic.

That said, I will readily admit that it was a lack of skill on my part. From talking to people in the Rust community though, I gather this isn't an uncommon experience.


Lifetimes are viral in Rust because 1. you can't abstract over them, 2. affine types are viral by design, but generics themselves are anything but viral.


They're not viral in rust - you can always replace the generic with a concrete type in super types.


Well that's rather the point of being generic with any language, isn't it?

You can stop being generic when you have sufficient information to stop being generic. If you're writing a library, that can easily bubble up all the way to the top, because you may never have enough information.

ETA: I think I was using a definition of viral that wasn't entirely correct. I thought it was a casual term rather than a precise one. But it seems like you're saying something is viral if you _must_ pass it on. In which case I apologize, generics are "semiviral" (I'm trying to introduce this term - if it already exists & isn't this, I apologize) - you don't need to pass them on, there's just a tendency to. The result looks very similar.


Rust also has associated types, which are exposed to the implementer, but not to the consumer.


I mean, generics and associated types aren't equivalent, but they also are often exposed to the consumer. Eg, if you're accepting an Iterator, you'll want to populate the Item associated type.


How is that different from any other part of a function signature? It’s part of the type system, of course changes have to be accounted for at use sites. But not doing so would be just incorrect.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: