TL;DR: Coffeescript, while not as polished, facilitates writing code with a similar level of expression as Python, allowing for terse yet readable source.
One of the main reasons I love Python is the level of expression, particularly as that applies to list processing and more generally functional programming. With list comprehensions and built-ins like filter and map, you can write code that almost reads word-for-word like the question it is trying to answer.
As part of that learning process, I decided to write a Hanging With Friends solver (finished product) to get a taste for Coffeescript. I had already written one in Python, and I wanted to see how different the two would be. You can find the full code of the core solvers here. Here are some of my thoughts:
Coffeescript list comprehensions go a long way toward cleaning up ugly sections of loop-riddled code. This remains my favorite part of CS. As an example, to create a set of sets of words of a certain length:
word_sets = [filter(lambda x: len(x) == l, words) for l in lengths]
word_sets = (x for x in words when x.length == l for l in lengths)
The lack of a built-in way to do filtering makes the CS version less readable, but it is still very concise. Multiple levels of nesting can also make the code harder to read, though this is a problem in Python as well. Worse though is the way different structures are expressed in Coffeescipt. For instance, in Python, you can do this:
>>> [a+b for a in range(1,4) for b in range(1,4)] [2, 3, 4, 3, 4, 5, 4, 5, 6] >>> [[a+b for a in range(1,4)] for b in range(1,4)] [[2, 3, 4], [3, 4, 5], [4, 5, 6]] >>> [[[a+b] for a in range(1,4)] for b in range(1,4)] [[, , ], [, , ], [, , ]]
In Coffeescript, though, the rules are next to inscrutable.
coffee> (a+b for a in [1...4] for b in [1...4]) [ [ 2, 3, 4 ], [ 3, 4, 5 ], [ 4, 5, 6 ] ] coffee> ((a+b for a in [1...4]) for b in [1...4]) [ [ 2, 3, 4 ], [ 3, 4, 5 ], [ 4, 5, 6 ] ] coffee> (((a+b) for a in [1...4]) for b in [1...4]) [ [ 2, 3, 4 ], [ 3, 4, 5 ], [ 4, 5, 6 ] ] coffee> ([a+b for a in [1...4]] for b in [1...4]) [ [ [ 2, 3, 4 ] ], [ [ 3, 4, 5 ] ], [ [ 4, 5, 6 ] ] ] coffee> [[a+b for a in [1...4]] for b in [1...4]] [ [ [ [Object] ], [ [Object] ], [ [Object] ] ] ]
I couldn’t figure out how to get a flat list like the first Python example above. Maybe I’m just doing it wrong. As another issue, the different format for iterating over keys in an
Object is less clean than the Pythonic style of iteration and seems to afford less flexibility. Ultimately, though, list comprehensions can handle most situations that I would field with map/filter and list comprehensions in Python.
The conditional syntax of the list comprehensions certainly limits the balance between terseness and readability. For instance, it is easy to first filter then map, but it’s a good bit messier to go the other way.
n = (x / 6.0 for x in [123...234] when (x*x) % 17 == 1) n = (y for y in ((x*x) % 17 for x in [123...132]) when y % 5 == 1)
In Python, it’s straightforward and easy to understand: just swap the order.
n = map(lambda x: x / 6.0, filter(lambda x: (x**2) % 17 == 1, range(123, 234))) n = filter(lambda x: x % 5 == 1, map(lambda x: (x**2) % 17, range(123, 132)))
Many of the other shortcomings of Coffeescript are less a failing of the list comprehensions themselves than a lack of expressive built-ins like
set which must be shimmed. Many of my most elegant lines of code do things like build a dictionary of unique sublists based on a filtered, larger list. This sort of thing is harder to do in Coffeescript.
Finally, and this is a bit nitpicky, but the order of operations on the operator aliases don’t match the expectation of natural language. It was confusing to find that
is not and
isnt do two different things.