DEV Community

Almaju
Almaju

Posted on • Updated on

Someone finally fixed Javascript

The JavaScript ecosystem evolves at a breakneck pace. Just as you get comfortable with a certain technique, a slew of new methodologies emerges. Some, like TypeScript, gain widespread adoption, while others, such as CoffeeScript, quietly fade away. Each innovation initially stirs excitement, but over time, the community often splits, with detractors eventually spawning their own frameworks. This endless cycle has made me increasingly wary of the latest "magic" frameworks purported to solve all problems. I've shifted from seeking tools as solutions to embracing the understanding of patterns over the constant chase for new technology.

This is why I'm pointing you towards something special for your TypeScript projects, not just another tool but a paradigm that encourages good practices: Effect.

Let's take a look at why you should take the leap.

Colored functions

Have you ever asked yourself what color is your function?

Let me summarize it for you. Imagine you have blue and red functions in your code base. The rule is simple: you can use red functions inside your blue functions but not the other way. Wouldn't that be a nightmare? Now replace blue by "async". Yep, you got function coloring in Javascript.

So how do we fight this coloring thing? If we want to remove colored functions, we would need to create some sort of wrapper that will use a Promise only when needed. Like "Future" or... "Effect"?

import { Effect, pipe } from "effect";

const fibonacci = (a: number): Effect.Effect<number> =>
  a <= 1
    ? Effect.succeed(a)
    : pipe(
        Effect.all([fibonacci(a - 1), fibonacci(a - 2)]),
        Effect.map(([a, b]) => a + b)
      );

await Effect.runPromise(fibonacci(10));
Enter fullscreen mode Exit fullscreen mode

The key difference when using Effect instead of Promise lies in how concurrency is handled. Effect provides fibers, which are lightweight concurrency structures similar to green threads or goroutines. This feature allows us to perform long-running or asynchronous tasks without blocking the main thread, which can be initiated even within traditionally synchronous functions.

import { Effect, Console, pipe } from "effect";

const longRunningTask = pipe(
  Console.log("Start of long running task"),
  Effect.delay(1000),
  Effect.tap(Console.log("End of long running task"))
);

console.log("Start of program");
Effect.runFork(longRunningTask);
console.log("End of program");

/**
 * OUTPUT:
 * Start of program
 * End of program
 * Start of long running task
 * End of long running task
 */
Enter fullscreen mode Exit fullscreen mode

While Effect does not eliminate the inherent async/sync distinctions (function coloring) in JavaScript, by using fibers to handle asynchronous operations, it allows synchronous functions to invoke asynchronous effects without becoming asynchronous themselves, thereby mitigating the "coloring" problem to a significant extent.

Typesafe errors

Let's look at this function:

const divide = (a: number, b: number) => a / b;
Enter fullscreen mode Exit fullscreen mode

We just introduced a problem here, we cannot divide by zero. So let's refactor the code a little bit:

const divide = (a: number, b: number) => {
  if (b === 0) throw new Error('Cannot divide by zero.');
  return a / b;
}
Enter fullscreen mode Exit fullscreen mode

Looks good to you? It is not. Because it is not typesafe. Someone that will want to use your function won't have any idea that your function can throw. This might look trivial with a simple function like this one but when you have dozens of potential errors, it can become a nightmare. Other more mature languages have notions such as Either or Result to have typesafe errors. It looks like that:

type Result<T, E> = Ok<T> | Err<E>;

// With something like:
type Ok<T> = { kind: "Ok", data: T };
type Err<E> = { kind: "Err", err: E };
Enter fullscreen mode Exit fullscreen mode

With Effect, you will have that out of the box: Effect<T, E>. You won't ever have to ask yourself what kind of errors can occur during the run, you can know it directly from the function signature. It also comes with helper functions to recover from errors.

const divide = (a: number, b: number): Effect<number, "DivisionByZeroError"> => {
  if (b === 0) return Effect.fail("DivisionByZeroError");
  return Effect.succeed(a / b);
}
Enter fullscreen mode Exit fullscreen mode

Newtypes or branded types

You know, looking back at my previous function I realize we could do better.

const divide = (a: number, b: NonZeroNumber) => ...
Enter fullscreen mode Exit fullscreen mode

How do you define NonZeroNumber though? If you just do type NonZeroNumber = number it won't prevent people to call it with "0". There is a pattern for that: newtypes. And yeah, Effect supports that too:

import { Brand } from "effect"

type NonZeroNumber = number & Brand.Brand<"NonZeroNumber">

const NonZeroNumber = Brand.refined<NonZeroNumber>(
  (n) => n !== 0, // Check if the value is a non-zero number
  (n) => Brand.error(`Expected ${n} to be a non-zero number`)
)
Enter fullscreen mode Exit fullscreen mode

This way, you know your function cannot be called with any number: it expects a special type of number which exclude zero.

Dependency injection

If you want to follow the "Inversion of Control" principle, you might want to look into "Dependency Injection". This concept is really simple: a function should have access to what it needs from its own context.

// With a singleton
const read = (filename) => FileReader.read(filename);

// With dependency injection
const read = (reader: FileReader) => (filename) => reader.read(filename);
Enter fullscreen mode Exit fullscreen mode

It's better to do it this way for many reasons such as uncoupling things, allowing for easy testing, having different contexts etc.

While several frameworks assist with this, Effect really crushed it by making it straightforward: put your dependencies as the third parameter of an Effect.

const read = (filename): Effect<File, Error, FileReader> => {
  return Effect.flatMap(FileReader, fileReader => {
    return fileReader.read(filename);
  })
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

There are many other reasons why you should consider Effect. For sure, it's not going to be easy at first, you will have to learn to code differently. But contrary to many frameworks that make you learn "their" way of doing something, Effect actually teaches you good patterns that have made their proofs in other languages. Actually, Effect is heavily inspired by ZIO from Scala which itself has been inspired by Haskell which is still today considered as one of the pinacle of good programming patterns.

Top comments (45)

Collapse
 
tyisi profile image
TyIsI • Edited

Thanks for writing about Effect. I do really love the premise of Effect! And I do like your writing style.

But after having checked it out and looked through the code, I feel that you're comparing apples to oranges. (Adding that I don't think using Promises in a non-parallelized setup is a fair comparison. Plus, I would love to see benchmarks that support your claim that it's slower. [This was in the initial version.]) Especially considering - as I understand it - Node handles IO in separate threads while all simple/regular computational work is done in the main event-loop thread. From briefly looking into the code, Effect uses a different mechanism to achieve its effects (over Promises) that hooks into the main event-loop. (Which is why you only see one Promise resolving.)

That said, I do really appreciate the effort you've put into this article and I look forward to playing around with Effect when I have some time. And I really do hope that you keep on writing! (I really do! It was actually a great read!) Thanks again!

Collapse
 
almaju profile image
Almaju • Edited

I did a quick benchmark and the least to say is that I found interesting results. I confirm that simply putting "async" makes the program way slower. However, it looks like it might be even slower with Effect.
I suspect they might be using iterators which might be slower in JS.
Doesn't change the nature of my argument about colored function, and that Effect is pushing for good patterns. However the part about performance gain is misleading. I removed it for now while I investigate further.
Thanks for reminding me to always double check my assertions!

Collapse
 
tyisi profile image
TyIsI

No problem! Like I said, I tried to be constructive because of the knowledge event horizon - yours and mine - and I did actually like your writing style and didn't want you to be discouraged.

I only recently encountered the coloured functions description and I fully agree with the premise of good patterns. The pattern used in Go also appeals to me. The problem of inconsistency in returns is a pretty universal one in that regard. The fact that I actually spent more than half an hour trying to figure out Effect and how you got your result (AND that I responded) should speak to that! :-)

From what I could tell from briefly looking into it, Effect has its own internal scheduler (at least for some aspects) and uses mechanisms that directly tap into the event-loop (setImmediate/setTimeout).

Appreciate you putting in the extra effort with the benchmarks!

Collapse
 
nerdydeedsllc profile image
Nerdy Deeds, LLC

There is no part of me that understands why people seem to think that Typescript, an abstraction layer library written in JS is going to somehow perform better than just the native JS is capable of doing. Even async is syntactic sugar spread across the Promise interface.

This is like saying "my Ford F-250 is slower... than just the F150 I loaded into its bed would be alone!?" My ghast... it is flabbered. 😐

Whether one likes or loathes TS, I think it's reasonable to assert that we can ALL agree it's not intended to offer performance gains of any kind. Its sole and exclusive role in the JS ecosystem is to facilitate (no, to very-opinionatedly-enforce) a specific programming style. The argument being that, for those who see the benefit in same, the overhead is WORTH it.

It is as a consequence of this understanding that I am vehemently-opposed to the inclusion of additional runtime functionality. If they have a great way to obviate "colored" functions or whatever, fine. Release a library. Let people elect to run it. But TS is supposed to be TYPE ENFORCEMENT for JS (a loosely-typed language).

The second you start adding methods that do not exist in JS, you've perverted that purpose. Now people will be UNABLE to seamlessly switch between, because the agreed-upon toolbox differs. It's like randomly altering an API endpoint.

Mark my words here: this is a bad decision.

Thread Thread
 
almaju profile image
Almaju • Edited

I think you completely missed my point. To be honest, I am a bit confused by what you are saying.

I was highlighting interesting patterns that are technically completely feasible in pure vanilla JS: the duality sync/async, returning errors instead of throwing, strongly typed primitives, IoC using tagged interfaces...
It appears that the Effect library has implemented all of that, hence the highlight.

Just adding async in front a JS function will make it 10 times slower. Effect don't use Promise. It has its own async runtime mechanism. So the question about performance is actually interesting.

Collapse
 
almaju profile image
Almaju

For the record, here is the result:

Sync: Result = 832040, Time = 7.69ms
Promise: Result = 832040, Time = 272.04ms
Effect (promise): Result = 832040, Time = 344.24ms
Effect (sync): Result = 832040, Time = 353.30ms
Enter fullscreen mode Exit fullscreen mode

And the code I used to benchmark:

import { performance } from "perf_hooks";
import { pipe, Effect } from "effect";

// Sync Variant
const fibonacciSync = (a: number): number => {
  return a <= 1 ? a : fibonacciSync(a - 1) + fibonacciSync(a - 2);
};

// Promise Variant
const fibonacciPromise = async (a: number): Promise<number> => {
  return a <= 1
    ? a
    : (await fibonacciPromise(a - 1)) + (await fibonacciPromise(a - 2));
};

// Effect Variant
const fibonacciEffect = (a: number): Effect.Effect<number> =>
  a <= 1
    ? Effect.succeed(a)
    : Effect.flatMap(fibonacciEffect(a - 1), (b) =>
        Effect.map(fibonacciEffect(a - 2), (c) => b + c)
      );

// Benchmark function
const benchmark = async (name: string, fn: () => Promise<number>) => {
  const start = performance.now();
  const result = await fn();
  const end = performance.now();
  console.log(
    `${name}: Result = ${result}, Time = ${(end - start).toFixed(2)}ms`
  );
};

const n = 30;

// Execute benchmarks
(async () => {
  await benchmark("Sync", () => Promise.resolve(fibonacciSync(n)));
  await benchmark("Promise", () => fibonacciPromise(n));
  await benchmark("Effect (promise)", () =>
    Effect.runPromise(fibonacciEffect(n))
  );
  await benchmark("Effect (sync)", () =>
    Promise.resolve(Effect.runSync(fibonacciEffect(n)))
  );
})();
Enter fullscreen mode Exit fullscreen mode
Collapse
 
ricardobeat profile image
Ricardo Tomasi

Not surprising at all, since the only way to achieve "non-colored" functions is by making everything async, which seems to be what Effect does.

But it's important to note that this is not a relevant performance benchmark in general. Running everything at once is always going to be faster vs slicing up the computation, but in reality you'll have other concerns such as not blocking the event loop (either in node or the browser), allowing other queued tasks to run etc.

Collapse
 
tyisi profile image
TyIsI

Thank you for posting those!

Collapse
 
almaju profile image
Almaju

Very good point! I wanted to do the benchmark actually. I promise I will do it asap and update my post.

Collapse
 
mikearnaldi profile image
Michael Arnaldi

Disclaimer: I am the author of Effect.

Regarding benchmark it is safe to assume that Effect would be the slowest alternative when compared to any non-boxed code and to JIT optimisable code (e.g. a fibonacci over Promise), which pretty much encompass all the possible micro-benchmarks.

Reality is a bit different from micro benchmarks though, performance issues in JS applications hardly ever come from sync computation but rather from sub-optimal concurrency, given that we mostly write IO-bound apps (e.g. calling databases, apis, etc).

What Effect gives is declarative structured concurrency plus great observability, which will help you spot & fix your code bottlenecks quicker than any alternative.

That said even though Effect will always be slower to non-Effect there are still folks who render their apps at 120fps while using Effect on the rendering happy-path, so I doubt that Effect will be the bottleneck in a specific flow, happy to help with debugging any such case :)

Thanks for the article, it is a great writeup!

Collapse
 
nerdydeedsllc profile image
Nerdy Deeds, LLC

THANK you.

Finally, a voice of reason (and from literally THE most qualified person to speak it!).

Personally, I don't really have a dog in this fight either way. Use your tools to most effectively GET THE JOB DONE. If you don't LIKE your tools? You're. A. Programmer. FIX that affront.

But to hear the author stand up and say clearly and concisely "this is what it was made for, and if y'all insist on bullying it into something beyond its intention you're missing the point" is refreshing as hell.

Kudos to you, m'man. Both for putting something out there and for being entirely rational about it. Mad respect.

Collapse
 
almaju profile image
Almaju

That's literally why I removed the part about performance from the article.

Collapse
 
ludamillion profile image
Luke Inglis

Some, like TypeScript, gain widespread adoption, while others, such as CoffeeScript, quietly fade away.

This sentence might be the most meaningful one in this whole writeup. Though I'm not sure you meant it as such.

I say this because all three: Coffeescript, Typescript, and Effect have a similar purpose and dynamic. My hope for both Typescript and possibly Effect is that they too go the way of Coffeescript.

Coffeescript was created at a time before the modern age of JavaScript. It set the groundwork for many of the improvements that were formalized as part of ES6. It 'quietly faded away' because it was no longer needed. (Though not entirely true as it does feature niceties not in vanilla JS)

Typescript could end up following the same path. Proving that there is something missing from JS that people really do want and pushing forward the inclusion of that in vanilla JS. Typescript as a language could well end up 'fading away' as well.

I haven't played around with Effect yet but I listened to a fairly extensive interview with one of the major players (on Syntax I believe) and he made a compelling argument for its approach. It might well be a library that demonstrates a paradigm that should be supported.

I'll be interested to see what happens with it. Thanks for the writeup.

Collapse
 
almaju profile image
Almaju • Edited

I like what you say! Not necessarily what I had in mind but I totally agree. Reminds me of the proposal for types (without any compiler or toolchain though) in vanilla JS. Typescript should disappear when Javascript swallows it.

Collapse
 
matiaslauriti profile image
Matias Hernan Lauriti • Edited

This article showed up on my chrome recommended cards, and I had a look until some point as I am not a frontend developer, I use PHP/Laravel, and I know JS, but also know some C#, Python (a bit) and Rust (a bit)...

I cannot understand why JS is not upgraded... You needing to do all this crazy stuff that was solved ages ago in a lot of other languages... Why can't the JS concortium finally upgrade JS to a "real" high level language like other languages?

I see the community, time after time, re implementing basic stuff... I would really love to see TS already integrated into JS natively... And more...

Collapse
 
latobibor profile image
András Tóth

Long story short: browser wars. That's why we can't have already a language natively work in the browser that is compatible with JS, but is designed and tested well.

You can come up with anything, but if Google, Apple or Mozilla won't implement it, it won't happen. We are still plagued with language features that only work in 1 special browser only.

In my opinion JS and CSS are old enough to be replaced with language purpose-built for scaling enterprises applications: you have all those years of high-usage to know what features should be implemented.

Collapse
 
nerdydeedsllc profile image
Nerdy Deeds, LLC

So... your position here is: "I say we break the totality of the existing Internet in favor of a slower, still-ocassionally-buggy alternative that is itself running on the original base you're trying to do away with"?

...and for reasons that amount to, "because everyone should have to code like me, because it's easier for me to understand that way!"?

I have that about right?

Yeah. Good luck with that.

Thread Thread
 
tyisi profile image
TyIsI

Not entirely sure why you seem to take this abrasive stance towards folks who are simply commenting about the state of the world as it currently is. I think we all feel the frustration with the system as it is. No need to shoot the messenger for it.

Because, yes, the browser companies are effectively gate-keeping progress. But at least nowadays we have more direction through ECMAscript. And yes, we don't really have any great alternatives. (ActionScript could've been great for this, IMHO. Java was great as a concept but ultimately carried too much of a resource burden to make it really viable for adoption on the level that JS could.)

Should we make C#, Go, Erlang, Haskel, Lisp, PERL, PHP, Python and Rust compilers in the browser so that we can extend the ecosystem with all the languages we desire? (We might as well build a WASM runtime C/C++ compiler while we're at it.)

And at the same time, transpiler optimization and run time compiler optimization also haven't stood still.
Various compilers get optimized all the time.

And it's not like WASM is a viable alternative at the moment. Not without taking a performance hit.

So, I am not entirely sure what issue you're exactly trying to get at?

Thread Thread
 
latobibor profile image
András Tóth

I'm also not sure what Nerdy Deeds wrote. From other comments it seems to be a person not getting what TypeScript is for.

My comment was about the lack of choice; you can't get a more performant language, you can't get a higher level language: you have JS and WASM. I contrast it with any other operating system, where you have a huge array of languages.

My advocacy here is for understanding the following:

  • the browser became a mini operating system, therefore we should treat it as such
  • languages (however beloved) in the browser were designed during a much different stage of the internet
  • both enterprise scale web applications and interactive UI applications are limited by the current status quo
  • even though these limitations are somewhat mitigated by an awesome community we are mitigating instead of purpose-building; my analogy here would be that you can mitigate bad transportation by chaining smartly together many trucks, but it would be better to transport cargo on rails
Collapse
 
almaju profile image
Almaju

Someone give this man a microphone!

Collapse
 
accplan profile image
accplan

I don't think Effect solves colored function problem in your example: you just added another type of functions that return Effect instead of Promise. Of course, we can run effects from regular functions, but we also can do that with promises (but you have to handle the results with .then() and .catch() if you care about them). Or we can get rid of sync functions and promises altogether, leaving only effects, but without effects we can leave only promises as well. I don't think leaving only red functions is the solution to blue/red problem.

BUT(!) Effect actually solves this problem the other way: you can start an effect from any regular function and forget about it, letting another fiber (goroutine equivalent) to handle the result, so a "red" function doesn't change the color of a "blue" function.

Collapse
 
almaju profile image
Almaju

Fair point! I'll update the article.

Collapse
 
goteguru profile image
Gergely Mészáros

Hmm... and how it is not the case in standard javascript? if you call an async function in a standard function (which is absolutely possible) it will be run in the default event loop (in micro-task-queue) which is pretty much fire and forget. What is the practical difference? If I want to do something with the results, I could easily wrap the call with some async queue function and that's all.

Collapse
 
accplan profile image
accplan

Yeah, you are right. Seems like effect doesn't solve red/blue problem at all.

Thread Thread
 
almaju profile image
Almaju

Yeah, touché. You can technically manage a Promise inside a sync function.

Quick experiment of thought: what if we'd just convert all of our codebase to Promise ? Promise states are hard to keep track of, hard to cancel, you need to catch the error, they have eager evaluation... Also we would still have to use regular functions because of standard methods like let's say Array.filter whereas Effect provides standard methods to use both sync and Effect functions.

So while I agree that you cannot completely solve the function coloring problem in JS, I still think that Effect helps us closing the gap even if that difference is thin. I will update the article in that sense. Thanks for the comment!

Collapse
 
johndunlap profile image
John Dunlap

It's a shame we are forced to use this farce of a language where problems that have been solved for decades have to be continually reinvented.

Collapse
 
almaju profile image
Almaju • Edited

I have good hopes for Rust with wasm on the frontend! Also keeping an eye on Gleam which can sort of compile to JS.

Collapse
 
sshark profile image
TH Lim • Edited

While Effect is heavily influenced by ZIO, as you said, the error handling you described here is a common pattern of handling error without throwing an exception. It is not from ZIO. In Scala, it is handled by Either and its subclasses Left and Right

Collapse
 
latobibor profile image
András Tóth

One thing to note here about async programming. The keyword await is to force a certain timeline to happen. You can use await strategically, when you do need the result as input for yet another async function. I don't know how Effect is implemented, but I think if you used Promise.all well in one of your examples you would have had a similar timeframe.

(Note, that in the effect Fibonacci example you wrote n === 1 which does not seem to be correct.)

Collapse
 
firecrow8 profile image
Stephen Firecrow Silvernight

I liked the beginning, I'm not sure JavaScript needs fixing though, the challenge I see is how many things try to hide the power of JS instead of embracing it.

This looks like a lot more layers and I'm mostly excited about the idea of composability, which usually involves simpler components than this. I personally prefer a for loop to the forEach function, which shows how much of native JS I prefer.

if you write a follow up about why you think CoffeeScript didn't pan out, and why TypeScript reigns supreme in a lot of ways, I'd be interesting to read that.

for example: I like the divide by zero handling idea, not 100% sold that the function signature is the right place for it though.

Thanks for posting it was an idea generating read!

Collapse
 
diskcrasher profile image
Mike

Instead of band-aiding JS maybe it's time to just use a real typed language like C# or Java.

Collapse
 
almaju profile image
Almaju

Like C# or Java haha, thank you for the good laugh!

Collapse
 
diskcrasher profile image
Mike

And you find this funny why? Typescript is basically modeled after C# but far more difficult to master.

Thread Thread
 
almaju profile image
Almaju

:)

Collapse
 
svsubith_51 profile image
Subith

True that. JS community wants some problems to not be fixed. So that the community can keep reinventing and creating contents!

Collapse
 
nerdydeedsllc profile image
Nerdy Deeds, LLC

Amen. Give THIS man a microphone.