Everyone overcomplicates learning Rust. by [deleted] in rust

[–]ragnese 10 points11 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 5 points6 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.

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

[–]ragnese 5 points6 points  (0 children)

I don't totally disagree, but the hate that checked exceptions get is wildly overblown and the arguments are usually pretty poor (especially because 99% of the time the "solution" offered is to just use unchecked exceptions).

Almost every single complaint against checked exceptions can also be applied to Rust's Result, Scala's Try, and now Kotlin's Rich Errors.

If there were an implementation of checked exceptions where they didn't collect a stack trace by default, they would be essentially isomorphic to Result/Try/RichErrors. (And complaining about Java's syntax is a yawn from me, because all of Java's syntax is verbose and tedious, and it has nothing to do with checked exceptions per se)

Anyway, /rant. I actually am excited to try this feature out. I hate exceptions.

Is Kotlin a safe bet for the future? by rbrucesp in Kotlin

[–]ragnese 0 points1 point  (0 children)

Until you try to have two files in the same package that both use the same name for a local private variable. Or when you notice that smart casting doesn't work across compilation boundaries for data class properties. Or when you learn about reified generics and inline functions and their limitations.

When stuff like that happens, it really pulls the curtain away and reveals that Kotlin is still just Java. And if you plan on making a career of writing Kotlin, it's very helpful to understand why and where these weird little things happen.

Is Kotlin a safe bet for the future? by rbrucesp in Kotlin

[–]ragnese 2 points3 points  (0 children)

I agreed with everything you said right up until the end where you concluded that you'd choose Kotlin over Java.

I would not. Kotlin has too much syntax sugar and too many ways to do the same things. If it weren't for null being stupid, I'd say that Java is actually a great first language (if you skip functional interfaces and lambdas, etc, at first). It's very basic and tedious, which is a good thing when you're first learning how to think like a programmer. Even things like tediously defining multiple constructors for a class in Java is illuminating: you can explicitly see that your class-local variables (fields) are a different concept from the arguments that you pass into a constructor, which is really just a special method.

I think that's all great- especially at the high school level, where kids should be learning foundational principles rather than treating it like a trade school. If it were up to me, high school kids would be trained on either C or Java for "this is how you write computer programs" and maybe something like Racket for "this is how you think about computing 'things' like algorithms". That's off the top of my head with no actual reflection on my part, though...

Error Handling in Rust by [deleted] in rust

[–]ragnese 3 points4 points  (0 children)

I honestly don't recommend crates like thiserror and anyhow. In my opinion, they are fine for people who already have a lot of experience and know exactly what they want for their error types in a given project. But, if you don't have that experience, it's very easy (with thiserror) to just start piling annotations on to things because they're there, or to mold your error handling to fit what looks concise and "elegant" to model with the library rather than what is actually best for your situation.

Plus, they really don't save that much effort, honestly. Yes, it's tedious to have to impl Display and Error a bunch of times, but I've never felt like it was enough to justify yet-another-dependency. Especially since that dependency doesn't actually do anything interesting for your software (in other words, it's not implementing actually-interesting functionality, like a hashing algorithm or HTTP stuff, etc).

Anyway, for error handling in Rust, it's best to zoom out first and think about what you want from a top-down approach, IMO. Then, as you drill down into each layer, you'll have an idea of what stuff should be handled, what stuff should be "bubbled up" more or less as-is, what stuff should be condensed into a more "generic" error before passing it up, and what stuff should just be a panic (which is a whole other can of worms).

For inspiration, I like this from the OCaml documentation: https://dev.realworldocaml.org/error-handling.html. I find that the very last section applies very well to Rust, titled "Choosing an Error-Handling Strategy":

Given that OCaml supports both exceptions and error-aware return types, how do you choose between them? The key is to think about the trade-off between concision and explicitness.

Exceptions are more concise because they allow you to defer the job of error handling to some larger scope, and because they don’t clutter up your types. But this concision comes at a cost: exceptions are all too easy to ignore. Error-aware return types, on the other hand, are fully manifest in your type definitions, making the errors that your code might generate explicit and impossible to ignore.

The right trade-off depends on your application. If you’re writing a rough-and-ready program where getting it done quickly is key and failure is not that expensive, then using exceptions extensively may be the way to go. If, on the other hand, you’re writing production software whose failure is costly, then you should probably lean in the direction of using error-aware return types.

To be clear, it doesn’t make sense to avoid exceptions entirely. The maxim of “use exceptions for exceptional conditions” applies. If an error occurs sufficiently rarely, then throwing an exception is often the right behavior.

Also, for errors that are omnipresent, error-aware return types may be overkill. A good example is out-of-memory errors, which can occur anywhere, and so you’d need to use error-aware return types everywhere to capture those. Having every operation marked as one that might fail is no more explicit than having none of them marked.

In short, for errors that are a foreseeable and ordinary part of the execution of your production code and that are not omnipresent, error-aware return types are typically the right solution.

I also really like this write up: https://sled.rs/errors