Readit News logoReadit News
reactordev · 2 years ago
Not to be a dog in the fight but…

How is:

   const count = (amount: number) => (amount > 0 ? [...count(amount - 1), amount] : []);
Succinct and better? It’s fewer lines. It uses recursion. But it’s obtuse and illegible syntax diarrhea that takes more time to grok.

The for loop example above, while simple, is easily understood. A junior engineer could fix it. Is it faster? No. Is it easier to understand? Yes. I like the final example as well.

Look, I’m all for being clever when cleverness is needed but some of you JS/TS folks take it to the extreme. I want to be able to read my codebase, not perform archaeological studies. I have the same issue with Rust syntax as well in places.

I will praise the article on this though, optimizations do matter and having something execute orders faster will improve your overall experience. It doesn’t have to be cuneiform. If you must be clever, wrap it in an abstraction that is well documented because you don’t live forever and neither will your code, make it easy(ier) on the next person. While you can nest a recursive function in a tertiary statement coro, don’t. Fire is cool but also burns.

Btw, bun is blazingly fast. I look forward to more.

kannanvijayan · 2 years ago
I’m surprised no one has brought up the fact that this code has quadratic time complexity when the underlying algorithm could be linear.

And the fact that it’s allocation behaviour is horrible, leaving behind ‘count-1’ garbage objects, count-squared garbage memory, and forcing the GC to chase after the algorithm.

I can accept this example as a contrived scenario to talk about TCO… but even in the presence of TCO one would not want to implement the function this way.

reactordev · 2 years ago
I wasn’t even going to dive into the allocations and garbage this style brings but it’s orders of magnitude slower, with more garbage created, disposed of, than a simple if statement. I’m glad you opened the can of worms. There are times when this doesn’t matter. Reading a config json, or performing your startup bootstrap, but if this was executed during a request or during a frame then you’re going to have a bad time.
djur · 2 years ago
The article doesn't explain why (and it should) but it does mention that this version is very slow and offers a version that doesn't have the same problems.
nsonha · 2 years ago
looks linear to me? Takes n calls, each is O(1).

> the fact that it’s allocation behaviour is horrible, leaving behind ‘count-1’ garbage objects

Topic is TCO, which is kinda about that.

mhink · 2 years ago
> Look, I’m all for being clever when cleverness is needed but some of you JS/TS folks take it to the extreme.

To be fair, this is very much a toy example meant to demonstrate tail recursion- although I do agree it's not a great example considering you need to understand the evaluation order of ternaries in order to understand why one version is tail-recursive and the other isn't.

That being said: generally speaking, one might argue that using recursion at all is "too clever" and algorithms should just stick to iteration altogether. ¯\_(ツ)_/¯

> While you can nest a recursive function in a tertiary statement coro, don’t.

Man, why do you gotta call me out like this? :D

akoboldfrying · 2 years ago
>algorithms should just stick to iteration altogether

Well, if your algorithm performs 2 or more recursive calls per function invocation (like, say, quicksort), at least one of them needs to be an actual recursive call that uses additional stack space, since TCO is only able to mitigate a single recursive call per function invocation. (You could simulate your own stack and use iteration... but the only reason to do so in practice would be to escape some artificial restriction on native stack size.)

reactordev · 2 years ago
>”Man, why do you gotta call me out like this?”

Because you lose the stack trace most of the time. Only God and you know where the Uncaught PromiseRejection happened. It’s just really hard to debug when things go south. This drives me bonkers with the AWS sdk.

satvikpendem · 2 years ago
If one is well-versed in functional programming, they would understand the above function just fine, plus it comes with all the benefits of functional versus imperative styles, which I will not repeat here as there are lots of articles on this. Perhaps JS devs should spend more time in functional codebases rather than imperative ones and they'll also get used to such code, and I for one am glad to see TCO making this style more efficient compared to traditional for-loops.
djur · 2 years ago
This style isn't efficient in JS, though, because it produces a ton of garbage to collect. TCO doesn't really make it more efficient, just possible.

Deleted Comment

cowsandmilk · 2 years ago
> Perhaps JS devs should spend more time in functional codebases rather than imperative ones

the pure functional version takes 700x longer to run than the imperative version; maybe the wins of functional programming just aren't there in JS engines today.

Deleted Comment

reactordev · 2 years ago
If one was well-versed in memory management and how the stack would blow up in production one would not have to repeat them here.

Also, if you have to justify cleverness, it’s not clever. Don’t defend the style by grandstanding on “functional is better” or “you don’t know func bro”, how about ask why I feel the imperative style is more legible? Why would this not be the best way to write this? Where can I write this way? Or even “I find this style to be legible to me, but I’m curious about why you think it’s not”.

mitt_romney_12 · 2 years ago
If you replace the ternary with an if then it's easy to understand for anyone who knows what ... means (which imo every JS developer should know)

  const count = (amount: number) => {
    if (amount > 0) {
      return [...count(amount - 1), amount]
     } else {
      return []
     };
  }

reactordev · 2 years ago
You missed the point entirely I’m afraid. It’s not about replacing the if with …, it’s about complexity.
femiagbabiaka · 2 years ago
The ubiquitousness of c-style makes it more familiar. But that doesn't make it better syntactically. And of course what's faster or more memory efficient is an implementation detail ultimately, but moving away from the imperative style opens up the opportunity for new and interesting -- and potentially more grokkable, ultimately -- patterns. Rust is actually a great example of this, with things like match and pattern matching.

Don't be such a language grinch.

reactordev · 2 years ago
Talk to me about lifetimes of Boxed RC Mutex then… because last time I checked, I had to do some insane amount of <T> to get a concurrent collection synchronized. I’m all for functional. Just don’t over use it in places where a simple function would suffice and try not to use it when dealing with recursive allocations of temporary arrays. Rust has come a long way but I feel like some of their style choices were to feel superior rather than make something functional. Go is a great example of simple, functional, but not quite as robust - as Rust.
Jarred · 2 years ago
(I work on Bun)

A neat thing about tail calls in JavaScriptCore: there’s an explicit bytecode intrinsic for it.

  return @tailCallForwardArguments(MyConstructor, this);

To use this in Bun, you’d have to start Bun with the environment variable “BUN_JSC_useDollarVM=1” and then $vm.createBuiltin(mySourceCodeString)

When using this intrinsic, if any of the arguments are incorrect or it cannot otherwise enable it, the entire process will probably crash. In debug builds of JSC it will have a nicer assertion failure but that is not enabled in release builds

Example code: https://github.com/WebKit/WebKit/blob/17351231b4dedb62d81721...

also happy to answer any questions about Bun

leptons · 2 years ago
When do you see AWS Lambda supporting Bun, if ever?
Jarred · 2 years ago
You can use it today with containers in Lambda or a custom Lambda runtime, but both approaches have embarrassingly slow cold start. Lambda has special optimizations for Node which are not possible without official support.

I have no idea when Lambda would add an official runtime layer. But if you work at Lambda and are interested, feel free to email me - jarred@bun.sh

CodeGroyper · 2 years ago
Love bun, thank you so much Mr. Sumner.

What is the priority of app router integration for NextJS?

Jarred · 2 years ago
After Windows support is further along, but not that long from now
johnfn · 2 years ago
> What is the priority of app router integration for NextJS?

I'm not Jarred, but what do you mean by this? I'm using bun + app router + NextJS and everything seems to work just fine.

MrJohz · 2 years ago
I think this article actually does a really good job of explaining why TCO hasn't really been an important part of the JS ecosystem.

There's a lot of situations where recursive algorithms are really neat and clear. I don't know if this is the best example, but it shows the benefit of being able to split logic into a base case and a recurrence case.

But, in my experience, an algorithm that elegantly fits a recursive relationship is rarely one that naturally fits the tail-call paradigm. Often part of the benefit of recursion is that you can store state in the stack - the very thing you need to avoid to use TCO.

This means you often need to put in effort to create a tail recursive algorithm. But that often ends up looking a lot like the imperative case anyway - an accumulator outside the loop that you either mutate manually, or update in a tail call. And in my experience, the mutating, imperative version is usually then the easier to read and write (assuming you can keep mutations to a given scope, and not have that state leak all over the place). (In fairness, this might be more familiarity, though.)

In the light of this, what is the advantage of TCO? In functional languages without mutation, it's pretty important to allow for functions to act on arbitrarily-sized inputs without constantly growing the stack. But if we have mutation, it's really just a different way of writing the same code. And if that different way is generally less clear and almost always less performant, it probably isn't a very useful choice.

Which is why I think TCO hasn't really caught on in the other JS engines. It's a cool idea, and there's definitely a handful of cases where it's the useful way to go, but usually you'll be better served by writing things in the more traditional JS way.

tikhonj · 2 years ago
Tail calls also matter for code in continuation-passing style (CPS).

This matters for some styles of programming (callbacks/promises/monads/etc), but it's also very useful for code emitted by compilers and DSLs. It's much easier to encode complex control flow with CPS than just with the basic constructs JavaScript supports.

Even for hand-written code it can be pretty important. It's easy to imagine rewriting simple tail-recursive functions in an imperative style, but it quickly gets much harder, especially if your problem naturally decomposes into mutually recursive functions: that is, instead of a function calling itself, you have multiple functions calling each other, often with a bunch of additional computation between each call. I've run into this when writing parsers as well as custom algorithms for constraint-satisfaction kinds of problems. (Which are pretty common!)

pjmlp · 2 years ago
Regardless, it has been approved as part of JavaScript specification and Chrome folks refuse for political reasons to implement it, which given the market domination everyone has helped them achieve is a bummer.
MrJohz · 2 years ago
Firefox have also chosen not to implement it, right? Indeed, my impression was that Firefox were far more against the whole thing than Chrome, who did implement TCO but then removed it for various reasons.

My impression is that this is less an issue with any particular implementor, and more with the feature not being fully thought through at the beginning. A lot of the browsers (including the Edge team at the time) ended up running into issues, on top of the UX/explanatory problems. That's why there was the alternative proposal for explicit tail calls, but those didn't go anywhere.

tomashubelbauer · 2 years ago
I find it surprising that the JS specification also includes optimizations an engine should implement. I'm not sure how difficult TCO is to implement, but I just checked if QuickJS supports it or not and it seems to be one of the few ES2020 omissions the author chose to make: https://bellard.org/quickjs/quickjs.pdf (3.1.1). This tells me it might be non-trivial to implement which if true makes the decision to make it a part of the specification all the more surprising, because that means the specification restricts the types of trade-offs implementers can make when attempting to achieve full compatibility. In other words, until today I was under the impression you could make a fully ES compatible engine, but choose to make it slow for implementation ease. Looks like the spec defines the floor for how (non)-performant the engine implementation can be in some cases. Is this common in other languages' specs?

Edit: Oh, seeing the sibling comment, are TCO and "tail calls" different things? If so, I remain unclear on the status of TCO support in QuickJS.

spankalee · 2 years ago
What "political" reasons?

My understanding of the situation is that it's entirely technical issues that came up after implementing.

Deleted Comment

tonyg · 2 years ago
> an algorithm that elegantly fits a recursive relationship is rarely one that naturally fits the tail-call paradigm.

That's because tail calls are structured gotos (with arguments). They're not really about recursion or even iteration.

> what is the advantage of [proper tail calls]?

There's a loss of expressiveness [1] without them, similar to the loss of expressiveness when there's no garbage collector.

> if [using proper tail calls] is almost always less performant, it probably isn't a very useful choice.

Don't assume PTC is less performant. There's no intrinsic reason for it to be. After all, the machine should be doing (slightly) less work: a goto instead of a call. Retrofitting it to an existing system can of course lead to a poor implementation, but it is not inherently a slow construct.

[1] Felleisen, Matthias. “On the Expressive Power of Programming Languages.” Science of Computer Programming, vol. 17, no. 1--3, 1991, pp. 35–75, https://doi.org/10.1016/0167-6423(91)90036-W .

MrJohz · 2 years ago
To be clear, I'm not talking about PTC/TCO in general in this comment, I'm talking about it specifically in the context of Javascript, and in the context of the common patterns of Javascript usage.

That is, PTC doesn't just need to be powerful than other forms of call, it needs to be more useful day-to-day than other ways of writing the same logic. I think this article shows that, at least with the common recursive examples, that's rarely the case - it is pretty much always clearer and significantly quicker to use loops and Array methods than it is to rely on PTC and recursion.

You make a good point that there are uses for PTC outside of recursion, and that there are things you can do with it that can't be done with existing constructs. I'm sure that's true, but in my experience, the things people want to do with these tools tend not to fit well with the other features of Javascript, such as the high degree of mutation, and the dynamic nature of the language. At which point, it's probably more useful to create a compile-to-Javascript language instead that uses these sorts of features internally.

But with the rise of WASM (which has PTC as a proposal that I believe some groups have already implemented), it makes a lot more sense to use WASM as a target instead. There, you have a lot more control over exactly what gets executed and how, along with generally smaller and faster code. The exception right now is obviously DOM manipulation, but with the GC work being done right now, the path is opening up to getting easier access to the DOM directly in WASM.

odyssey7 · 2 years ago
I don’t think the example is particularly good. TCO is essential in functional programming, where recursion shines, but the example given is an impure function which mutates its input parameter by calling Array.prototype.push. Basically, this isn’t a demonstration of the paradigm for which TCO is required.

We’ll never know how big of a role TCO could have played in JavaScript over the past several years, because it hasn’t been available outside of some narrow contexts.

MrJohz · 2 years ago
I agree that TCO is essential in functional languages where loops and mutability are disallowed or at least heavily discouraged, but Javascript is not one of those languages. In Javascript, both mutation and and loops are very simple, and as I think the article demonstrates, often the clearest ways to express an idea.

Where recursion is clearer (for example, when dealing with recursively nested data structures), the clearest expression of a function usually isn't tail recursive - that often requires rewriting the function to put more state into the function arguments.

This means that tail recursion rarely matches with patterns used regularly in Javascript. That's not to say it's useless - if you want to keep to a particular functional style, or if you're compiling to Javascript from a language that emphasises recursion, then it's a useful tool to have. But in the former case, you're generally going to struggle with browser engines not being optimised for this style, and in the latest case, you're probably better off targeting WASM directly.

o11c · 2 years ago
The real problem, of course, is that in the cases where compiler-based TCO is possible, the user can trivially refactor the function to use a loop.
akoboldfrying · 2 years ago
>I think this article actually does a really good job of explaining why TCO hasn't really been an important part of the JS ecosystem.

Nicely put. TCO is a bit like a guitar solo: more fun to implement than it is useful/enjoyable to the intended recipients.

pcl · 2 years ago
> Recursion takes up precious memory on the call stack! There may be commands to increase the call stack size for your program, but there is only so much the OS will allow. Memory on the heap is much less restricted.

This reasoning implies that tail call optimization is some sort of memory-segmentation workaround. This isn't accurate.

A (non-TCO) recursive approach is more expensive because it uses O(n) bookkeeping memory, whereas the iterative approach uses O(1) memory. Naive recursion will allocate a stack frame for each "iteration", whereas the loop approach will not allocate anything per iteration.

So, the iterative approach simply uses much less memory for large iteration counts; the placement of the memory in stack space or heap space isn't the relevant factor.

fyrn_ · 2 years ago
But without TCO, even if your function doesn't have any state of it's own, it will use memory on the call stack. I don't think anything about what they said implied memory segmentation was the problem. It's pretty clear they mean each recusion increases the size of the call stack without TCO.
rezonant · 2 years ago
Agreed, I think the mention of heap space was more to answer the question of someone new to this topic of "but wait, I can allocate gigabytes of data in an array, why can't I allocate 100,000 stack frames?"
chrismorgan · 2 years ago
This is not actually about Tail Call Optimisation, which is more flexible and optional matter of optimisation, but about Proper Tail Calls, which are a guarantee made in the ECMAScript 6 specification (over implementer concerns objections)—in strict mode, calls in tail position must not create additional stack frames. This is the last piece of ECMAScript 6 that most engines haven’t implemented, because it’s rather controversial: it actually causes some performance problems, and makes debugging harder, and may have security issues (in 2016, Mozilla declared it impossible to implement across realm boundaries due to their security model).

https://github.com/tc39/proposal-ptc-syntax has a lot of useful information about it all, and a proposal to make it explicit in syntax, such as with `return continue …`, though I don’t believe that’s really gone anywhere. The practical situation is that this is the one part of the ECMAScript spec that most implementers ignore, and thus which you can’t depend on. Which says to me that it should either be made optional or be removed from the spec. Not sure if there are any other features similarly disregarded. ECMAScript specification policy was different in those days, they operated more independently of implementers. (HTML was like that once too—that’s roughly why WHATWG was formed, because the actual implementers weren’t happy with how things worked in W3C, so they took matters into their own hands.)

(Fun terminology problems here. The term TCO is commonly used for PTC, and PTC is very close to being a subset of TCO, but the mandatory stack frame elision which ruins debugging feels to me like it falls outside of TCO. In various situations, debuggers will mark things like “stack frame omitted” when they’ve optimised one out of existence, but you can generally compile things differently, or something like that, to prevent this. (Compiled/dynamic language variation showing up here.) But with PTC, it feels like the engine is kinda not even allowed to know that a stack frames may be absent. So I say PTC and TCO are a little distinct, though PTC is mostly just a subset of TCO. Reminds me of the terminology of tree-shaking versus dead code removal—where the former is essentially a subset of the latter, but that the effects are just slightly different, though I’d say it’s more slight in that case than this.)

imjonse · 2 years ago
Related: Guido van Rossum's argument for why there is no TCO in Python.

https://neopythonic.blogspot.com/2009/04/final-words-on-tail...

sbarre · 2 years ago
TCO in this article = Tail Call Optimization, not Total Cost of Ownership.

It makes the article more interesting!

smarx007 · 2 years ago
Actually, as a chiefly Java/C# person, I was hoping for the article to cover how Bun helps lower the Total Cost of Ownership of Javascript projects by helping to get off the hamster wheel of infinitely deep npm dependency trees, wild west of constant project deprecations and major version releases with immediate deprecation of previous versions, daily game of CVE alert whack-a-mole etc. Preferably, short of writing all the code yourself.
sbarre · 2 years ago
I also assumed the other TCO when I clicked through, which would have been an interesting take to read for sure (particularly since Bun is so new, so I was wondering how much data would be backing this analysis), but I'm not a strong functional programmer so any time someone covers functional-adjacent topics, it's interesting to me!

I'm with you on wanting to learn and explore more about Bun's main advantages though, the bundled tooling and some of the convenience APIs feel like well-planned time/effort/maintenance optimizations.

gerikson · 2 years ago
It's also not Tjänstemännens Centralorganisation, the largest white collar union in Sweden, and also the org behind the TCO labelling of computer monitors.
sbarre · 2 years ago
If you google TCO, at least where I am (North America), "Total Cost of Ownership" is the first result - and the dominant one on the first page of results.

It's also the main definition of TCO that most people are probably familiar with, me included.

This felt like a reasonable clarification to make.

Deleted Comment

imjonse · 2 years ago
It's nice, but the downside is that relying on TCO can make your Bun-tested code unexpectedly break with stack overflow error if it ever gets run on Deno or Node. So probably a good idea to comment this part of the code and make sure if it's in a library to similarly warn the callers somehow.
quickthrower2 · 2 years ago
Yes the O is more than an Optimisation, it is a change in semantics.
bryancoxwell · 2 years ago
Does serverside JS have anything similar to #ifdef?
vlovich123 · 2 years ago
Yes. You can define a constant (eg IS_SERVER) which your bundler (eg esbuild) replaces with a specific value depending on what environment you’re targeting. There are also ways to detect it using runtime globals that are only defined within the browser although that can be monkeypatched by your own code or 3p libraries you import (not common but could be a compat layer you add to make server side JS behave more like a browser).
antipurist · 2 years ago
Something like

    if ('Bun' in globalThis) ...
    if ('Deno' in globalThis) ...
    if ('window' in globalThis) ...
could be used to determine the runtime.