15 ms·
Ruby methods are colorless
- racketprogram 2y ago[flagged]
- skywhopper 2y ago[flagged]
- hyualsdfowjasd 2y agoSadly didn't have to turn out this way if people had used Chris's work.
- thechao 2y agoI've implemented coroutines in C and C++; my preferred multitasking environment is message-passing between processes. I'm not quite sure what the async/await stuff is buying us (I'm thinking C++, here). Like, I get multi-shot stackless coroutines, i.e., function objects, but I don't get why you'd want to orchestrate some sort of temporal Turing pit of async functions bleeding across your code base. I dunno. Maybe I'm old now? Anyways; good for Ruby! Async/await just seems very faddish to me: it didn't solve any of the hard multithreading/multiprocessing problems, and introduced a bunch of other issues. My guess is that it was interesting type theory that bled over into Real Life.
- anonymoushn 2y agoIf you have hundreds or thousands of connection-handlers or scripts-attached-to-game-objects, then it can be useful to write code in those without dividing the code into basic blocks each time I/O might be performed or time might pass in the game. I generally agree that manually migrating everything to "be async" in order to achieve this exposes at the very least a lack of belief that computers can be used to automate drudge work.
- mikepurvis 2y agoI really like the patterns that it enables for situations with a lot of parallel IO. Easy example is any kind of scraper, where you're fetching large numbers of pages and then immediately processing those to generate more requests for the links and assets referenced within.
- skywhopper 2y agoMy theory is that JavaScript programmers who were forced into thinking this way for decades with their single-threaded runtime have infected other languages with the idea that this style of coding is not only good but furthermore that it needs to be explicit. Thank goodness we have wiser language developers out there who have resisted this impulse.
- Erem 2y agoDidn’t async/await originate in c#?
- explaininjs 2y agoF# first actually. Then C#. Then Haskell. Then Python. Then TypeScript. Parent just has an axe to grind.
- jerf 2y agoasync/await may have originated as keywords in C#, I don't know, but the programming style itself is ancient. Almost every GUI ever written is programmed in the style. Network servers were written this way back when threads were just a fever dream of academics yet. The entire Mac OS ecosystem prior to OSX had to be written in this style because time was cooperatively shared across the entire OS. This is one of the most blatent "failure to learn from our history" debates I know of, when people act as if the question started with Javascript just a few years ago, when in fact the programming community has experience with this style that predates when most of us were born. I'm getting to old fogey myself and it still predates my 1978 birthdate. I was never even tempted to get into Node because I'd already made an async/await-style mess in Perl before Node was even released, and the mess had everything to do with the programming style and little to do with Perl qua Perl, and the libraries I used were already old and well-established then, if not outright long in the tooth. There is nothing new about this, except keywords.
- explaininjs 2y agoComing from a heavy TS background into a go-forward company, I’d say the main thing you get with async is it makes it incredibly obvious when computation can be performed non-sequentially (async…). For example, It’s very common to see the below in go code: a := &blah{} rA, err := engine.doSomethingWithA() b := &bloop{} rB, err := engine.doSomethingWithB() This might have started out with both the doSomethings being very quick painless procedures. But over time they’ve grown into behemoth network requests and very thing is slow and crappy. No, it’s not exactly hard to spin up a go routine to handle the work concurrently, but it’s not trivial either - and importantly, it’s not immediately obvious that this would be a good idea. Contrast to TS: let a = {blah} let [rA, err] = engine.doSomethingWithA() let b = {bloop} let [rB, err] engine.doSomethingWithB() Now, time passes, you perform that behemoth slowing down of the doSomethings. You are forced by the type system to change this: let a = {blah} let [rA, err] = await engine.doSomethingWithA() let b = {bloop} let [rB, err] await engine.doSomethingWithB() It’s now immediately obvious that you might want to run these two procedures concurrently. Obviously you will need to check the engine code, but any programmer worth their salt should at least seek to investigate concurrency when making that change. I wouldn’t be bringing this up if I hadn’t made 10x+ performance improvements to critical services within a month of starting at a new company in a new language on a service where the handful of experienced go programmers on the team had no idea why their code was taking so long.
- hombre_fatal 2y agoOf course, the other nice thing about the JS example compared to Go is that it's trivial at the callsite to do this: const [[rA, errA], [rB, errB]] = await Promise.all([ engine.doSomethingWithA(), engine.doSomethingWithB() ]) At least these days you can ask an LLM to write the WaitGroup boilerplate for you in Go.
- explaininjs 2y agoIndeed. And breakpoints and stepping across concurrent context actually works in JS, which is nice.
- 2y ago
- DarkNova6 2y ago> Anyways; good for Ruby! Async/await just seems very faddish to me: it didn't solve any of the hard multithreading/multiprocessing problems, and introduced a bunch of other issues. My guess is that it was interesting type theory that bled over into Real Life. I think what happened what that JavaScript absolutely necessitated the addition of async/await to avoid callback hell (due to its single-threaded mandate)... just to create a new kind of hell all of its own. But before the long-term consequences within large code bases could be observed, the cool kids like C# & friends jumped on the bandwagon believing that "more syntax equals more better code".
- incrudible 2y agoJavascript necessitates async because of its focus on UI. You just can not have good UI while blocking on the main (or UI) thread, you will inevitably have something like async and callbacks, so you might as well embrace it. Whether that is a good tradeoff for a server is a different question.
- DarkNova6 2y agoWhat you need is concurrency. And async/await is just one form of concurrency. I don’t see why you would need threads to create a stackful coroutine implementation. However, what you would indeed need is a much more heavy runtime.
- rpeden 2y agoAsync and await appeared in C# well before they were added to JavaScript, so I'm not sure the reasoning of your timeline makes sense.
- sqeaky 2y agoI think the async/await patterns solve one problem really well: UI latency. My UI background is web testing and C++ game UIs in C++. There are like 3 patterns in multithreaded games. Thread per responsibility (old/bad), Barriers blocking all threads and give each game system all the threads, or something smart. Few games do something smart, and the thread per responsibility is not ideal and we will ignore it for now. In games often pausing everything and letting the physics system have all the threads for a few milliseconds is "fast enough". Then the graphics systems will try to use all the threads, and so on so that eventually everything will get all the threads even thought everything else rarely needs it. Sometimes two things are both close to single threaded and have no data contention so they might both be given threads, but this is almost always a manually decided things by experts. This means that once UI, the buttons, the text, cursors, status bars, etc, gets its turn there won't be any race conditions (good), but if it needs to request something from disk that pause will happen on a thread in the UI system (bad and analogous to web sites making web API calls) so UI latency can be a real problem. If any IO small or some resource system has preloaded it then there isn't a detectable slowdown, but there are still plenty of silly periods of waiting. There is also a lot of time when some single threaded part of the game isn't using N-1 hardware threads and all that IO could have been asynchronous. But often game UIs are a frame behind the rest of the game simulation and there is often detectable latency in the UI like the mouse feeling like it drags or similar. Allowing IO to run in the back while active events are processed can reduce latency and this is the default in web browsers. IO latency in web pages is worse than in games and other computation seems smaller than games, so the the event loop is close to ideal. A function is waiting? throw it on the stack and grab something else to do! This means that all the work that can be done while waiting on IO is done and when does well makes a UI snappy. If that were available sensibly in games it could allow a game designed appropriately to span IO across multiple frames and be snappy without stutters. With games using the strategy I described above latency in the game simulation or IO can cause the UI to feel sluggish and vice versa. In games caching UI details and trying to pump the frame rate is "good enough". If the UI is a frame behind but we have 200 frames per second, that isn't really a problem. But when it chugs and the mouse stops responding because the player built the whole game world out of dynamite and set it off the game will not process the mouse until that 30 minutes of physics work is done. There are better scheduling schemes for games. I am a big fan of doing "something smart" but that usually means scheduling heterogenous work with nuanced dependencies and I have written libraries just for that because it isn't actually that hard. But if you don't have the raw compute demands of a game scheduling IO along side your UI computation is often "fast enough" and is any easy enough mental model for JS devs to grok and allow them freedom to speed things up with their own solutions like caching schemes and reworking their UIs.
- kubectl_h 2y agoI work on a large multi-threaded ruby code base and it's always a pain to deal with engineers introducing the async usage in it. Most of the time these engineers don't have a grasp on what fibers are good for and we have to painstakingly review their code and provide feedback that no, it's not magical concurrency, we have a limited number of fix sized connection pools for postgres, redis, memcache, etc available in the process and fibers have to respect those limits as much as threaded code. In fact it's better if you don't introduce any additional concurrency below the thread-per-request model if you can. Only in places like concurrent http calls do we allow some async/fiber usage but even then it ends up nominally looking like threaded code and the VM context switching time saved by using fibers instead of threads is trivial. Trying to use fibers instead of threads in typical CRUD workloads feels a little cargo culty to me. Fibers are cool and performant but usually they should be limited to lightweight work with large concurrency requirements.
- Glyptodon 2y agoI wish there were limited forms of true parallelism available with fibers instead of it just being another concurrency construct limited by an interpreter lock. I feel like there should be an in-language construct for stuff that's safely parallel and narrow, and not just Ractors. But I do get that it'd be a footgun for many.
- kubectl_h 2y agoI agree. I've thought about POCing some ractor usage for actual CPU bound workloads but ultimately it would be an academic exercise and most of our engineers aren't even thinking about the code performance on that level. My beef, if you will, with async (and I realize now I was a little loose in making the distinction between async and fibers) is that engineers reach for async because they are intimidated by threaded code. Instead they are used to the ergonomics of async/await in the browser and think it's basically the same with the async library in ruby and is somehow immune to the considerations you have to make when spawning a thread. This just isn't true. The dragons are mostly the same, or at least they are in our code base.
- jayd16 2y agoEver done any UI programming or other paradigms where you have a specific OS thread used for cross runtime synchronization but you need to efficiently call background threads knowing exactly what's running where? Async/await handles that well and so far the other paradigms just have not targeted these use cases well. Message passing and promise libraries end up looking a lot like async/await with less sugar. (Compared to C#'s async/await implementation. The level of sugar depends on the language, of course.)
- binary132 2y agoIn our C++ stack at work we accept HTTP requests, and then spawn internal IO which takes some time. During that time, we yield the handler thread to handle other requests. The arrival of the internal response sets up the next step in the request handler to resume, which needs the response message. This can be done manually by polling status of descriptors, and stepping into and out of callables / passed continuations by hand, or it can be done with a task scheduling API and typesafe chained async functions. Pick your poison, I guess, but it probably scales better than using synchronous IO and thread-per-request.
- kmeisthax 2y agoJavaScript has async/await because it solves a very specifically JavaScript problem: the language has no threads and all your code lives in the main event loop of the browser tab. If you do the normal thing of pausing your JavaScript until an I/O operation returns, the browser hangs. So you need to write everything with callbacks (continuation passing style), which is a pain in the ass and breaks exception handling. Promises were introduced to fix exceptions and async/await introduced to sugar over CPS. There's also a very specifically Not JavaScript problem (that happens to also show up in Node.js): exotic concurrency scenarios involving lots of high-latency I/O (read: network sockets). OS kernel/CRT[0] threads are a lot more performant than they used to be but they still require bookkeeping that an application might not need and usually allocate a default stack size that's way too high. There's no language I know of where you can ask for the maximum stack size of a function[1] and then use that to allocate a stack. But if you structure your program as callbacks then you can share one stack allocation across multiple threads and heap-allocate everything that needs to live between yield points. You can do exotic concurrency without exposing async to the language. For example, Go uses (or at least one point it did) its own thread management and variable-size stacks. This is more invasive to a program than adopting precise garbage collection and requires deep language support[3]. Yes, even moreso than async/await, which just requires the compiler transform imperative code into a state machine on an opt-in basis. You'd never, ever see, say, Rust implement transparent green threading[2]. To be clear, all UI code has to live in an event loop. Any code that lives in that event loop has to be async or the event loop stops servicing user input. But async code doesn't infect UI code in any other language because you can use threads and blocking executors[4] as boundaries between the sync and async worlds. The threading story in JavaScript doesn't exist, it has workers instead. And while workers use threads internally, the structured clone that is done to pass data between workers makes workers a lot more like processes than threads in terms of developer experience. [0] Critical Runtime Theory: every system call is political [1] ActionScript 3 had facilities for declaring a stack size limit but I don't think you could get that from the runtime. async/await syntax in Rust also just so happens to generate an anonymous type whose size is the maximum memory usage of the function across yield points, since Rust promises also store the state of the async function when it's yielded. Yes, this also means recursive yields are forbidden, at least not without strategic use of `Box`. And it also does not cover sync stack usage while a function is being polled. [2] I say this knowing full well that alpha Rust had transparent green threading. [3] No, setjmp/longjmp does not count. [4] i.e. when calling async promise code from sync blocking functions, just sleep the current thread or poll/spinwait until the promise resolves
- didip 2y agoAsync/await is just a sugar that many people happened to like. As for me, I like Go's CSP better.
- Fire-Dragon-DoL 2y agoI'm with you here, actor model is the way to go. I thought Go style could be better, but the Go type system is completely inadequate to support that style: it is impossible to guaranteed that a goroutine is panic free, it is not possible to put a recover in the goroutine unless that code is under author's control (could be a lib) and a goroutine panic takes down the whole app. Suddenly I want a wrapper around each goroutine that bubbles up the panic to the main thread without crashing the app, that sounds a lot like an erlang supervision tree
- explaininjs 2y agoThat sounds a lot like async/await. Any errors thrown in an async context bubble to the callsite awaiting it.
- Fire-Dragon-DoL 2y agoSorry you are right, that was specific to our case where we were already waiting for a result from the goroutines. In an actor model, you would let the "goroutine" die and the supervisor would restart it if it has a restart policy, otherwise it will die and stay dead (without bringing the system down). In erlang you can also "link" the current actor to another actor so that if the linked actor dies, the linker dies too (or receives a message)
- hosh 2y agoI have written Ruby, Elixir, and Typescript on Nodejs. I have yet to see a good reason for async/await, other than syntax sugar on a flawed language architecture. The very thing people like about Nodejs (async reactor), creates a lot of problems in production web and data pipeline code. As an aside, Elixir’s Task.async and Task.await are function helpers that work with message passing primitives. Code execution can be truly suspended, and messages are queued. Javascript’s async/await queues code execution rather than messages, and I think that leads to error prone code by design.
- raggi 2y agoasync/await like many similar constructs buys you the ability to defer design decisions you would otherwise have to have made earlier. in a program that stays small, this is good, less work to do. in a program that grows large, this is a hazard and will likely lead to substantial debts down the line.
- BiteCode_dev 2y agoI don't like colored function for obvious reasons, but fully colorless for async means you don't know when things are async or not. There are a lot of things I dislike in JS, but I think the I/O async model is just right from an ergonomics point of view. The event loop is implicit, any async function returns a promise, you can deal with promises from inside sync code without much trouble. It's just the right balance.
- throw10920 2y ago> fully colorless for async means you don't know when things are async or not The IDE can tell you.
- BiteCode_dev 2y agoGiven Ruby culture of monkey patching, not always. Besides, many people dev Ruby with a lightweight text editor, like text mate, that can't introspect code.
- mp1mp2mp3 2y ago> ...culture of monkey patching... I haven't seen more than a handful of PRs with monkey-patching in the last decade and even then they are accompanied by long-winded explanations/apologies and plans for removal ASAP (eg monkey-patching an up streamed fix that hasn't been released yet). Also, ruby classes/methods can tell you all about themselves, so if you haven't got ruby-lsp up and running (and even if you do) you can always open a repl (on its own, in the server process, or in a test process) and ask the interpreter about your method. It's pretty great! It's definitely the case that the editor's ability to understand code via static analysis is limited compared to other languages, but it's not like we ruby devs are navigating in the dark.
- goatlover 2y agoCan't the text editor open a shell where you can run the repl to do the inspection?
- 2y ago
- thwarted 2y ago> Async code bubbles all the way to the top. If you want to use await, then you have to mark your function as async. Then if someone else calling your function wants to use await, they also have to mark themselves as async, on and on until the root of the call chain. If at any point you don’t then you have to use the async result (in JavaScript’s case a Promise<T>). I find many descriptions of async code to be confusing, and this kind of description is exactly why. This description is backwards. You don't choose to use await and then decorate functions with async. Or maybe you do and that's why so many async codebases are a mess. You don't want to block while a long running operation completes, so you decorate the function that performs that operation with async and return a Promise. But Promises have exactly the same value as promises in the real world: none until they are fulfilled. You can't do further operations on a promise, you can only wait for it to be done, you have to wait for the promise to be fulfilled to get the result that you actually want to operate on. The folly of relying on a promise is embodied in the character Whimpy from Popeye: "I'll gladly pay you Tuesday for a hamburger today". Once you have a promise, you have to await on it, turning the async operation into a synchronous operation. This example seems crazy to me: async function readFile(): Promise<string> { return await read(); } This wraps what should be an async operation that returns a promise (read) in an expression that blocks (await read()) inside a function that returns a promise so you didn't need to block on it!. This is a useless wrapper. This kind of construct is probably the significant contribution to the mess: just peppering code with async and await and wrapper functions. await is the point where an async operation is blocked on to get back into a synchronous flow. Creating promises means you ultimately need to block in a synchronous function to give the single threaded runtime a chance to make progress on all the promises. Done properly, this happens by the event loop. But setting that up requires the actual operation of all your code to be async and thus callback hell and the verbose syntactic salt to even express that in code. That all being said, this piece is spot on. Threads (in general, but in ruby as the topic of this piece) and go's goroutines encapsulate all this by abstracting over the state management of different threads of execution via stacks. Remove the stacks and async programming requires you to manage that state yourself. Async programming removes a very useful abstraction. Independent threads of execution, if they are operating system managed threads, operating system managed processes (a special case of OS managed threads), green threads, or go routines, are a scheduler abstraction. Async programming forces you to manage that scheduling. Which may be required if you don't also have an abstraction available for preemption, but async leaks the single threaded implementation into your code, and the syntactic salt necessary to express it.
- throw10920 2y agoAs they should be. I object to doing what a computer can do for me (in programming), and manually creating separate versions of functions that are identical up to async absolutely falls into that category.
- Rapzid 2y agoThey have different signatures because they return different things.
- goatlover 2y agoNot in languages like Ruby.
- Rapzid 2y agoSpicy!
- throw10920 2y agoThe difference between returning a Foo and Promise<Foo> is utterly irrelevant in this case because the computer is capable of automatically handing the difference.
- lmm 2y agoSo use a language that knows how to be polymorphic over async. Just like you don't want to have to write one version of sort() for each possible array element type, but the solution isn't to make all array elements untyped, the solution is to have a language that can abstract over that.
- svieira 2y agoAnytime this comes up I plug the excellent "Unyielding" (https://glyph.twistedmatrix.com/2014/02/unyielding.html https://glyph.twistedmatrix.com/2014/02/unyielding.html) and "Notes on structured concurrency" (https://vorpus.org/blog/notes-on-structured-concurrency-or-go-statement-considered-harmful/ https://vorpus.org/blog/notes-on-structured-concurrency-or-g...) as the counterpoint to "What color is your function". Being able to see structurally what effects your invocation of a function will result in is very helpful in reasoning about the effects of concurrency in a complicated domain.
- eduction 2y ago>Because threads share the same memory space they have to be carefully coordinated to safely manage state. Ruby threads cannot run CPU-bound Ruby code in parallel, but they can parallelize for blocking operations Ugh. I know Ruby (which I used to code in a lot more) has made some real progress toward enabling practical use of parallelism but this sounds still pretty awful. Is there any effort to make sharing data across threads something that doesn't have to be so "carefully coordinated" (ala Clojure's atom/swap!, ref/dosync)? Is the inability to parallelize CPU-bound code to do with some sort of GIL?
- vidarh 2y agoThat's what Ractor is for, if you want full parallelization without processes. And, yes, it's to do with a GIL/GVL. The lock is released during blocking IO, and some C extensions etc., so in practice for a lot of uses it's fine.
- sqeaky 2y agoThey ditched the GIL a while a ago. But there are smaller locks fighting for resources. EDIT - I remember when patch notes years ago said the GIL was gone and this says there is a GVL, I guess there is some subtle difference. then I think for practical purposes, "yes" is your answer, but not in precisely that name.
- Lio 2y agoIt all depends on your Ruby runtime. If you want parallel threads then you can use JRuby and your threads will run in parallel on the JVM. I've used the Concurrent-Ruby gem to coordinate that[1]. It has copies of some of the Clojure data structures. Otherwise, Ractors the up coming solution for MRI Ruby. 1. https://github.com/ruby-concurrency/concurrent-ruby https://github.com/ruby-concurrency/concurrent-ruby
- curtisblaine 2y agoSo maybe it's me, but isn't that line mapping on `&:value` in Ruby the exact equivalent of doing `Promise.all` on a bunch of async functions in Javascript, with the downside that you don't explicitly say that the array you calling `value` on is a bunch of asynchronous things that need to be (a)waited for to realize? In other words, since you have color anyway, isn't it better to highlight that upfront rather than hiding it until you need to actually use the asynchronous return values?
- clayg 2y agoI feel like I have some intuative understanding of how go achieves colorless concurrency using "go routines" that can park sync/blocking io on a thread "as needed" built into the runtime from the very begining. I don't understand how Ruby added this after the fact, globally to ALL potential cpu/io blocking libraries/functions without somehow expressing `value = await coro` Python is currently going through an "coloring" as the stdlib and 3rd-party libraries adapt to the explicit async/await syntax and it's honestly kind of PITA. Curious if there's any more info on how Ruby achived this.
- mattgreenrocks 2y agoUnless I'm misunderstanding: isn't the JVM's virtual threads another instance of this colorlessness?
- zogrodea 2y agoI think you're right. People describe Java as being colourless.
- hyualsdfowjasd 2y agoThe C code in Ruby has to yield to the scheduler when it detects a fiber. For historical context, python had several "colorless" attempts but none achieved widespread adoption.
- DarkNova6 2y agoJava has shown that a sufficiently strong runtime can take the burden onto itself long after the facts have been established. Python in contrast has a runtime ultimately held back by its deep integration with the C-ecosystem which creates more black-boxes inside the runtime than you can find inside a swiss cheese. Similar with C# and Rust. No strong runtime, no colorless functions for you.
- hyualsdfowjasd 2y ago> Similar with C# and Rust. No strong runtime, no colorless functions for you. Zig is the exception here but not sure how well it worked out in practice.
- fny 2y agoI'm confused, and please correct me if I'm wrong. Aren't all these calls blocking? Doesn't `File.read` still block? Sure it's multithreaded, but it still blocks. Threading vs an event loop are two different concurrency models.
- masklinn 2y ago> Aren't all these calls blocking? Only locally, which is pretty much the same as when you `await` a call. > Threading vs an event loop are two different concurrency models. The point is that you can build a "threading" (blocking) model on top of an event loop, such that you get a more natural coding style with most of the gain in concurrency. It loses some, because you only have concurrency between tasks (~threads) leaving aside builtins which might get special cased, but my observation has been that the vast majority of developers don't grok or want to use generalised sub-task concurrency anyway, and are quite confused when that's being done. For instance in my Javascript experience the average developer just `awaits` things as they get them, the use of concurrent composition (e.g. Promise.all) is an exception which you might get into as an optimisation, e.g. `await`-ing in the middle of a loop is embarrassingly common even when the iterations have no dependency.
- Rapzid 2y agoThe whole point of async/await is to allow for not blocking the caller though until it's ready for an explicit synchronization point. If you are blocking the caller you have not "solved" the colored function "problem".
- Blackthorn 2y agoYou can build promises with fibers too if you just want to use the value you'd "await" on later on in execution, it's trivial.
- masklinn 2y ago> The whole point of async/await is to allow for not blocking the caller though until it's ready for an explicit synchronization point. That is an end not a mean. Again in my experience the vast majority of devs could not give less of a shit about “not blocking the caller” by default. What most devs want is a reasonably cheap way to get a high amount of concurrency. If anything not blocking the caller by default is generally an error, because somebody forgot an await. > If you are blocking the caller you have not "solved" the colored function "problem". Of course you have: you have solved the actual problem that needs solving without using function colouring. That’s how e.g. Go works. Go has problems up the ass but it doesn’t have that one at least. Java is also moving back to Userland threads rather than towards async/await.
- janci 2y ago> 3. You can only call a red function from within a red function The base of most arguments against async. And it's false. You can call red from blue. And you should, sometimes.
- lta 2y agoYes... But it's a PITA most of the time, right ? I'm not sure for JS, as I can't remember right now but it's a annoying as f*k in python at least
- masklinn 2y agoIn JS it’s not an issue as long as you just fire and forget. Because calls to async functions essentially fork off tasks. No way to synchronise on them though. Python (and rust) are coroutine based so calling an async function does essentially nothing, you need to acquire a runtime handle in order to run and resolve the coroutine.
- stephen 2y agoMaybe its Stockholm syndrome after ~4-5 years of TypeScript, but I like knowing "this method call is going to do I/O somewhere" (that its red). To the point where I consider "colorless functions" to be a leaky abstraction; i.e. I do a lot of ORM stuff, and "I'll just call author.getBooks().get(0) b/c that is a cheap, in-memory, synchronous collection access ... oh wait its actually a colorless SQL call that blocks (sometimes)" imo led to ~majority of ORM backlash/N+1s/etc. Maybe my preference for "expressing IO in the type system" means in another ~4-5 years, I'll be a Haskell convert, or using Effect.ts to "fix Promise not being a true monad" but so far I feel like the JS Promise/async/await really is just fine.
- goatlover 2y agoDon't you generally know when you're making an I/O call?
- t-writescode 2y agoNot when I'm using some library function that's "helpful"
- CuriousSkeptic 2y agoI seem to recall a similar argument being made a while ago: https://scholar.harvard.edu/files/waldo/files/waldo-94.pdf https://scholar.harvard.edu/files/waldo/files/waldo-94.pdf
- bilalq 2y agoOther languages may handle it differently, but having to manage threads is not a small compromise for going colorless. You're now forced to deal with thread creation, thread pooling, complexities of nested threads, crashes within child or descendant threads, risks of shared state, more difficult unit testing, etc.
- dang 2y agoRelated: What color is your function? (2015) - https://news.ycombinator.com/item?id=28657358 https://news.ycombinator.com/item?id=28657358 - Sept 2021 (58 comments) What Color Is Your Function? (2015) - https://news.ycombinator.com/item?id=23218782 https://news.ycombinator.com/item?id=23218782 - May 2020 (85 comments) What Color is Your Function? (2015) - https://news.ycombinator.com/item?id=16732948 https://news.ycombinator.com/item?id=16732948 - April 2018 (45 comments) What Color Is Your Function? - https://news.ycombinator.com/item?id=8984648 https://news.ycombinator.com/item?id=8984648 - Feb 2015 (146 comments)
- munificent 2y agoIt fills me with delight that apparently my lasting contribution to computer science is a piece of writing that also contains the phase "Spidermouth the Night Clown".
- vips7L 2y agoDon't sell Crafting Interpreters short!
- munificent 2y agoFair. But that one has a hand-drawn picture of an alligator eating characters and pooping tokens, so it's in about the same category of maturity. :)
- zanellato19 2y agoDo you still maintain more or less the same opinion from the post?
- munificent 2y agoYeah, I do, actually. Dart, which I work on, is still a colored language. And it's like, fine. But I do wish it was colorless. It would make library design a lot easier. There is real friction all the time when doing API design to decide which things should and shouldn't be async.
- moralestapia 2y agoThe argument for "color"ed functions in Javascript is flawed and comes from somebody with a (very) shallow understanding of the language. Javascript is as "colorless" any other programming language. You can write "async"-style code without using async/await at all while it being functionally equivalent. Async/await is just syntactic sugar that saves you from writing "thenable" functions and callbacks again and again and again ... Instead of: function(...).then(function() { // continue building your pyramid of doom [1] }) ... you can just do: await function() That's literally it. 1: https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming) https://en.wikipedia.org/wiki/Pyramid_of_doom_(programming)
- anonymoushn 2y agoIf you program with promises and without async/await, then your language is still missing something compared to languages that have coroutines in which functions that suspend and do not suspend may be composed (used by higher-order functions, etc.) in the same ways as each other. You've moved from the situation where both types of functions were present and must be handled differently to the situation where one type of function is entirely missing.
- moralestapia 2y agoCan you give an example?
- moralestapia 2y agoNope. - anonymoushn
- anonymoushn 2y agoAn example of a language or of a program? You could try Lua, Python with greenlet, Zig 0.10.0 or Zig master with zigcoro, dozens of libraries that add this sort of capability to C, or becoming a the kind of person who uses search engines when they have questions. BulletML is not even Turing complete and still has a wait function that does the exact thing mentioned.
- Rapzid 2y ago99.9% of these "colored" function articles have an incomplete or even flawed understanding of async/await symantics. Fibers are not fungible with async/await. This is why structured concurrency is a thing.
- pjungwir 2y agoGreat article! I'm looking forward to reading the rest of the series. I noticed a couple details that seem wrong: - You are passing `context` to `log_then_get` and `get`, but you never use it. Perhaps that is left over from a previous version of the post? - In the fiber example you do this inside each fiber: responses << log_then_get(URI(url), Fiber.current) and this outside each fiber: responses << get_http_fiber(...) Something is not right there. It raised a few questions for me: - Doesn't this leave `responses` with 8 elements instead of 4? - What does `Fiber.schedule` return anyway? At best it can only be something like a promise, right? It can't be the result of the block. I don't see the answer in the docs: https://ruby-doc.org/3.3.4/Fiber.html#method-c-schedule https://ruby-doc.org/3.3.4/Fiber.html#method-c-schedule - When each fiber internally appends to `responses`, it is asynchronous, so are there concurrency problems? Array is not thread-safe I believe. So with fibers is this safe? If so, how/why? (I assume the answer is "because we are using a single-threaded scheduler", but that would be interesting to put in the post.)
- hosh 2y agoSo is Erlang/Elixir colorless or do those function calls have color?
- juped 2y agoErlang has a preemptive SMP-using scheduler, so none of this really applies
- vlucas 2y agoLong-time JS/TS/Node programmer here. Knowing ahead of time which functions are async is a feature. It's a big neon sign that says "hey, this function call is expensive". This is a good thing for programmers to easily see and know at the call site. If you make multiple calls with async/await in a row, the performance issues are plainly obvious at the call site. With "colorless" functions, this information is hidden in a deeper layer. You have to know what the function does on the inside to even know what its performance impacts are. Also, a nitpick - you can call async functions from sync ones, you just can't access the return value. Sometimes, you don't need to.
- mananaysiempre 2y ago> It's a big neon sign that says "hey, this function call is expensive". IMO the more important part is that it can yield control. Half the advantage of cooperative multithreading is knowing where you may yield control makes sharing memory more or less workable. I also like Trio’s model: if it’s async, it will yield control, even if the result is already available so it doesn’t technically have to. Makes it more difficult to accidentally starve other tasks. (I suppose I also have to mention Curio’s model: if it’s async, it’s a scheduler upcall, whether yielding or not. But for all that Curio deserves credit for freeing Python from asyncio hell, I just don’t find myself caring about things being scheduler upcalls or not.)
- newZWhoDis 2y agoRuby/rails is full of shortsighted crap like this. I feel stuck, our whole backend is legacy rails and I can’t escape.
- revskill 2y agoI loved Ruby as a total beginner but hate it as an experienced programmer. Colorless brings no meaning when i look at the signature of a method, which is a warning ! Async at the boundary, sync at the core is my favorite paradigm.
- lofaszvanitt 2y agoRuby is like Japanese candlestick charts, both of them are absolute bullshit, idiotic things.
- wiseowise 2y agoKotlin solved this pointless debate long time ago the moment they’ve released coroutines. Best of both worlds: you no longer have two functions with ReturnType and Promise<ReturnType>. You just mark potentially blocking function with suspend and you’re done.
- downsplat 2y agoTranslated into simple language, Ruby chose to expose the multithreading paradigm (multple threads over shared data), like Java and others. Multithreading is strictly more powerful than single threaded event loops. For some kinds of software there is just no alternative - a modern browser engine for example needs to be multithreaded. The trade off is that you need to make sure your code is thread safe, which is not trivial as the collection of articles explains. That's your function color right there, green functions are verified thread safe, gray functions are not or not sure. Personally in nearly 30 years of programming I've never needed to write multithreaded code. I still haven't found a business need that could not be met with suitable choices between multiprocessing (i.e fork) and event loops. I'll definitely take wait/async programming over having to worry about concurrent thread safety any day of the week.
- dmux 2y agodef log_then_get(url, context) puts "Requesting #{url}..." get(url, context) end def get(uri, context) response = Net::HTTP.get(uri) puts caller(0).join("\n") response end def get_http_thread(url) Thread.new do log_then_get(URI(url), Thread.current) end end Good example of the downsides of dynamic typing: 1) get_http_thread takes a url (String) and converts it to a URI object 2) log_then_get defines its parameter as `url`, but really its expecting a URI object 3) get defines its parameter as `uri`, but we're passing it an argument called `url` from within log_then_get. Lots of traps readily awaiting an unsuspecting programmer or newcomer to a project that contains code like this.
- mst 2y ago> Even more onerous, if it isn’t built into your language core like JavaScript/node.js, adding it later means modifying your entire runtime, libraries and codebases to understand it. Interestingly, while this has proven true of async/await for many languages it has not at all been true for perl. The pluggable keywords feature lets us register 'async' and 'await' with the parser as (block scoped) imported keywords and with a little suspend/resume trickery you get https://p3rl.org/Future::AsyncAwait https://p3rl.org/Future::AsyncAwait which I've been using happily pretty much since it was released (generally operating on https://p3rl.org/IO::Async::Future https://p3rl.org/IO::Async::Future and https://p3rl.org/Mojo::Promise https://p3rl.org/Mojo::Promise objects, often both in the smae process). I even wrote https://p3rl.org/PerlX::AsyncAwait https://p3rl.org/PerlX::AsyncAwait as a pure perl proof of concept later on, which injects computed gotos as resume points ala the switch/case trick you can use for resumable functions in C (nobody should really be using that one, mind, I wrote it to prove that I could and as potential fodder for https://p3rl.org/App::FatPacker https://p3rl.org/App::FatPacker usage later). I do very much appreciate there are a lot of reasons one might dislike perl (I've been writing it long enough my list is probably longer than most naysayers') but its sheer malleability as a language remains unusually good.
- lowbloodsugar 2y agoOnly having blue functions is not the same as being colorless.
- masjl 2y ago[dead]
- vcxbx 2y ago[flagged]
- 01HNNWZ0MV43FF 2y ago"Colorless" is probably referring to the thing in Rust where functions are "Colored" for async or sync execution I can tell you as a non-Ruby, non-web dev what the Ruby mindshare feels like to me: - I hear that Ruby is slow - The syntax looks odd - I know Ruby on Rails was very popular at some point but I think it's been displaced almost entirely by C# and Node.js - I have no intention of learning Ruby. If I was going to invest in learning web tools, and Rust was not an option, I'd prefer TypeScript first, Python second, and something else third
- IshKebab 2y agoIt's not a Rust thing. It's any language with a async/await. IIRC the original article was about JavaScript.
- ctz 2y agoThe underlying concept is applicable to lots of things, really. For example, the functions that are unsafe and safe to call from a POSIX signal handler can be treated as a function colouring problem.
- williamcotton 2y agoHere's three different ways to write a small shell script that turns letters into uppercase: Node: let inputData = ''; process.stdin.on('data', chunk => inputData += chunk ); process.stdin.on('end', () => console.log(inputData.toUpperCase()) ); Python: import sys sys.stdout.write( sys.stdin.read().upper() ) Ruby: puts $stdin.read.upcase
- racketprogram 2y agoconsole.log(require('fs').readFileSync(0).toString().toLowerCase())