brick-lang / brick

The Brick language spec
University of Illinois/NCSA Open Source License
31 stars 0 forks source link

Async/await #13

Open weswigham opened 10 years ago

weswigham commented 10 years ago

C# started it, ES6 has it, even C++ 14 has it. Async/await is an amazingly simple construct for creating asynchronous code. Weather it's implemented with promises or continuations on the back end, we'll see later (likely continuations, since I see no indication that Brick will be event-loop based).

Regardless, the async/await keyword syntax for defining asynchronous code is a godsend; it helps create self-documenting code, simplifies asynchronous control flow, and fits snugly with already-defined chunks of brick syntax:

#=
A super-simple asynchronous IRC bot, to experiment with Brick
#=
import Net.Socket.TCP
server = "irc.freenode.org"
port = 6667
nick = "Brick"
channel = "#brick_bot"
commands = {
    "!quit": |sock:Socket, _:Array<String>| async {
        await sock.write("QUIT :Exiting")
        await sock.destroy!
        exit(0)
    },
    "!id": |sock:Socket, words:Array<String>| async -> boolean {
        await sock.write("PRIVMSG " + channel + " :" + words.implode(" "))
    }
}
async listener(sock:Socket, s:String)
    if s.substring(0, 6) == "PING :"
        await sock.write("PONG :" + s.substring(6, s.length))
    else
        let p = s.find(":")
            cs = s.substring(p, s.length)
            exp = cs.explode(" ")
        in
            if commands.has_key?(exp[0])
                puts(exp[0] + "> " + exp.drop(1).implode(" "))
                await commands.get(exp[0])(sock, exp.drop(1))
            else
                puts("> " + cs)
            end
        end
    end
end
async main
    let !sock = await TCP(server, port) in
        if not !sock.success?
            puts("Failed to connect to server")
        else
            await !sock.write("NICK " + nick)
            await !sock.write("USER " + nick + " 0 * :" + nick + " bot")
            await !sock.write("JOIN " + channel)
            sock.listen!(listener)
        end
    end
end

Important notes on what I see as proper use: async isn't an used like an access modifier like in C++ or C#, instead, it's an alternate keyword for function declaration, which implies that the return type should be wrapped in an Awaitable Promise/Future. This way, if manually specifying a type declaration for an async, you needn't include that yourself. In effect, the async keyword adds the type -> Awaitable<T> to the end of the type line. You'll notice that for lambdas, the async keyword appears after the argument types. If I specifying the lambda's return type, it would appears before that. This makes sense, since in practice, the keyword modifies the return type of the function.

As far as the specifics on how async/await interact with threading/blocking, the C# documentation section is excellent: http://msdn.microsoft.com/en-us/library/hh191443.aspx#BKMK_Threads

toroidal-code commented 10 years ago

I'm sorry but I don't really see why this is needed. Can you elaborate?

weswigham commented 10 years ago

It's a shorthand for specifying things that are asynchronous - it clarifies asynchronous code far better than

spawn(proc() {
    x = x + 1
})

Which is difficult to know when has been completed (without inefficient busy-loops, or nested promises/callbacks). It's never been required, per sey, it's not a necessity to use it to write async code in C#, C++, or ES6, but it makes the code you do write wit it far easier to understand. It also adds another layer where the compiler can optimize asynchronous calls.

Specifically, in the above IRC example, now that everything is being awaited, even in a single-threaded process, multiple commands can now be processed simultaneously if they wait on external resources, since the listen! handler no longer blocks. If I were to implement the listen! loop myself, it would (a little simplified) look like:

async method listen(handle: Awaitable<Socket, String, Unit>)
    loop
        let line = await self.getline(MAX_LENGTH)
        in
            handle(self, line)
        end
    end
end

Note how the call to handle is not awaited, thereby letting handlers execute concurrently (even in a single-threaded scenario, since the scheduler will yield while self.getline is waiting on I/O). This is a really great pattern that allows for simple concurrency and efficient handling of IO bound work in a functional style (even if under the hood it's not very functional), and I think it'd be a great idea if it was included in Brick.