Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Effect – Build robust apps in TypeScript (effect.website)
127 points by PaulHoule on June 14, 2024 | hide | past | favorite | 74 comments


I've used Effect-ts for a little under a year.

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.

¹ https://github.com/golemcloud

² https://www.dbos.dev


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.


This takes 0 reasoning. You’ve given it an exact map to follow to the answer.


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.


This is essentially Scala's ZIO for Typescript.

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.


Buried pretty deep in the docs: “Effect's major unique insight is that we can use the type system to track errors”

This website should probably start with this. Instead it’s a bunch of like, modern synergy-ese.


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.

How does EffectTS improve upon that?


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.


It makes the errors that a function can fail with part of that function's type.


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.

Is there something even better with EffectsTS?


You can but should you in some particular way?

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.


I'd say if you have checked exceptions, you need great ergonomics to handle them, transform them and ignore them.

Java has poor ergonomics for these, and in my opinion languages like Rust have better ones and that's why they manage with Results.


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!


I’m not sure !! is poison. I see it the same as rust’s .unwrap() calls.


From the main page:

https://postimg.cc/sBQB37h6

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.


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 can imagine, how would you improve it though?

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.


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?


Algebraic effects are the new hot craze in FP circles. That’s exactly who this is marketing to. People who want monads/pure fp in js.


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.


I know what unknown is but why isn't it typed? I don't see it as an improvement from the well typed thing on the left.


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.

1. https://github.com/Effect-TS/effect/blob/main/packages/schem...


Reminds me of old React codebases that used mobx :')


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.


Ok this is cool and all but where do I put my breakpoints now when something unexpected happens and I need to debug production code?


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.


You do it the same way you do it in any JS application.


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:

  Http.request.get(`/todos/${id}`).pipe(
      Http.client.fetchOk,
      Http.response.json,
    )


You put the breakpoint where you run it with Effect.runX then.


Yeah, and then it won't step you through your code but through unintelligible abstractions and Effect internals. Good luck debugging.


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.


> (Please do not reply with "I found RxJS easy". Good for you.)

What about "what makes it hard"? I've never tried RxJS before, so I'm curious to know where the learning curve is.


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.


That's basically Rust's `?` operator, along with its upcoming Try trait


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.

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.


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.


I liked the plot comment "Complexity(Lower is better)" because in Javascript community it seems like not so obvious to many people


A bit on the side:

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?


Zod is a great library, you can use it with Effect without any issues!

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.


Indeed both Zod and Schema are great with Effect.


This is nothing but just TaskEither that has existed (among others incredibly useful things for years) in fp-ts.


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.


Why would you want to simulate threads instead of using something like actors instead?


Without do-notation this is kind of dead on arrival no?


Pipe/do pipe function [0].

Also pipe operator is coming soon [1].

[0] https://effect.website/docs/guides/style/do

[1] https://www.proposals.es/proposals/Pipeline%20Operator


They actually do a decent impression of one using generators: https://effect.website/docs/guides/style/do#using-effectgen

  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
  })



Reminds me of Result in Rust.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: