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.
Do you know if people have tried distributing execution across multiple machines using Effect? That's one of the big advantages of fully immutable code execution imo, and I'm really interested if anyone has succeeded in leveraging Effect for that purpose.
I don't believe anyone has done that yet in the way I think you're thinking for Effect-ts - i.e. something which would give effect execution a kind of location-transparency.
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.
I really just wish groups would train a llama 3 model and have AI assisted answers pulled directly from source code. Like if this project made https://docs.effect.website and I could query it directly secretllama.com style.
That would require the models to have real reasoning ability as opposed to looking up preformed answers on Stack Overflow.
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.
I just asked GPT 4o to do the following and it succeeded:
> 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.
LLMs are also good at tasks that are roughly "linear" in the sense that a group of input tokens corresponds to a group of output tokens and that translation moves from left to right.
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.
Hm, maybe I'm not seeing the point here, or maybe I'm just too old and crotchety. But it seems to me that Effect provides two things:
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.
Something which gets me about these Effect methods is that very similar things can be accomplished with generators. I assume this is all generators under the hood. I personally would rather just write that code because once you’re familiar with it, it’s really not that difficult to work with.
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.
It reads a bit backwards indeed. Effect.Http.request.get().pipe(Http.response.json, Effect.Retry)? Why not Retry(get())?
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.
That was my first thought too. I thought it was a new web framework at first, then an ETL pipeline library. It took me about 5 pages of reading before I found an actual description, and I only knew what it was trying to accomplish from the code example because I've done functional programming before
In plain (ES6) JavaScript I can extend the class Error with my own error-classes and then extend those further. So I can create a different error-class for ever conceivable error. I can also use "instanceof" to infer whether an error is an instance of any (recursive) subclass of the error-class I'm testing for.
Just reading the docs, but it looks like it forces these errors into compile-time checks instead of runtime checks. It makes you a lot more confident that code that compiles with run correctly and without error.
Seems like it's mostly trying do similar things to what Haskell or StandardML does, but in Typescript.
The other answers you’ve gotten are right, and this is mostly just expanding on their points. But speaking for a me, this is how I view the value proposition:
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.
I see, I think. I sometimes write JavaScript functions which instead of throwing an error return an error-instance. If the caller knows what kind of result may be returned it can check if it is "instanceof Error" or of some subclass of Error and handle it somehow.
(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.
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.
Errors/Exceptions are just way too contextual without some sort of operator to say “hey trust me I know this can’t happen”. I’d like to see Java adopt something like Kotlins !! operator rather than coloring the stack with throws clauses.
Otherwise your code is just littered with try/catch/throw new RuntimeException(ex).
!! is poison but what is priceless in the JVM is attaching an exception handler to each thread so exceptions don’t fall off ignored, mostly you don’t want to catch exceptions if you can but instead use finally as much as possible to get the state right on both happy and sad paths, if you must rethrow use a rethrower function that handles InterruptedExeptions!
Maximum Type Safety and Error Handling doesn't tell the interesting bit that it uses the type system to track errors. (And I really don't know what Maximum Type Safety is supposed to mean in the first place)
I guess to me it looks like yet another monads for JS library. I get its a marketing page but marketing to whom? Maybe there's a better document to share on HN that shows how Effect improves those features?
For me personally, I find a functional programming to be the most productive when working on a project as a solo dev or as part of a very small team that I've hired. But the downside is that it's much harder to onboard devs without experience in functional programming. It takes a while before they can be productive when introduced to a codebase that's mostly written in a functional style — one that's written in Effect for example.
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.
Oof this looks awful and I pity anyone who started projects on this. It’s so far removed from regular JavaScript you should have just written your own language. If/when this project goes under, whole codebases need to be binned.
It's one of those things that's great for a solo dev. But I'd cringe having to come into a codebase like this. And then have to learn all the pitfalls, and onboard folks and have them learn the pitfalls. . .
I also am a bit weirded out by the Effect.Effect<unknown, HttpClientError>. . . What is unknown?
`unknown` is a standard type in TypeScript for "it could be anything and you don't know what it is, so you need to introspect it and assert a type if you want to use it". (As opposed to `any`, which is "it could be anything but you don't have to introspect it to use it, you're just opting out of the type system".)
This is not "normie" TypeScript but I wouldn't characterize it as particularly scary.
It isn’t typed since the response from the http call can be anything. It could be a Todo, a generic success wrapper that contains a Todo, or maybe it was recently changed to return a Checklist instead of a Todo. Since the response can be anything we have two options, either make the type be any or unknown. any is an escape hatch typescript provides that allows anything, so if you access a field that doesn’t exists on the response you’ll get a runtime exception. With unknown typescript doesn’t let you do anything with the value until you figure out the type. In this example the next step would be to try to decode that unknown into a Todo, or fail with some error[1]. This guarantees that the object your code expects is actually what you are expecting both at compile time and runtime.
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.
At various points in my career, I've seriously considered writing an effects library very similar to this one because the benefits in a sufficiently complex system are clear to me. But ultimately, JavaScript doesn't have the functional primitives to make a library like this very ergonomic. In particular, the lack of syntactic sugar for function composition (e.g. a "pipe" operator such as |> as seen in functional languages) means every operation needs to be composed via method calls and argument passing, which adds a ton of visual noise that detracts heavily from the conceptual simplicity of the library.
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.
This is why it was hard for me to work on complex functional programming projects: execution flow is not obvious and you can't just debug/print in the middle of the function.
Repl is often sufficient, but not in cases where you need to pass some convoluted data as input.
A fair criticism. To Effect's credit, though, their OpenTelemetry integration reports usable spans (i.e. stack trace matching the mental model, not the trampolined event loop) and associates the logs with them appropriately.
You’re making two invalid assumptions that break down when dealing with a functional paradigm.
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.
Well that's the theory. Then the code becomes so complex that the type checker is not able to explore all the ramifications anymore. Or if it does then it takes an exponential amount of time, which is the worst ever. After all I guess you're right that the code won't run, but not for a good reason.
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.
Well you can't because now you declaratively represent the execution flow, and the actual execution is implicit. If you put any breakpoint in there it will break during the declaration of the execution flow rather than when the corresponding logic actually gets executed:
Plus if you are able to debug it somehow, and discover the bug in your code, can you fix it right there in your debugger?
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?
That's a typical issue when debugging the JS ecosystem, it's not really an effect problem per se, albeit since Effect comes with a runtime there's way more noise as you noticed. It doesn't help your example has only library code to test.
It is actually Effect-specific, because Effect modifies control flow in JS. For instance, if you pipe 3 functions in Effect, you'll have to step through the pipe function to get from the execution of the first to the execution of the next. Working around this adds overhead every time you debug your application. In vanilla JS, `a = foo(); b = bar(a); c = baz(b)` might not be as pretty, but it is trivial to step through.
No, thanks. I’m not gonna convert my typed scripts into haskell abominations. We need more clear and straightforward code at the syntax level of the language, not more quirky slangs all over it.
This is appealing in the first half of your bell curve. You have to recover from it a few times and start writing code.
The operator chains remind me of RxJS. Which is great! I love reactive programming. But speaking as someone who spent two years teaching RxJS to a team of frontend newbies, the learning curve is brutal.
(Please do not reply with "I found RxJS easy". Good for you.)
I once used RxJS for a dev tool platform, we had a use case where we had to take a tree-like structure of user data and recursively resolve all the async nodes.
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.
The team I was teaching struggled with the concept of streams and reactivity. It's kind of like programming backwards, especially if you're used to an imperative mindset. They just had a hard time with the concept of a stream.
They also had a tough time remembering all the operators in the library.
Hmm, I suppose I can see your point. When doing reactive things you have to think "what should this depend on". When doing imperative things you have to think "what depends on this that I need to remember to update". I always found reactive programming to be easier to reason about generally, but I suppose if someone's stuck in an imperative mindset it can be difficult to pick up reactivity.
I was going to comment something snarky, but damn, it does look good!
As someone with Elixir background, it looks really attractive to me and I was able to grasp the problem statement in just 10 seconds. Impressive presentation format!
What I really want JS/TS is one of my favorite features about Swift:
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.
Shout-out to my coworker who highlighted this banger from a wonderfully down & dirty review of effect-ts,
> 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.
I dont have an opinion on the project itself (yet), but I gotta say the website and the introduction is great.
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.
I haven't finished the whole intro video yet, but if anyone from Effect sees this, curious why y'all went with your own schema validation library? I've worked on a bunch of projects across a bunch of companies and everyone seems to have standardized on Zod. Any thought on making Zod pluggable here?
It's a good intuition. Indeed you can see it as ReaderTaskEither (albeit on steroids).
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.
This is for all intents and purposes, fp-ts 3. The projects essentially merged, Giulio Canti is working on it and fp-ts 2 is renaming methods to ease the transition to it.
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
})
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.