A recent post on Reddit caught my attention: A “One” Line Echo Server Using “let” in Python (original article). The basic idea is that you can use Python’s
lambda with default arguments as a
let, which in turn allows you to write a simple echo server in
one line a nicely functional style.
To start with, here is their original code:
import socket import itertools (lambda port=9000, s=socket.socket(socket.AF_INET, socket.SOCK_STREAM): s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) == None and s.bind(('', port)) == None and s.listen(5) == None and list(map(lambda c: c.sendall(c.recv(1024)) and c.close(), (s.accept() for _ in itertools.count(1)))))()
It’s certainly not very Pythonic, but there are a few neat tricks in there:
lambdawith default arguments to define things
andto sequence function calls
- Using list comprehension to handle the response threads
It got me thinking though, what would the same sort of code look like in Racket?
Well, one of the draws Racket advertises (rightfully so) on its home page is that it comes batteries included. That means that if you’re using
, you get a bunch of useful functions for TCP built in. Let’s start with a fairly direct translation:
(let ([s (tcp-listen 9000)]) (sequence->list (sequence-map (λ (in+out) (thread (thunk (apply copy-port in+out)))) (in-producer (thunk (call-with-values (thunk (tcp-accept s)) list))))))
Okay, so that looks really weird. But it’s a fairly straight forward translation. A few of the lines got folded into the
call and the list comprehension became a
. It’s kicked off via
and forced to run to termination (which will never happend) with
. Unfortunately, it has the same problem that the original Python code does. Since we’re constructing a list, we’ll eventually run out of memory.
One interesting addition it does that the Python version didn’t is that it both allows for multiple lines (the Python version would read one packet and hang up) and any amount of data. I’ve never actually used the
function before. It’s really cool!
If we broaden our definition of “one” line a little more to allow
sequences (which aren’t really that different under the hood), we can clean it up a bit to this:
(let ([s (tcp-listen 9000)]) (for ([(in out) (in-producer (thunk (tcp-accept s)))]) (thread (thunk (copy-port in out)))))
Heck, if you want to get a little less clear about it, you can actually fold the
let into the
(for* ([s (in-value (tcp-listen 9000))] [(in out) (in-producer (thunk (tcp-accept s)))]) (thread (thunk (copy-port in out))))
This works because
for* is actually a nested loop. So in the outer loop, it runs over the single value of the open socket. The inner loop then runs forever, accepting new incoming connections.
Actually, I may have to put this in my quick-scripts toolbox. There are a fair few times when writing networking clients that having a dead simple echo server could come in handy.