JavaScript Code Golf
September 21st, 2018After winning the 2018 edition of the Js1k contest, I was suggested to try https://code-golf.io/. In this website/game there are coding problems that you can solve in any of the multiple languages available and you are supposed to try to achieve the shortest as possible working solution. You can compete with other users through scoreboards that show the best results.
I had no idea before, but this type of competition is called "code golf". If you are curious about the term, you can check its Wikipedia article.
At first my interest in code-golf.io was moderate, but the user @ETHprod started to dare me to beat his high scores, we ended battling for the top positions on several problems and this way my interest increased.
So after a few problems I thought that it would be a good idea to become first in all the JavaScript problems and then write an article - this one you are reading - with the size reduction techniques I learned from the process. And this idea kind of made sense in my mind: I didn't consider what a gargantuan effort and waste of time it would be. When I noticed, it was too late to stop. At least I had fun. Well... kind of. Whatever.
Minimal Dictionary Coder
There are several JavaScript dictionary coders designed for js1k - check JSCrush and RegPack - but they are not well suited for the tiny sizes of the solutions of the problems at code-golf.io.
I developed what I believe is the most little as possible JavaScript dictionary decompressor and it looks like this:
`0 1, or not 0 1`.replace(/\d/g,i=>`to|be`.split`|`[i])
// "to be, or not to be"
It works only for strings that doesn't contain arabic numerals, and this limitation is what makes the decompressor shorter than any other and also the compression ratio higher.
Probable Primes
There are a couple of problems in code-golf.io that deal with prime numbers. It is possible to check if a number is prime in very few characters but to reduce the size even further probable primality testing can be used instead.
I don't want to spoil the game so I'm not going to mention the exact method I used, but I want to mention that it works due to a couple of peculiarities, one of the floating point numbers and other of the way one math operator works in JavaScript - but it is not a common behaviour in other languages.
Hash Brute-forcing
In the morse decoder problem you are given a set of dots and dashes that represent characters and spaces that represent character separators.
In order to decode a character, you may first convert dots and dashes into numbers, and then numbers into characters. To convert numbers into characters a compact way is to use a perfect hash function.
There are virtually unlimited ways to convert the dots and dashes into numbers, and what I wanted to find was a way that makes the code including the hash data the shortest as possible. Each time I found a new shorter solution the search space was reduced to solutions shorter than it. In the end I brute-forced a few trillion possibilities, and I'm quite confident that it made a good solution. However it might be possible to find better solutions by using significantly more computing power and/or a radically different approach.
Don't Solve the Problem, Only Pass the Tests
This might be one of the worst advices ever for serious software development however it works great for code golf.
In code-golf.io the validity of the solutions is checked against tests. In a few problems these tests are not covering all edge cases, and if you don't consider these edge cases it is possible to shave one or two characters from your solution: enough to set a top score.
This feels like cheating and well... it is probably cheating. Can we call it "hacking" instead? Yeah, it is hacking! And isn't code golf all about hacking? Ok, it is cheating.
Using the Full Unicode Range
Code-golf.io measures code size in characters. Because the characters are Unicode, they can store a significative amount of information, and you can take advantage of this to reduce the size of your solutions in multiple ways.
For example you can store in a single character a positive integer up to 1,114,111:
// Before
for(i of [12345,3333,1000000,31415,567890])console.log(i)
// After
for(i of "〹അ窷")console.log(i.codePointAt())
With this unpacker by @MaximeEuziere and @subzey you can halve the size of your code by decoding 1 Unicode character into 2 ASCII characters:
eval(unescape(escape`𨑬𩑲𭀨𘡈𩑬𫁯𘁗𫱲𫁤𘐢`.replace(/u../g,'')))
// Unpacks and evals 'alert("Hello World!");'
Read more about it here for a full explanation of the brilliant way they use escape and unescape to unpack the code.
Finally for the problems with longer solutions - 12 Days of Christmas, Poker, Spelling Numbers - I developed an unpacker that has a bigger overhead but is able to pack 3 characters into 1 Unicode character, so it performs better when the code is long enough:
_=0;for(c of`爁䍞援`)for(v=c.codePointAt();v|0;v/=97)_+=String.fromCharCode(v%97+31);eval(_)
// Unpacks and evals '0; alert("Hello World!");'
There are more limitations than in the previous unpacker: the packed code has to start by a semicolon, its length has to be a multiple of 3 and all characters have to be in the visible ASCII range.
BigInt
V8 supports since May the BigInt proposal: arbitrary precision integers support. The proposal is beautifully designed in a way that prevents unnecesary and/or accidental type casting by disallowing most of the automatic conversions, but allows the ones that can't cause problems and would be painful to do explicitly. If there is a need or not for big integers in JavaScript is disputable, but if there is, I can't imagine a better design.
The problems of code-golf.io include computing several constants - pi, e, golden ratio... - with a thousand of decimals of precision. BigInt is perfect for the computations. As BigInts are integers and the constants are real numbers, you have to use binary scaling.
Modern JavaScript
There are multiple articles that talk about JavaScript code golf techniques like this article and this other article. However I haven't seen a lot that use modern JavaScript, so I'm going to write a few examples:
s=function(a,b){return a+b} // Before
s=(a,b)=>a+b // After
Math.pow(a,b) // Before
a**b // After
Math.sqrt(a) // Before
a**.5 // After
Math.cbrt(a) // Before
a**(1/3) // After (for a >= 0)
Math.sqrt(a*a+b*b) // Before
Math.hypot(a,b) // After
(a*a+b*b)**.5 // Shorter
{foo:foo} // Before
{foo} // After
for(i=0;i<3;i++)v=[1,2,3][i] // Before
for(i=3;i--;)v=[1,2,3][i] // Before (in reverse)
for(v of[1,2,3]) // After
[1,2,3].map(v=>) // After
for(i=2;i<8;i+=2) // Before
for(i of"246") // After
There is way more than this, like new methods as String.prototype.padStart and lots of possibilities with the destructuring assignments.
Code Golf in Other Languages
I was curious about whether or not a good solution in JavaScript would translate to good solutions in other languages. To try it I refreshed my forgotten PHP, I learnt a bit of Perl 6 and Python and I solved a few problems in these languages.
I don't have a very precise answer to my question, I just found out that in some cases a good solution in JavaScript helps to find a good solution in other language, but in other cases the language differences are so big that it doesn't help at all.
For example for the prime numbers the solution using probable primes was only working in JavaScript because of an implementation difference in one of the math operators. And in Perl 6 there is a built-in method .is-prime that makes the problem trivial.
I Double Dare You 😜
I'm throwing down the gauntlet at you, reader. Can you beat any of my records?