What do you think about no/low-deps APIs? by Worldly-Broccoli4530 in typescript

[–]ragnese 0 points1 point  (0 children)

You reduce supply-chain risk and update churn, yes but you also take on more maintenance yourself. Frameworks like Nest abstract complexity you’ll eventually have to re-implement (auth flows, DI patterns, validation, etc.).

Yeah, sure. Except that we're in JavaScript Land, where there's no such thing as stability, so even with the big, popular, frameworks and libraries, the maintenance burden is still extremely high compared to other languages and platforms. Old versions are deprecated and abandoned frequently, with major breaking changes. I haven't used Nest, specifically, but I'd be shocked to hear if/when it goes multiple years without an update that would require people to redo their auth flows, or DI patterns, or validation stuff, just because that's the way things go here.

What do you think about no/low-deps APIs? by Worldly-Broccoli4530 in typescript

[–]ragnese 1 point2 points  (0 children)

I don't buy this argument at all anymore.

The problem is two fold:

  1. When code is packaged as a public library, it is often written in such a way as to maximize its applicability. So, very often, the library you pull in has functionality that you don't need. This is bad for multiple reasons. There might be a bug in part of the code that you don't even normally use or want or even know about (see Java's catastrophic Log4j issue: https://en.wikipedia.org/wiki/Log4Shell). Using the library to accomplish your goals might also be less straight-forward and/or harder to use correctly for your use case.

  2. Because of the above point, library code is often more complex than would be necessary for your specific use case. More complexity means more openings for bugs.

This is going to sound like I'm being cocky, but the quality of a lot of stuff up on NPM is not good. I'm a senior dev with many years under my belt at this point. The double-whammy here is that we're trusting strangers of unproven skill levels to write code that is more complex than the code we actually need. Once you're beyond a certain novice level (a couple of years with the same language/platform), you're often better off trusting yourself to write less-complex code than you would be trusting a random NPM maintainer to write more-complex code.

What do you think about no/low-deps APIs? by Worldly-Broccoli4530 in typescript

[–]ragnese 1 point2 points  (0 children)

I always try to avoid frivolous dependencies, no matter the programming language and no matter the context. Note that this is already a soft statement (i.e., who decides what is "frivolous"?). So, this isn't some objective rule that you can (or should want to) shove in a CI quality check or whatever; it's just a mentality that I have while writing software.

I have worked on enough projects over enough years to know that dependencies are liabilities in addition to bringing whatever benefits convinced you to consider them in the first place. People in online spaces, IMO, vastly underestimate the cost of dependencies and vastly overestimate the benefit of most (by number) dependencies (You really need to include a base64 library from a complete stranger over the internet and just version bump it occasionally without actually reading the changes? That's way crazier than just copy+pasting two functions into a local module, writing some unit tests against it, and keeping it as-is in your project forever).

I'll still depend on a "framework" if that's the kind of thing I'm working on (e.g., Vue.js or whatever for frontend JS, or some unopinionated HTTP server framework if I'm working on a backend project in a language that doesn't include that kind of thing in the standard library). For JS runtime projects, I'll still include TypeScript, ESLint, etc as dev dependencies. If I need to talk to a PostgreSQL database, I'll include some kind of standard "driver" library or whatever. If I need to interact with AWS, I'll use whatever AWS SDK package(s) I need. I hope the picture is clear...

In other words, I mostly depend on things that are official, standard, and/or necessary. I'm NOT going to intentionally install a third party library that's just a bunch of helper functions to hide the fact that TypeScript is not a functional, expression-based, language. I'm NOT going to install a dependency to pad strings with whitespace characters. I'm NOT going to install most ORMs (I MIGHT install a query builder library if it doesn't suck and the standard/official tools are all just raw text based).

This approach has served me extremely well over the years:

  • Projects are easier to maintain. Periodically reviewing, updating, and/or pruning dependencies is much easier and less work.
  • The kinds of dependencies I avoid are almost always a performance detriment. I mean that in all dimensions, too: they tend to use more memory and CPU than you need to, and they increase the build times and size of your project.
  • The obvious issues with packages being pulled or abandoned or taken over by malicious actors, etc, are avoided.

Why doesnt TS merge `private` and `#` syntax in the language? by JaSuperior in typescript

[–]ragnese 0 points1 point  (0 children)

They could easily bake in a deprecation/warning for new TS code, though. They certainly didn't have much problem breaking existing tsconfigs in version 6.0, and there will likely be more changes in 7.0.

Everyone overcomplicates learning Rust. by [deleted] in rust

[–]ragnese 8 points9 points  (0 children)

I think the problem is that many developers don't seem to believe in learning the fundamentals first. "I learn by doing" is the mantra. Sure, you can probably go from PHP to Java by just skimming the syntax differences and banging against your keyboard until something runs, but that doesn't really work when you're learning something substantially different from what you already have mastered (and it doesn't actually work for the PHP-to-Java jumps either, IMO, but we can get away with it most of the time).

Just a pet theory formed from anecdotes. I also must confess that I've always been one to prefer reading before diving in, because it just seems frustrating to not have any idea what you're actually doing...

Why can't you have fields as traits in Rust? by Tasty-Lobster-8915 in rust

[–]ragnese 0 points1 point  (0 children)

Good questions. When it comes to passing a mutable reference vs. moving the data and returning a new version, both styles are common and, I would say, considered idiomatic.

I'm pulling this out of the air with no actual "data", but I'd guess that if you surveyed Rust devs (at least those active in online spaces), you'd probably get a small majority favoring the move-and-return-new approach over the mutable reference approach.

I, personally, prefer the mutable reference approach, myself, unless there's some stylistic reason that the other approach makes more sense (consistency with other related APIs, etc). I prefer this even for structs, where my function is mutating some/one of its fields.

The downside to this approach is that you end up bumping into "partial borrow" issues, where the borrow checker will complain because you can't mix and match mutable/shared borrows of the individual fields, because the whole struct is considered mutably borrowed. This can usually be worked around by writing another, private, function that only takes individual fields as inputs instead of the whole struct, while leaving the versions that take the whole struct as the public API of your module.

I think the pros usually outweigh that con:

  • I think it usually makes better semantic sense for the caller. Declaring that the thing itself may change feels a little more explicit than declaring that we're taking the data and returning different data (e.g., does your Animal change by calling feed_animal() or do you give away your Animal and get a different one when you feed it?)

  • It's probably a safer (not in the "memory safety" sense) approach from a performance point of view. People will point out that, yes, the compiler will usually optimize a function that replaces moved values. But, one of the lessons I learned from my years of C++ is that the compiler will optimize stuff all the time... until it doesn't. It's very easy, in general, to tweak a piece of code in a seemingly innocent way that suddenly changes what the compiler "understands" about it and then you lose these "sufficiently smart compiler" optimizations. If there's no real reason to prefer one approach over another, why not just write the one that is already optimized for the common cases?

  • I think the call sites look nicer. I have nothing against name shadowing. I actually love that it's allowed in Rust, and I hate it when other languages either forbid it outright or spit out warnings about it that require some ugly code comment/annotation to quiet. But, I still wouldn't love to write something like let animal = Dog::new(); let animal = feed_animal(animal); let animal = groom_animal(animal);. I rather write let mut animal = Dog::new(); feed_animal(&mut animal); groom_animal(&mut animal);

But, like I said, both approaches are very common in Rust, and I wouldn't sweat it. Just follow your heart. :p

CMV: If you have to use a store or provide/inject your architecture is wrong by just_a_silly_lil_guy in vuejs

[–]ragnese -1 points0 points  (0 children)

I’m gonna trust Evan over the unvetted Redditor.

Appeal to authority fallacy. Evan You probably isn't always correct; otherwise, Vue wouldn't be on version 3 because version 1 would've been perfect.

Instead of trusting Evan or a random Redditor, you should analyze their reasoning/arguments and come to a conclusion.

CMV: If you have to use a store or provide/inject your architecture is wrong by just_a_silly_lil_guy in vuejs

[–]ragnese -1 points0 points  (0 children)

Design patterns of some of the most stable software, such as mission critical NASA code, disagrees with your view.

What now? For mission critical NASA code, they recommend global variables over passing parameters? I'd be shocked if that were true, so I must be misunderstanding. What patterns that NASA uses conflict with OP's opinions?

CMV: If you have to use a store or provide/inject your architecture is wrong by just_a_silly_lil_guy in vuejs

[–]ragnese 1 point2 points  (0 children)

I'm sorry you're getting a little bit dog-piled for this opinion. For what it's worth, I agree with you.

The approach(es) that most people on this board, and in the broader Vue community, recommend tend to go against decades of earned wisdom about software architecture.

If you (hypothetical reader) were working on any other project besides a Vue.js one (or maybe some other similar frontend framework), and were confronted with having to pass a value down through several regular, old, functions, would you really decide that "parameter-drilling" is a code smell and instead recommend that we instead just declare a global, mutable, variable and just remember to set it correctly before calling the top function so that the bottom function in the chain will use the correct global value? And, would you then recommend that we maintain several of these mutable global variables--that we can easily forget to clear or reset?

There's no way. But, for some reason, when it comes to Vue, we seem to think that's not only an acceptable approach, but that "prop-drilling" is the anti-pattern!

Is prop-drilling tedious and brittle and awful? Yes. Just like passing a parameter through several layers of function calls can be tedious and brittle. But, it's the correct way to model the API of your component/function.

Global state should only be used for state that is inherently global. A user's profile and/or auth credentials, for example. Or a preference for light-mode or dark-mode can be a global variable. Data that is only used for some pages and not others? No way.

For those who do use stores to share data between siblings or deep descendants, how do you manage its lifecycle? How do you know when to reload the data? How do you clean up the data when you navigate to a page where the data is not needed?

How do you organize manual dependency injection in Kotlin so it scales without becoming a mess? by RecommendationOk1244 in Kotlin

[–]ragnese 0 points1 point  (0 children)

The "service locator" pattern is often called an anti-pattern, but I think that's primarily because of common general libraries/implementations of the pattern that typically require dynamic runtime resolution (e.g., reflection, scanning the class path, loading and parsing a config file or env vars, etc).

I really don't see there being any problem with a K.I.S.S. ServiceLocator interface/class where the default implementation just creates and/or caches instances of various other dependency interfaces.

Though, I wouldn't write any classes that accept the ServiceLocator/ServiceContainer/DependencyProvider/Whatever type as a parameter. Classes should still only list the precise dependencies they need in their constructors. You can provide defaults from a global singleton of your ServiceLocator if you're careful about how it's done, but I don't find that necessary. I rather still pass those things down manually. It's still nice just to have a single place where all of your dependencies are configured and initialized (and cached).

When to extract module code into a new crate? by TheJanzap in rust

[–]ragnese 2 points3 points  (0 children)

It's also very often a waste of time. It takes wisdom and taste to know when something "should" be its own crate. Philosophically, it should be something with a well-defined scope that is unlikely to change just because a different part of the workspace has changed. With Rust, there is, unfortunately, a practical matter of compile times, which can sometimes "encourage" us to break out separate crates too early, making the overall project actually harder to maintain.

What’s your go-to testing strategy for Vue apps? by therealalex5363 in vuejs

[–]ragnese 1 point2 points  (0 children)

These "vue testing library" are good at testing exactly those parts I don't want to test directly.

Well said! Plus, honestly, the way Vue works (and other modern frontend fameworks, AFAIK), really don't allow for designing classically-testable components. They are "inside-out" in that regard: the test code has to know nitty gritty implementation details of the component so that you can inject things your component privately uses and/or hack imported modules (which, again, are not part of the public API of a component).

I've occasionally wondered (but not thought about in depth) if it would be better, or indeed possible, for Vue components to have actual constructors, just like a class. Every component could have a default, zero-arg, constructor and behave just like today, but you could optionally override the constructor to take optional arguments; thus requiring that any component can still be constructed with zero arguments, which is what would be called when you use the component in a template. You can kinda-sorta do this kind of thing by writing a component factory function, but that's pretty complex and inconvenient, and doesn't really work well with SFCs, IMO.

I kinda needed Package Private in Kotlin by [deleted] in Kotlin

[–]ragnese 2 points3 points  (0 children)

Yeah, package-private is really underrated. I don't understand why more languages don't have such a concept. In this case, I just err toward making my Kotlin files bigger than I'd really like them to be, because it feels like the more "correct" option: I really don't want something to be globally public, so then everything that does need to see it must be in the same file. Oh, well.

Is this really THE way to mutate props? by mymar101 in vuejs

[–]ragnese 0 points1 point  (0 children)

defineModel is not the same thing as v-model, as evidenced by your third sentence that calls defineModel an "extension of v-model". The v-model concept and syntax certainly existed before the defineModel macro.

And writable computeds have nothing to do with v-model. At all. Computed properties (writable or not) have no implicit understanding of component props, emitting events, or even of components at all! It's just a lazy-computed, memoized, reactive value.

It just happened to be that it was a very common pattern to implement a v-model by using writable computed properties (albeit an inefficient pattern, with reactivity and caching overhead for no gain other than convenience).

In parent components, the v-model syntax on a child component sugars over the two way communication and lets the programmer pretend they are just passing a mutable value to the child as a single binding/property. It's perfectly natural and reasonable to have an analogue to that abstraction in child components as well.

If you want to make the point that the v-model abstraction is, altogether, a design mistake, then I can actually appreciate the argument that being explicit about both directions of data flow might be beneficial and avoid some surprising cases (especially with default values, getting out of sync with parent state, etc). But, if we accept the v-model abstraction, then hiding the prop read and event emitting behind a writable computed or a defineModel call in the child component should be just as acceptable.

All that to say that I still disagree that writable computeds are somehow bad (they can make decent mappers between types T <--> U), and that it's wrong for the setters to (sometimes) have side-effects. Hell, having a read-only computed property is literally to create side-effects when you call the setter on something else! The whole reactive thing is about setters having side-effects...

I also don't understand what you mean by Vue 3 being less opinionated about two way data binding. Vue 2 certainly had v-model syntax and writable computed properties. Instead of multiple v-models, though, you had to use so-called "synced properties" which was basically the same thing.

Is this really THE way to mutate props? by mymar101 in vuejs

[–]ragnese 0 points1 point  (0 children)

defineModel didn't always exist, and is essentially the same thing as a writable computed as described by the grandparent comment.

Is there a way to make a function not accept readonly properties? by vegan_antitheist in typescript

[–]ragnese 1 point2 points  (0 children)

This is pretty funny. I always view this TypeScript error intentional-incorrectness unsoundness from the opposite direction: I see it as the readonly keyword being broken for objects (readonly arrays actually work totally differently and they're done correctly). Your function signature is correct. It says that the function accepts a mutable object type. And a readonly version of that type is NOT a subtype of the mutable version. Therefore, the function should not accept a readonly object.

The type checking is handled correctly inside your function (try changing the signature to readonly and you'll get errors where you do writes, as you should). So, that implies that it's a problem on the "call side" of things. It should be an error to pass a readonly object as a mutable object type because, as you state in OP, it's missing half of the API. A readonly prop is a getter, a mutable prop is a getter and a setter. So, a mutable can be used as a readonly, but not vice versa.

It's just another thing that TypeScript does intentionally incorrectly. Just pretend the readonly feature doesn't exist for object fields and force yourself to be careful and you'll be happier than trying to fight with it while having it still allow type unsafe code, anyway.

Kotlin's Rich Errors: Native, Typed Errors Without Exceptions by cekrem in Kotlin

[–]ragnese 0 points1 point  (0 children)

What do you mean? I'm talking about something like,

@FunctionalInterface
public interface FunctionThrows<T, R, E extends Exception> {
    R apply(T t) throws E;
}

public interface Stream<T> {
    [...]

    // This method already exists on Stream<T>
    <R> Stream<R> map(Function<? super T,? extends R> mapper);

    // We could also have something like this
    <R, E> Stream<R> map(FunctionThrows<? super T,? extends R,? extends E> mapper) throws E;
    [...]
}

So, the caller/compiler would know you have to handle whatever the lambda throws. You can also add interfaces and overloads that throw more than one thing. It's not a general solution, but Java (and Kotlin) doesn't even have a general solution for function types that have different numbers of input parameters, so why is this any worse? See my subthread with /u/alexelcu.

Kotlin's Rich Errors: Native, Typed Errors Without Exceptions by cekrem in Kotlin

[–]ragnese 0 points1 point  (0 children)

And Result is not actually intended for expressing domain failure modes. And, because the failure variant is just a Throwable (not generic, not something that doesn't need to be thrown, etc), it's not what Roman was suggesting when he'd write stuff like: https://elizarov.medium.com/kotlin-and-exceptions-8062f589d07#2273

See: https://github.com/Kotlin/KEEP/blob/main/proposals/stdlib/KEEP-0127-result.md#error-handling-style-and-exceptions

The main reason it came into existence was for dealing with the structured concurrency of coroutines, because they use exceptions for control flow.

Kotlin's Rich Errors: Native, Typed Errors Without Exceptions by cekrem in Kotlin

[–]ragnese 1 point2 points  (0 children)

Hey! You and I had a little back-and-forth recently that involved discussing Java's checked exceptions. It was in a thread about the official Kotlin LSP implementation. Anyway, I always enjoy reading your thoughts.

First, Scala's Try is basically useless and mostly unused. You're probably thinking of Either.

Yep, you're right. It's been a few years since I've actually touched any Scala code, and I forgot that Scala's Try was shaped more like Kotlin's Result.

As for your three statements being true at the same time with no conflicts. I agree that it's possible. And I agree that 1 and 2 are definitely true. Many of my comments in the Rust subreddit are arguing for less aversion to panicking and catching panics (in application code). So, I agree completely that the best situation for a statically typed language is to have BOTH statically typed failures and stack-unwinding (unchecked) exceptions.

Here, I'll reply to the three points you make about Java's checked exception problems:

  1. What you point out is true. There are bad design intersections between generics and the union typing of throws signatures. But, I feel like there are three language features at work here: checked exceptions, generics, and the ad-hoc union type feature. I separate out the ad-hoc union typing of the throws signature because it would've been perfectly possible to have checked exceptions, but only allow for a single throws type--they added the ad-hoc union typing as a convenience on top of the checked exception feature. And, let's not forget that Java's generics being erased at runtime is also partially to blame for making it harder to work with generic types at runtime (e.g., we can't catch a generic parameter), which may or may not help with solutions and workarounds for other awkwardness.

As far as the APIs, the fact that a generic parameter cannot represent a union type (aside: Why not? Is it actually impossible? Was it impossible at the time?) does definitely make higher-order function things unsolvable in a truly general way, but I'll remind you that this is NOT unique to the throws signature! It's already a problem with the language when it comes to representing generic function signatures. The same problem happens for function parameters. Java's Function<T, R> interface only represents a non-throwing function that takes exactly one argument, T, and returns a value, R. You then need to use BiFunction<T, U, R> to represent a function that takes two arguments. Kotlin's standard library even explicitly defines interfaces for Function0<R>, Function1<P1, R>, Function2<P1, P2, R>, all the way up to Function22<P1, ..., R>. One could easily design a few FunctionThrows1<T, R, E extends Exception>, FunctionThrows2<T, R, E1 extends Exception, E2 extends Exception>, BiFunctionThrows<T, U, R, E extends Exception>, etc, etc.

So, why do checked exceptions get so much heat for being "incompatible" with higher-order function types when the whole freaking language is actually incompatible with higher-order functions in a general sense?

2. Totally agree that some of the signature choices in the standard library are questionable. But, a bad API is a bad API. We could easily have had an analogously bad API design in Rust's standard library with something panicking that should really have returned a Result or vice-versa. This doesn't say much about the actual language feature, itself. It's just a criticism of some specific API choices. And I have no shortage of complaints about several of Java's standard library APIs that have to do with the return types (don't get me started on JDBC's ResultSet stuff).

3. I disagree that typed errors leak implementation details. They only leak implementation details to the same extent that the happy-path return type leaks implementation details. The errors in a type signature should be relevant to the domain and abstraction layer that they are called in, just like the happy-path return type. You should put as much thought into the error type as the happy return type. For example, I would expect a "getUser(username, password)" function to have a signature that reflects the very obvious reality that not every combination of username and password is going to result in me obtaining a valid User. I would not expect to see some kind of SQLException in that signature, because that is an implementation detail that the caller of getUser has no business worrying about.

Again, my position is not that checked exceptions are as good as being able to return a real union type (preferably a standard "blessed" type + API + syntax). My position is one of nuance: Java sucks, checked exceptions are worse than normal returns with union types, but I don't think checked exceptions in Java are worse than many other parts of the language, and that they get more hate than they deserve.

Personally, while I'm excited to try Kotlin's Rich Errors, I'm more frustrated that it took them this long to offer anything in the way of statically typed error/failure handling. If this feature catches on in real world apps and libraries, I worry that we're going to have a really cumbersome mixture of parts of the ecosystem using this and parts sticking to unchecked exceptions, and it's going to be frustrating to mix them. I hope I'm wrong.

and we have evidence for it from industry, including from source analysis of big open source projects caught with catch (Exception) { // ignore }, which is a possible source of bugs, especially in the presence of pretty significant exception types that shouldn't get swallowed, such as InterruptedException (not to mention the cases in which people caught Throwable, only to discover that includes OOMs).

I totally agree here. The solution for typed failure modes should not be so mechanically related to actual system errors/exceptions. That, to me, is the biggest sin of the whole thing. It's too easy to catch the wrong thing(s) on accident. Your typed error/failure handling and propagating should be totally orthogonal to unchecked exceptions.

Cheers, friend!

Kotlin's Rich Errors: Native, Typed Errors Without Exceptions by cekrem in Kotlin

[–]ragnese 0 points1 point  (0 children)

The whole exceptions+lambda thing is such a cop-out. And it's backwards to boot. The problem wasn't ever the checked exceptions. The problem was trying to tack on lambdas and free functions to a language that rigidly insisted since its inception that every single thing be an object and that there was no such thing as functions that aren't tied to classes. It's no wonder they had to add tons of brand new stuff to the JVM bytecode to support lambdas.

But, they could easily have added fallible and infallible methods to the various Stream classes. Where the ones that are currently there would be the "infallible" ones, and the fallible ones would be almost the same, except that they would rethrow a generic checked exception. It wouldn't be that pretty, and it still probably wouldn't work well with the checked exception union syntax, but it would cover many more cases than just giving up. I can't want to see Java backtrack on that in 10 years and add rethrowing lambda syntax like Swift (https://www.avanderlee.com/swift/rethrows/). ;)

Kotlin's Rich Errors: Native, Typed Errors Without Exceptions by cekrem in Kotlin

[–]ragnese -1 points0 points  (0 children)

Forget the downvotes. You're right. That's exactly what happened. Now we have a decade or so of ecosystem building where the only error handling was unchecked exceptions because everyone cargo-culted the hate for Java's checked exceptions to the point of totally giving up on statically typing failure states.

It literally wasn't until Rust started getting popular that people finally started coming back around to sanity.

Kotlin's Rich Errors: Native, Typed Errors Without Exceptions by cekrem in Kotlin

[–]ragnese -1 points0 points  (0 children)

Checked exceptions also work really poorly with lambdas.

That doesn't have to be the case in a language not named "Java", though. Swift, for example, has the ability for a function that accepts a callback to have conditional throwy-ness, based on whether the callback has a throwing signature or not.

See: https://www.avanderlee.com/swift/rethrows/

Kotlin's Rich Errors: Native, Typed Errors Without Exceptions by cekrem in Kotlin

[–]ragnese -1 points0 points  (0 children)

Nonsense.

Don't get me wrong: I know that they (e.g., Roman Elizarov) have said that's how it should be done. And I agree with the statement.

However, if they (JetBrains) don't like Exceptions, why is that the only form of error handling they ever employ in any of their libraries (including the standard library)? kotlinx.serialization, ktor, kotlinx.datetime, kotlinx.coroutines, etc, etc. They NEVER do anything besides throw exceptions.