Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

Rather flamey indeed, but let me take the bait. I've been using callback-style programming in many applications, ranging from IRC bots to application servers and whatnot. I've also been playing around with Go for several months and tried very hard to implement something that is inherently asynchronous. I gave up after about two months, I just couldn't get used to the concurrency model. I always end up with either data races, deadlocks and have otherwise no good overview of the program logic. This is exactly what the article describes, and seems to apply to me as well.

Incidentally, while figuring out how to implement something inherently asynchronous in Go I looked at some IRC client implementations, and noticed how all of those used callback-style programming one way or another. That doesn't inspire much confidence. :(

All in all, I am not sure yet whether this is purely due to my inexperience with concurrent programming or whether I am just inherently incompatible with it. I'd love to be able to play with alternatives to callbacks in the future, but for now it's just way over my head for the things I want to do.



It appears, much like FORTH, callback coding rots the brain. I am surprised you had such issues with deadlocks in Go, it takes some effort to write code that deadlocks in Go (I don't mean that a deadlock cannot be shown with a trivial amount of Go code, but that the style of programming tends to lend itself to not having deadlocks).

> and noticed how all of those used callback-style programming one way or another.

What do you mean precisely here, because I think you are conflating two concepts. There is a distinction between using a callback as a unit of concurrency (do this async task and call THIS function when you're done), and genuinely being an event handler (call THIS function when you get receive a particular IRC event). I would expect to see the latter in Go for IRC code, not the former.

But there is another aspect of this too, beyond code looking prettier. Go/Erlang/Haskell scale to multiple cores as the runtime evolves to support this. Callback codes doesn't, it can't. This doesn't seem to be because callback developers don't see the value in multiple cores, Node has built-in support for spawning a VM per core. This is quite restrictive. Callback developers have to either limit themselves to solving problems that don't need to communicate with each other or use some other means to communicate between VMs. That doesn't sound like progress to me. But shrug, plenty of people don't neat things in Node so maybe it doesn't matter.


It's hard to pinpoint exactly why I was having so many problems with deadlocks. Most of the time they could easily be solved by having channels with an unbounded buffer, thus guaranteeing asynchronous behaviour. But since the Go authors intentionally left that out, I must be doing something wrong at a more fundamental level.

You're probably right about the IRC clients using the latter style of callbacks. But then it turns pretty much into the following style:

  irc_connect();
  register_event_x(callback_x);
  register_event_y(callback_y);
  while(read())
    dispatch_event();
Which is essentially an event loop with callback-style dispatching. The only major difference here and with regular callback programming is that the callbacks are allowed to block. But in my mind that only complicates things as I can't tell anymore whether I can receive a particular event at some time because I have no idea whether there are any blocking handlers going on. Of course, you can dispatch everything into a separate goroutine, but then suddenly I can't easily argue anymore about the order of message arrival and what effect that has on shared state. Of course, this example may be a bit vague and abstract, but these resemble the kinds of issues I have constantly run in to.

On the scaling on multiple cores: Sure it's a nice advantage when you're already used to concurrent programming, but for most things I've written so far it doesn't really matter. In the few cases that I had a component that required heavy disk I/O or computational resources, it was only a small and easily-identifyable component that could be implemented in a separate OS thread. I'm not familiar with Node, but both glib2 and libev allow spawning a separate event loop in each thread and provide mechanisms to communicate between each other (idle functions in glib, ev_async in libev). Those are for C, however, and I have to admit that callback-style programming isn't too convenient there due to the lack of closures and automatic garbage collection.


I would expect the code to look more like an interface and handing your interface object off to the goroutine in charge of the IRC connection. Or the other way and have a goroutine in charge of shuffling the bytes back and forth and when it parses a package it has a channel to pump the information down and then you receive it and work on it. Either way, this seems much more straight forward than callbacks.

> But in my mind that only complicates things as I can't tell anymore whether I can receive a particular event at some time because I have no idea whether there are any blocking handlers going on

Don't really get what you mean here. The select operator seems like it would solve the problem your implying.

> then suddenly I can't easily argue anymore about the order of message arrival and what effect that has on shared state

You can barely, sometimes not even, reason about the order of message arrival and it's effect on state in callbacks, though. At every point in a callback you have the entire state of the program to deal with, at least in Erlang or Go you only have the context of your local process/goroutine to deal with. I have seen plenty of callback code explode because, what seemed like, an obvious sequence of events got reorderd or sent before the previous work was expected to be done, or any variation, and the code didn't handle it properly. You might be thinking "well, duh, that is easy to fix and a silly mistake to make", but it isn't. In a shared memory, especially where mutability is the default mode of action, an event-driven framework can be represented as a function that takes a 2 dimensional matrix: 1 dimension is every variable in my program, the other is every event that can happen in my program. At every point in the program any variation of this matrix must be a valid state. In a language like Go, or Erlang, I can cut this matrix down significantly so that I only have to worry about the matrix involving some subset of events and some subset of variables. In short, that's a massive win IMO.


Not sure whether what you saw was an artifact of your long exposure to callbacks. The Go/Erlang kinds of approaches need you to think at a slightly higher level than callbacks. A small step is the concept of a "promise" .. which, in Go, can be modeled (roughly) as a channel on which some process waits to receive a value promised to it that is being computed in another process. At a higher level are streams of data. Do you have an example you found hard to express in, say, Go without thinking "callback"?




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

Search: