PurelyFunctional.tv Newsletter 375: JS vs CLJ

Issue 375 - April 27, 2020 · Archives · Subscribe

Clojure Idea 💡


In the last issue, I mentioned that the challenge was considered Expert level in the JavaScript section of Edabit. That surprised and dismayed some of my readers. I was unsettled myself. Was it really that hard? Many people reading this newsletter submitted solutions to the challenge in Clojure. Could all the submitters really be expert-level? Or is it something about Clojure that makes the solution easier than in JavaScript?

The shortest solution was an 18-line concise answer from Steffan Westcott. It's one function and has obviously been golfed a bit. Still, it has an elegant solution. If I were to code this challenge for production use, I might start with this, then expand it out for clarity. Often, when code golfing, among eliminating extraneous characters, you find extraneous parts of the solution. Code golfing is a good way to find the core.

I put myself through my own challenge: could I port this solution to JavaScript? It wouldn't prove anything. But it would give me an idea of how hard it might be to write this. And if I couldn't port it, maybe it means JavaScript does make the solution harder.

Well, I did it. You can see the code here.

I didn't quite get it down to 18 lines. I had to write an array equality function and the frequencies function. Other than that, I wrote everything with plain JavaScript. There may be a way to do it without frequencies, or perhaps a shorter way to write frequencies inline. But I think it's a faithful reproduction of the original.

The question, though, is "what did I learn?"

Well, when I look at the code, it is certainly terse and compact. I had to cheat a bit and write some common Clojure functions inline. For instance, instead of calling partition, which is the correct function to make straights, I wrote this

rankSeq.map((_, i) => rankSeq.slice(i, i+5)).filter(a=>a.length===5)

Not terrible, but not really clear. The Clojure version looks like

(partition 5 1 rank-seq)

In this case, Clojure's standard library really wins. JavaScript, of course, has almost nothing built-in. The built-in function is clearer in intent and of course shorter.

JavaScript also lacks any way to compare collections. Even Java has that. But you can't take two arrays and know if they contain the same elements. So I wrote it.

function arrayEqual(a1, a2) {
  if(a1.length !== a2.length)
    return false;
  for(var i = 0; i < a1.length; i++) {
    if(a1[i] !== a2[i])
      return false;
  return true;

I figured out a way to put it inline, but it's super ugly and long:

s=>s.reduce((a,b,i)=>a && b === handRanks[i])

Imagine that in a larger chain of maps and filters. Not pretty, so I used the named function.

The lack of these kinds of basic features is a real detriment. But it's also not such a big deal to write them yourself. Any large project is going to quickly recoup the expense of writing them very shortly.

Which brings me to frequencies. I wrote frequencies as well. This function is so useful, it should exist in many projects. It's not hard to write. It's ten easy lines. But I wonder if a JavaScript programmer would even think of writing it, since it doesn't exist in JS.

The final lesson was that the JS version is very hard to read. The Clojure version is difficult since it is so concise. But the JS version is obtuse. It's almost as if the language is resisting the density. That's a very subjective call. You be the judge.

So is it expert level? Yes, in JavaScript it is. Even though I could port the function over, it was unfair to have the Clojure version to work from. If I were to do it from scratch, I think I might still be coding it. Clojure gives many advantages, and most of them are mental. The fact that frequencies exists already is not just a savings of 10 lines of code. Its existence is a hint that maybe your problem could be solved with it. Without the hint, who would even think of it? Go ahead and try out your own challenge.

Quarantine update 😷

I know a lot of people are going through tougher times than I am. If you, for any reason, can't afford my courses, and you think the courses will help you, please hit reply and I will set you up. It's a small gesture I can make, but it might help.

I don't want to shame you or anybody that we should be using this time to work on our skills. The number one priority is your health and safety. I know I haven't been able to work very much, let alone learn some new skill. But if learning Clojure is important to you, and you can't afford it, just hit reply and I'll set you up. Keeping busy can keep us sane.

Stay healthy. Wash your hands. Stay at home. Wear a mask. Take care of loved ones.

Clojure Challenge 🤔

Last week's challenge

The challenge in Issue 374 was to score poker hands. You can see the submissions here.

You can leave comments on these submissions in the gist itself. Please leave comments! You can also hit the Subscribe button to keep abreast of the comments. We're all here to learn.

This week's challenge

Poker hand ranking, revisited

Last week, the challenge was to write a function to score a poker hand. It took a hand of poker cards and returned a keyword naming the hand. For instance:

(score [[3 :diamonds] [3 :hearts] [3 :spades] [5 :hearts] [:king :clubs]])
=> :three-of-a-kind ;; three 3s

Cards are represented as a tuple of rank (number or name if it's a face card) and suit. Face card names are the keywords :ace, :king, :queen, :jack. Suits are :diamonds, :spades, :hearts, :clubs.

However, this doesn't tell the whole story. What if two players both got two pairs? In the rules of poker, the hand with the pair with the highest rank wins. If I have a pair of Aces and a pair of 2s, and you have a pair of Kings and a pair of Queens, I win, even though both hands are two pairs.

The keyword return value from last week doesn't contain that information. The challenge for this week has two parts: 1) make score return not just a keyword, but a tuple, and 2) write a function to compare these tuples to determine a winner.

Here is what the score function should return now:

(score [[:ace :diamonds] [2 :spades] [:ace :clubs] [2 :clubs] [5 :spades]])
=> [:two-pair :ace 2 5]
(score [[:ace :spades] [:queen :clubs] [:queen :diamonds] [:king :hearts] [:king 
=> [:two-pair :king :queen :ace]

The tuple contains the name of the hand, followed by the high cards, followed by the "kickers" (tie breakers). In the above example, the 5 is the kicker since it isn't part of the two pairs but might be useful for breaking ties.

The second part of the challenge is to write a function winning-score that chooses the winner between two scores.

(winning-score [:two-pair :ace 2 5]
               [:two-pair :king :queen :ace])
=> [:two-pair :ace 2 5]

The way the scores work, you should be able to compare the high cards and kickers in order after comparing the hand name. These are all of the components that go into scoring hands, and in their order of importance.

Here is a summary of the high card rules:

  • Royal Flush: Ace, King, Queen, Jack, and 10 are the high cards, in that order
  • Straight Flush: All cards are high cards in order, starting with the highest rank
  • Four of a kind: The rank of the set of four is high, the fifth card is the kicker
  • Full house: The set of three is the first high card, the set of two second
  • Flush: All cards are high cards in order, starting with the highest rank
  • Straight: Five consecutive cards, not in same suit (careful, Aces could be low if it's Ace 2 3 4 5)

Three of a kind: The rank of the set of three is high, then the rest are kickers in order

  • Two pair: The highest pair first, then the second, the last card is the kicker
  • Pair: The rank of the pair is high, then the rest are kickers in order
  • High card: All cards are kickers, in order

Please see this site for more details and examples of scoring. You may also want to refer to last week's challenge for more details.

Note that the suit does not play into comparing hands. A flush of spades is the same as a flush of hearts. Just the high card and kicker ranks count if both hands are flushes.

Note also that ties are still possible if the scores are the same. This can happen in poker. The winning-score should return either one, since they are equal.

You may modify your submission from last week or work from someone else's for this challenge.

You can also find these same instructions here. I might update them to correct errors and clarify the descriptions. That's also where submissions will be posted.

As usual, please reply to this email and let me know what you tried. I'll collect them up and share them in the next issue. If you don't want me to share your submission, let me know.

Rock on!
Eric Normand