While this article specifically mentions metaprogramming with Clojure macros, I think that a programming language can make you smarter in other ways as well.
It’s an oft-repeated aphorism that learning many programming languages (especially languages in different paradigms) helps you to become a better programmer in all of them. This is because the solution to any particular programming problem is usually determined by the semantics of the programming language. Learning a broader set of language semantics gives you more ways of reasoning about a problem, and more ways of expressing solutions. Combine this with the prospect of solving novel problems in these varied languages, and you’ve got a strong combination for becoming a very capable problem-solver. Lisp macros are repeatedly mentioned as giving many programmers a moment of euphoria when they finally understand the concept of homoiconicity. I’ve got a feeling that Haskell programmers have lots of moments like these as well.
Recently I was reeled by a particularly cool property of relational semantics in logic programming languages like Prolog or miniKanren. Given a relation between arguments and results, you could actually query for arguments based on a result. For example: Take the relation append. In addition to saying “Append the lists (1, 2, 3) and (4, 5)”, you could ask a system “What argument, when combined with (4, 5), gives the list (1, 2, 3, 4, 5)?”, and it will correctly respond (1, 2, 3). Cooler still, you could even ask it “What arguments, when combined together, give the list (1, 2, 3, 4, 5)?”, and the system will produce a stream of argument pairs that produce (1, 2, 3, 4, 5).
Does anyone have any different examples of ways programming languages have caused similar ‘eureka!’ moments?
I don’t think macros are the right way to think about programming. They encourage you to think of an AST structure and manipulate it, but this only adds to the divide between a program’s text and its semantics. To my mind the monadic style encouraged by the ML/Haskell tradition encourages a smoother understanding: you write the business logic as a monadic value, and a succession of interpreters to translate values into less declarative/more operational values, until the final value is something like IO that has purely operational semantics and then the language actually performs the operations.