Hacker Timesnew | past | comments | ask | show | jobs | submit | throwaway234232's commentslogin


This thread is just about pip freeze not including hashes, and while it's true that would provide a lot of reassurance, you can still pin transitive version dependencies w/o them.


You don't need virtual environments when using npm or composer or cargo.

They also create lock files with hashes by default.

pip still doesn't use lock files. It's subpar compared to other ecosystems.


I worry that the inclusion of lockfiles will make semver less relevant. You shouldn't need to use lock files if you're using semver properly.

If your ecosystem is healthy than pinning exact versions with lock files shouldn't be done. Making it so that every dev uses the latest patch or minor when they run your program.

Using lock files for libraries should absolutely never happen, you should at most fix your dependencies to the latest major version.


> You shouldn't need to use lock files if you're using semver properly.

I think this issue is unrelated to semver.

I've had so many issues over the years with Python packages, even very polished libraries like Flask and Celery.

For example, you could install something like Flask 2.0 but end up with a major difference in versions of its sub-dependencies depending on when you installed it.

That's because Flask has its own dependencies defined like this:

        "Werkzeug >= 2.2.0a1",
        "Jinja2 >= 3.0",
        "itsdangerous >= 2.0",
        "click >= 8.0",
The above means installing Flask 2.0 could one day in the future install Jinja 4 or Click 10 unless you lock your entire dependency tree.

I've also had all sorts of things break because Flask installed Jinja 3.1 which wasn't a problem 6 months ago when Jinja 3.0 was the latest release. I've also had cases where installing a specific version of Celery in the past worked but failed in the future because it didn't lock one of its sub-dependencies down well enough (vine) which caused a breaking change.

This stuff happens all the time and it's a nuisance. I never experienced issues like this with Ruby and other languages that have the idea of a lock file built into their package manager. IMO it's a desperately needed feature that should be built into pip.


Semver promises are insufficient in a world where supply chain attacks are increasingly common. Pulling untested and invalidated code in at every project build is how that transitive dependency on a package that was taken over for a small window wrecks your development team. You should never be pulling in new code by surprise, it should always be something I’m aware of and signing up for.

The Rust ecosystem is good evidence that locking doesn’t kill semver. Semver is still widely used and has all of its meaning.


> You shouldn't need to use lock files if you're using semver properly.

You mean "if all your dependencies are using semver properly". Yes, you're right, and you've found the problem.


I've experienced multiple minor and patch (according to semver) updates that broke APIs and behavior, and I'd guess most devs have as well.

I think semver makes sense to humans. I can derive a lot of meaning from it when I see n.n.n. But when it comes to the software supply chain, it's just too rickety. Frankly, when you lose customers after a new deploy broke one of your dependencies, "but the dependency author didn't respect semver" isn't an excuse.

I say this as someone who strongly pushed `dependency>=1.3.2,<1.4` until that happened to me. My argument was "security updates", and now I just don't care. The software supply chain is too chaotic, and you have to be defensive against it.


I agree, but they also include hashes.


The difference between virtual environments in Python and node_modules in JavaScript is minor I would say. It’s the same concept. I believe composer has something similar. It’s a directory where packages go and they are not part of the system wide installation.

I agree that lock files are useful and it’s a pity that pip does not offer them.


Your example doesn't help your argument here, as square should be taking `T` instead of `Option<T>`, a good use case for `Option::map`.

But if you are speaking about ergonomics of handling Options, it is being improved upon with the usage of try operators with Options as well as possibly seeing try expressions in the near future.

I don't think lack of ergonomics is a good justification for usage of unwrap, but that is entirely subjective. However, I do see the misappropriation of `unwrap` as a signal that the ergonomics in Rust are lacking and it's something that needs to be addressed, so again I do sympathize.


This is a good use case for let-else, however I won't agree with you here on usage of unwrap for the sake of brevity. I will opt for `?` or match instead.


The footgun here is that `unwrap` may be used inappropriately, and from what I see using github code search it's used inappropriately quite often.


So is `panic`, or the subtraction operator on unsigned values which may cause underflow.


You can still use a null, however the point being made here is that null was failed to be included at type level.

See these for reference

* https://kotlinlang.org/docs/null-safety.html

* https://dart.dev/null-safety

* https://docs.microsoft.com/en-us/dotnet/csharp/nullable-refe...

An orthogonal approach would be using a data type such as Maybe or Option, but ergonomics depends on language.


I've ran into this before, but to be fair I was using Monad Transformers.


> Usually people recommend crates designed to make the experience better but that's a failure in my opinion.

I agree, I love Rust, but I'd like to see error handling ergonomics improved.

I'd like to see error handling become more like Zig or Swift.

Until then, I cope with the `anyhow` and `thiserror` crate :|


I guess I have the Blub effect. Coming from C++ and C#, Rust has my favorite error handling so far.


> I would think the Err(err) => return Err(err) line needlessly constructs a copy of result

It sounds like this is coming from a C++ bias? So please forgive me if this is wrong.

Rust, in my experience, favors move semantics first, then copy semantics after.

I know in C++, we had implicit copy constructors, with move semantics after with rvalue references, where you need to use `std::move` in a lot of cases.

So what helps, in my opinion, is to think of Rust as using `std::move` as a default.


> So what helps, in my opinion, is to think of Rust as using `std::move` as a default

Even more than that, with the exception of types that implement `Copy` (e.g. bools, integers, etc.), using after a move won't just silently degrade to a copy, but will cause a compiler error. Copying is required to be explicit for all but the most trivial types.


I think in Java, you would need to do a type-class approach.

    interface Numeric<T> {
        T zero();
        T add(T a, T b);
    }

    static <T> T sum(Numeric<T> n, T[] v) {
        T summer = n.zero();
        for (int k = 0; k < v.length; k++) {
            summer = n.add(summer, v[k]);
        }
        return summer;
    }


This will work, but will have poor performance due to the way Java generics are implemented. You could use your function with `Integer[]` or `Long[]` arrays but not with `int[]` or `long[]`. `Integer` being a wrapper class around `int` is a regular Java object accessible via reference, hence slow and memory-inefficient.

On the other hand C# offers "real" generics and you can use almost exactly the code that you wrote (generic syntax is slightly different).


Type classes are on the roadmap after Valhalla [1].

[1] https://blogs.oracle.com/javamagazine/post/what-are-they-bui...


This is good news.

My wishlist for Java is for null to be illegal for all types unless they are encoded as nullable such as `T?`.

Until then, I just use Kotlin and/or Scala when I require JVM.


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.


Honestly I have no idea what's going on with this code.

Where is `zero()` defined? Why does a `Numeric` have a function to `add` two numbers? Shouldn't you add a single number to a numeric? What is the relationship between `<T>` and `Numeric<T>`? I thought `T` was a `Numeric`?

Edit: Upon closer inspection, I kind of get what's going on. But it's really cryptic for something that should be simple to express.


> Where is `zero()` defined?

It's defined by whatever implements the interface.

> Why does a `Numeric` have a function to `add` two numbers?

Because Java doesn't have user-defined operator overloading, so if you want to add stuff in a generic fashion you can't rely on `+`.

> Shouldn't you add a single number to a numeric? What is the relationship between `<T>` and `Numeric<T>`? I thought `T` was a `Numeric`?

`Numeric` doesn't contain data, it just defines operations on numbers. So `Numeric<Integer>` would define operations on `int`s, `Numeric<BigInteger>` would define operations on `BigInteger`s, etc.


Think of `Numeric<T>` as analogous to `Comparator<T>`. T is the type you're doing stuff with, `Comparator<T>` tells sorting algorithms how to compare two Ts, and `Numeric<T>` tells mathematical algorithms how to add two T's, and what a "zero" value for T should be.

There are probably other reasons to do this that I'm forgetting, but off the top of my head,

1. You can implement `Numeric<T>` or `Comparator<T>` for types `T` that come from a library that you can't change (so can't make T implement an interface that the library author didn't implement), or where you don't want to introduce a dependency (you could have a type class `JsonDecoder[T]` that comes from a library, and you don't want the library with your T to introduce a dependency on the json library).

2. You can have more than 1 implementation for a given T. For JSON decoders/validators, you might provide a decoder which bails out on the first error, or one which tries to continue reading fields so it can return all errors (field X was a string, expected number. Field Y was expected to be >= 1024, etc.). For comparators, you might have a `.reversed` function to easily make a comparator that sorts in reverse order. etc.


I think this may be good reference material: http://learnyouahaskell.com/types-and-typeclasses

Basically I'm encoding this approach in Java.


Isn't the java problem that (a) the + operator is only defined for some built-in types, and (b) the int/Integer boxing distinction?


Right, you can't overload the + operator, and primitives can't be used in generics.


yep, though a monoid isn't strictly necessary - a semigroup is sufficient for this use-case.


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

Search: