Hacker News new | ask | show | jobs
by enjo 4867 days ago
In that case? Not very much different at all, but that's missing the point. Think of it this way (I apologize, this is going to get obtuse):

Imagine you're a barista. You clock in and start doing barista things. You have three responsibilities. You have to take orders from customers and you have to pour coffee. When there are not customers you have to clean the shop.

The rules:

- You must take at least two orders if there is a line, but no more before you start pouring coffee. You should never have more than two outstanding orders at a time.

- If you have no customers in line and no coffee to pour, you must clean the shop.

Lets do it with generators (again pseudo code):

  function day_of_work() {
    generator take_orders() {
        orders = array()

        while(during_shift()) {
           while(customers_in_line()) {
             if(length(orders) >= 2) {
                yield orders
             } else {
               orders.push(take_customer_order())
             }
           }

           yield orders
        }

    generator clean_shop() {
        unpack_coffee()
        yield
        
        clean_espresso_machine()
        yield

        take_out_trash()
        yield
    }
 
    while(orders = take_orders.next()) {
        if(length(orders) > 0) {
          pour_coffee(orders.pop())
        } else {
          clean_shop.next()
        }
    }
There are lots of cool things going on here.

First notice that these generators not only have variable state, but they also have control flow state and THAT is why they're different. Notice that take_orders will yield control if more than two orders exist in the queue. It will also yield control if no more customers are in line. So our main program loop is really simple. It gets the current set of orders from "take_orders". Once the order queue is full it starts pouring coffee.

Notice what happens when no orders are present. Clean shop is executed once per iteration. It first unpacks the coffee, and then yields control. Now we go back and check for more orders, if we find them we fill them until we don't have any more again. Then it's back to cleaning shop, but we pick back up RIGHT WHERE WE LEFT OFF! So we just start cleaning the espresso machine... this goes on and on until the work day ends at which point take_orders cleanly exists and orders is null. Then we pack up and go home.

Now you might go: Well I can implement that in Javascript! You'd be right! Closures + callbacks make it totally possible. Lets take a stab at it:

  orders = [];
  current_task = -1;

  while(during_work_day()) {
       take_orders(
            orders,
            function(order) { pour_coffee(order); },
            function() { current_task++; clean_shop(current_task); }
       );
  }

  function take_orders(orders, orders_full_callback, no_orders_callback) {
      if(customers_in_line()) {
          if(orders.length > 2) {
              orders_full_callback(orders.pop());
          } else {
              orders.push(take_customer_order());
          }
      } else {
          no_orders_callback();
      }
  }

  function clean_shop(current_task) {
      switch(current_task) {
        case 0:
            unpack_coffee()
            break
        case 1:
            clean_espresso_machine()
            break
        case 2:
            take_out_trash()
            break
        default:
            do_nothing()
      }
  }
So that will work. Every iteration we look for customers, if the queue is full we callback to start pouring coffee. We clean the shop if nothing is going on.

Notice that our callbacks are closures. However, the callbacks do not have execution state. This means we have to manage state externally in the form of current_task.

Now for a trivial example like this, that might not be a big deal. However, imagine that we had a whole list of things to do when there weren't customers. Lets make it even harder, and have those tasks depend on state.

For instance what if we actually had a bunch of separate tasks we had to do:

- unload the truck

- draw something cool on the chalkboard

- clean the shop

What if the order those should be done in depended on state?

Before 1:00 PM the priority is:

#1 draw something cool

#2 unload the truck

#3 clean the shop

After 1:00PM the priority changes:

#1 unload the truck

#2 draw something cool

#3 clean the shop

After 3:00PM the priority changes again!

#1 clean the shop

#2 unload the truck

#3 draw something cool

Now think about how hard this is going to be in Javascript. You're going to have to maintain state for three separate jobs (where am I in that job), but also deal with making sure you're working on them in the right order.

What makes generators so damn handy is that the state is internalized. I only need to change the order I invoke the generators. As long as I keep calling next() until there aren't any items left...they'll just keep churning through whatever they're doing in the first place. They become more expressive as the complexity of the state they are managing increases. Things that are hard to do in languages that don't support them, are often pretty easy with generators. Do some work, yield, do some more work, yield... becomes a very simple pattern to reason about while not having to manage all of the state you have to do in other languages.

I hope that helps clear it up a bit:)

1 comments

Great example. Thank you.