
Helpful links
https://typesanitizer.com/blog/effects-convo.html
https://alexn.org/blog/2022/10/13/the-case-against-effect-systems-io-data-type/
I want to use Effect.ts in frontend... but
I was learning about the free monad, and while learning the fact that effect.ts isn't one gave me a surprising revelation / intuition on what effect's goal is ( sadly still a bit lost on the free monad part ), and why it was hard to assimilate its importance as frontend engineer. I knew that I was characterizing Effect incorrectly before, but now I think I understand the source of my confusion.
The mantra of effect usually goes as follows:
You are writing instructions ( blueprint, thunk, or whatever you prefer ) instead of writing code.
But but but, when I write computer programs, I’m always writing instructions! This internal mischaracterization comes from the way frontends ( myself included ) learn about programming. The problem with learning frontend in an application first way ( which is something I did, and something many people do ), is really not grounding ourselves about the core lower level software development abstractions. Thus the words “reliability” and “scalability” has either a 1) different meaning or 2) just not important.
One of the key abstraction that flies over our heads is representation of data. Let’s say you are writing a react component. You are literally thinking of it as writing something that exists visually on the screen, and to have some sort of a magic 1 to 1 correspond with whatever the object that would appear on the screen is. AND that’s ok, at least initially, because design is a very imperative activity. The mischaracterization is, and hopefully I’m thinking about this correctly, thinking of the screen to component relationship as an isomorphism. The assumption that the changes in the component you write will result in the changes on the screen, and any change you make to the screen, would put the component in such a state that would not be different from altering the component state to be the “changed” state in the first place.
Because we “code” the component, and we see it existing right in front of us, along side the changes that appear on the screen, it makes it really hard to understand things just existing as data. We don’t think about error handling other than in the sense of application failure, because frontend developers care not to distinguish the “program” failure v. the “application” failure when the product / design directive is to put an error screen.
This also explains the kneejerk reaction towards a lot of developers disliking server components - because FRONTEND DEVS CAN’T FATHOM THAT CLIENT COMPONENTS ARE ALSO SENT FROM THE SERVER.
// Traditional: assumes 'user' exists
function getUserName(userId) {
const user = database.get(userId);
return user.name; // What if database fails?
}
// Effect: describes HOW to get user
Effect<User, DatabaseError, Database>
// ^^^^ ^^^^^^^^^^^^^ ^^^^^^^^
// What How it fails What it needsThe effect’s motive is to resolve data unavailability and uncertainty. And the “writing instructions instead of values” comes from the fact that, when I do const user = await getUser(id); I am assuming that this value just exists out of thin air. And that’s something that’s so easy to mischaracterize. I can easily construe it as “how do you mean? getUser(id) gets the user, there’s nothing complicated about it”’
But you cannot forget the fact that getUser(id) LITERALLY runs somewhere else, with some dependencies unknown to you, which may fail for unknown reasons ( again, not application reasons ). This also helped me have a fuller understanding of async await; this is a pattern in JS to tackle the similar problem of data uncertainty.
This does open a can of worms though, is Effect STILL useful for frontend. The answer is seemingly no, and I think I understand the Tanstack Query’s superiority as a simple client-side caching pattern.
The data unavailability problem in frontend, for most full-stack apps, is resolved through caching because its the most intuitive source of data availability. You make a call, with some error handling, based on queryKeys defined by client actions ( user submits a form). Beyond that, concurrency, batching, needing multiple swapper dependencies genuinely don’t matter ON the client to manage and architect. Not that those things shouldn’t be considered unimportant - client being a single device, as long as you acknowledge the component and its lifecycle as data, outside of that words its hard to see the benefits from the effect primitives.
Neverthrow is still cool though.