Effect – Build robust apps in TypeScript
effect.website
effect.website
It's great for workflows with clear points in the logic where things should happen and it provides more sophisticated (i.e. powerful but harder to master) tools like Fibers and Streams which allow you to reason about failure cases of reactive asynchronous operations. In many cases it offers a clear path out of callback-hell that is more reliable than promises and async/await.
However while the Effect-ts docs are getting better and may be ok for people with a good knowledge of functional programming and Typescript, they are nowhere near the quality they need to be for those who don't. People looking for examples online will get frustrated because Effect API churn over the past three years has made many old posts and articles obsolete. Old github repos won't work out of the box. And you better be comfortable with codemods if you haven't frozen your Effect-ts version.
Fortunately the Effect-ts Discord channel is full of friendly and helpful people and the Effect team provides high quality assistance to people who ask for it. It makes me sad this treasure trove of information is trapped in Discord where search is of little value.
A good book or collection of high quality examples of how to use Effect-ts with de-facto standard frameworks like React could help its adoption grow significantly.
That said, the person who built the ZIO project which inspired Effect-ts is now working on an exciting project called Golem Cloud¹ which aims to provide durable and reliable location-transparent execution of Wasm programs.
Mike Stonebraker's DBOS² looks to provide something similar for Typescript.
Study after study shows that many evaluations of LLMs are deceptive because they evaluate them on questions that were answered in the training set. On the other hand, this is exactly why they can do so well on medical board exams because you don't learn to pass the medical board by predicting what medical treatments should work according to first principles but instead by remembering which medical treatments have been proven to work.
For that matter I've long wanted a conventional search engine which can be specialized for a particular software project I am working on: for instance I might be using JDK 17 and JooQ version 3.15 and I only want to search those versions of docs. It shouldn't be hard to look at the POM file to figure these versions out. I have the exact same problem with Javascript where I have to work with code that uses different versions of react-router, bootstrap, MUI, etc. The idea LLM coding assistant should do the same.
> Write me JS code that generates a random list of 10 prime numbers, takes the sine of each (as radians), then takes the cosine of each of those values (as degrees) and then adds 5 to each value
I assure you, this task is not on Stackoverflow (because nobody would ever be deranged enough to want to do this) but GPT 4o could do it!
Admittedly, this shows only a very rudimentary reasoning ability but it demonstrates some. The idea that LLMs are just a slight evolution of search indexes is demonstrably false.
In a hard programming problem, for instance, you have a number of actions that have to take place in a certain dependency order that you could resolve by topological sort, but in a problem like the above one bit of English corresponds to one bit of Python in basically the same order. Similarly if it couldn't translate
take the sine of...
to Math.sin(x)
because the language didn't already have a sin function it would have to code one up. Similarly, translating between Chinese and English isn't really that hard because it is mostly a linear problem, people will accept some errors, and it's a bit of an ill-defined problem anyway. (Who's going to be mad if one word out of 50 is wrong whereas getting one word wrong in a program could mean it doesn't compile and delivers zero value?)LLMs can do a bit of generalization beyond full text search, but people really underestimate how much they fake reasoning by using memory and generalization and how asking them problems that aren't structurally close to problems in the training set reveals their weakness. Studies show that LLMs aren't robust at all to the changes in the order of parts of problems, for instance.
The biggest problem people have reasoning about ChatGPT is that they seem to have a strong psychological need to credit it with more intelligence than it has the same way we're inclined to see a face inside a cut stem. It can do an awful lot, but it cheats pervasively and if you don't let it cheat it doesn't seem as smart anymore.
1. A way to have strongly-typed errors
2. A standard library, like a better HTTP library and better concurrency primitives.
I'm not particularly sold on 1) in isolation. If you want strongly typed errors, you can use vanilla TS: just use `type Result<T> = { type: 'result'; data: T } | { type: 'error'; message: string }` as a return type and you're basically there, modulo an if statement on the calling side.
On the other hand, 2) seems a bit more compelling. I do think it's true that JS could use a better standard library. But the cost of the standard library is that I now have to write my code in this extremely unusual way, and that seems like a bit too high of a cost to me. Effect.forEach (instead of for...of), Effect.runPromise (instead of await), Effect.andThen (instead of... nothing?) have, to my eyes, significant penalties in readability, debuggability and maintainability.
I agree that a wrapper around generators is nice if it suits the situation (like async/await), and abstracting them is useful at times, but I’m not sure I’d want to pull in an entire library for it.
One thing, the Effect.runPromise and similar methods which seem like overkill are probably providing quite a bit more utility and reliability than it appears. It might offer good cleanup guarantees as well.
I should add that this is the equivalent of a hot take. If I looked closer I’m sure I’d change my mind, but I’m old and crotchety like you.
This doesn't feel like a pipe, the pipe is mixing data pipeline operations with declarative configuration reusing the original operation, almost like the Config Builder pattern. Nothing wrong with that in itself, except it's obscured by calling it a pipe.
What will be retried anyway? The last step in the pipeline or the whole pipeline? Do you really want to retry a json decode or 4xx client side error? Ignoring this makes for a nice front page demo of short code, but when you start considering real scenarios, some of the same complexities of the vanilla ts version eventually creep up. The question is, do you want to solve those with straightforward, yet slightly more verbose, ts code, or by learning all the edge cases of the library.
If you are looking for something similar to only handle the difficulty of error and retry handling related to fetch, and are ok with being tied to React concepts, i can recommend TanStack Query instead. Thanks to the reactive nature, you just need to configure a query object, which then only returns data to you and re-renders when it is valid. Good stuff.
The huge advantage is Typescript obviously, a much more popular language to spread the same ideas.
I've been happily using the effect ecosystem for years and can't but say I am super happy to do so.
It does require some patience to learn and it's not really super friendly to people that never really took the time to learn typescript, but if you did it's such a pleasure to write code for Node/Demo/Browser/Workers.
This website should probably start with this. Instead it’s a bunch of like, modern synergy-ese.
There's lots to Effect beyond error handling, I can imagine it can seem a bit overwhelming.
The great thing is that you can cherry pick what you need.
Concurrency, runtime, tracked errors, functional composition, dependency injection, etc there's lots of problems that the ecosystem solves.
It's hard to say which of these features is particularly more important.
How does EffectTS improve upon that?
(Sum-)Types would of course be helpful in making it clear that the function can sometimes return an instance of some specific Error-subclasses of course.
Now I assume I could do something like that in plain TypeScript also, return error-instances of different error-subclasses and declare them to be possible return-types, and then handle them in the caller or its callers somehow.
Anybody getting the error-result could not pass it or return it to anybody else whose type does not expect an error-instance.
Is there something even better with EffectsTS?
Seems like it's mostly trying do similar things to what Haskell or StandardML does, but in Typescript.
With Effect (and ideas like it), you can treat error conditions as normal expressions, without special control flow. You can reason about the error conditions the same way you reason about the successful data flow. You can treat your own Error subclasses just like any other value type, with the same kind of conditional logic in the same flow. And you can do that without looking deeper into the call stack to understand where errors come from, because errors flow exactly where values flow.
Because errors and values are in the same flow, you can compose things in ways you couldn’t (or at least wouldn’t typically) with separate error control flow. You can build complex logic by composing simpler fundamentals, in a way that’s more declarative and self-documenting. Your error instanceof checks can be combined with map/filter/whatever else just like you might use with success values, and they can be mixed so that recoverable error conditions are expressed even more like success conditions.
If you’re into types, all of the same benefits apply at the type system level. You don’t have to care about types to enjoy these benefits. If you don’t care now but embrace types in the future, you’ll just get more of the same benefits in that hypothetical future.
It's often said that checked exceptions are the worst mistake in Java and I believe that.
To be specific consider the case of a class that implements FetchSomeData which fetches some data (could just as well be a function with parameters encoded inside it as a closure) which could come from many sources such as
* hard coded in the class
* read from a file
* fetched via http
* fetched from Postgresql or CouchDB or ...
the one thing these have in common is they can fail in very different ways. If you want to take encapsulation seriously here the right way to do it is to expose certain semantics of the error such as * Could the user avoid this problem by changing their inputs?
* What do we tell the end user? What do we tell the sysadmin?
* Is it likely that this problem will clear up if we retry the request in five minutes?
* Is attempting to use this object likely to cause worsening data corruption
and such. In Java we get the very week beer of people creating a large number of exceptions like FailureinSubsystemSeventeenException and sprinking thousands of methods with throws clauses and doing a lot of error-prone catching and rethrowing to please the compiler but if you really are serious about error handling you know you could get a RuntimeException because something wrote a[15] on an array that has only 12 elements and you still need to catch those anyway.So far systems that use something like Either[Result|Exception] aren't based on an ontology of failure but my experience is that code written like that tends to have the same problems you have with checked exceptions: harried by the compiler people often omit error handling code to the maximum extent that they can. From working with both kinds of code bases I'd say that code bases worked on exceptions have the glass half full and handle errors properly more than they do improperly, these monad-based systems are the other way around.
Java has poor ergonomics for these, and in my opinion languages like Rust have better ones and that's why they manage with Results.
Otherwise your code is just littered with try/catch/throw new RuntimeException(ex).
I recently started a company and was tempted to use Effect as I felt I would be able to build faster and higher quality than Standard TypeScript, but decided against it since I felt that I would pay the price when it came to grow the team. So far the decision has paid off.
I did indulge myself a little bit though and do make heavy use of remeda and ts-pattern across my code.
I also am a bit weirded out by the Effect.Effect<unknown, HttpClientError>. . . What is unknown?
This is not "normie" TypeScript but I wouldn't characterize it as particularly scary.
Given the goal of effect is to provide type safe tracking of a codebase; having an any breaks that assumption, while unknown forces you to add validation to keep it type safe.
1. https://github.com/Effect-TS/effect/blob/main/packages/schem...
If your project, as a whole, benefits heavily from a functional style of programming, you are likely better off using one of the many functional programming languages that compile to JavaScript, like ReScript, OCaml, F#, or Scala.
Http.request.get(`/todos/${id}`).pipe(
Http.client.fetchOk,
Http.response.json,
)Or do you need to figure out what is the place in your original unprocessed source-code that corresponds to the code you see in the debugger, and then switch from the debugger to edit the original file and then rebuild and rerun everything and see if that fixes the problem?
I wonder if that is a solved problem in plain TypeScript either?
Repl is often sufficient, but not in cases where you need to pass some convoluted data as input.
1. You don’t have breakpoints, your code doesn’t run. Your code is the thing that configures the code that runs. Your code is descriptive.
2. Nothing unexpected can happen. That’s the point of pure functions and a strong type system. You don’t need to know the exact values that flow through at runtime to know that your code functions as written.
Granted, if you write code that doesn’t make sense then it will produce* results that don’t make sense. If you don’t fully understand the problem you’re trying to solve no programming paradigm can save you from creating bugs.
Have you ever waited forever for Typescript to finish its typechecking and it eventually just gave up and timed out because it was too complex? Now imagine that the typechecker is the only thing that stands between you and insanity.
If there is a bug in the framework (here Effect) then you're in trouble because these layers of abstraction make it an absolute nightmare to figure out that it's not your code but the framework that has a bug — you can't step through your code, and there is only so much that you can encode in types.
This is appealing in the first half of your bell curve. You have to recover from it a few times and start writing code.
(Please do not reply with "I found RxJS easy". Good for you.)
Took it to the RxJS discord after a couple days of pounding my head on it. One of the creators was there and was super helpful.
We went back and forth on the problem at least 6 times each, with new attempts. He tried quite a few variations, but none ever worked.
What about "what makes it hard"? I've never tried RxJS before, so I'm curious to know where the learning curve is.
They also had a tough time remembering all the operators in the library.
Throwing functions in Swift don't throw traditional exceptions (please someone up the stack catch this). Throwing an error in Swift actually returns an Error to the calling function instead of that function's normal result type. Swift also enforces that when you call code that might "throw" (return an error instead of a result) you either handle the error or "rethrow" it.
In TS I use this pattern:
`type ErrorThrowingFunction = <T,>() => T|Error`
But it's not nearly as nice to use in TS since it's not a language feature.
> If you're on drugs, check out Effect.loop. Someone smart asked "How can we make a for loop deterministic and palatable to Functional Programmers?" they then smoked crack and did a good job.
https://www.linkedin.com/pulse/quick-thoughts-effect-ts-jess...
The review is discursive & dense; I wish we saw more hot take rampages through codebases like this.
The section on "without effect" and "with effect" is intriguing and makes me want to dig in more.
I used to be of the opinion that things should just stand on their own but lately I've accepted the fact that the hind brain visual appeal is important.
Stream processing of data is nicer using a declarative approach. Though I've found that describing control flow is often more intuitive using an imperative approach. It's a shame that redux-saga got tied to react/redux, because the idea of generators and declarative effects are universal - useful on both backend and frontend.
Schema is a more advanced library though that goes beyond parsing, it supports both encoding and decoding, also supports more complex types.
Check the docs of effect/schema to get a taste.
The fp-ts author is also an effect maintainer and recommends effect for new projects. fp-ts is not dead, but it's core goals (providing Haskell/PureScript-like types and type classes) have been mostly achieved.
Effect is an ecosystem with different goals from fp-ts.
import { Effect } from "effect"
// Get the current timestamp
const now = Effect.sync(() => new Date().getTime())
// Prints the elapsed time occurred to `self` to execute
const elapsed = <R, E, A>(
self: Effect.Effect<A, E, R>
): Effect.Effect<A, E, R> =>
Effect.gen(function\* () {
const startMillis = yield\* now
const result = yield\* self
const endMillis = yield\* now
// Calculate the elapsed time in milliseconds
const elapsed = endMillis - startMillis
// Log the elapsed time
console.log(`Elapsed: ${elapsed}`)
return result
})Also pipe operator is coming soon [1].