haskell-game / tiny-games-hs

Haskell Tiny Game Jam
160 stars 31 forks source link

Question about minifying and unminifying. #52

Closed hyunsooda closed 1 year ago

hyunsooda commented 1 year ago

Some of the submitted games have been compressed to adhere to the guidelines. Can anyone share their techniques for effectively compressing (minifying) the code, as well as the steps for decompressing (unminifying) them?

OsePedro commented 1 year ago

I wrote my first submission in minified form by hand. This was only practical because I had a very clear idea of what I wanted to create from the outset, but even then it took me a whole day to write it! But even if you write it in readable form and use a minification tool, you'll still have to do a lot of manual work to make it as small as possible.

The techniques that I used include:

kindaro commented 1 year ago

I contributed a minifier. Try it out!

simonmichael commented 1 year ago

Here's my current approach:

hellwolf commented 1 year ago

Using applicative notation to shorten list comprehensions. E.g. it allowed me to reduce [l a b|a<-r x,b<-r y] to l<$>r x<*>r y.

Oh, that's one nice trick... :) That means I might have a chance to jam a minimax strategy in?

hellwolf commented 1 year ago

1) I also use https://pointfree.io/ a lot to convert some functions to pointfree ones if it shortens. 2) If common function such as map,foldl,take,drop appearing multiple times, you may also assign a single letter to it.

OsePedro commented 1 year ago

I just learnt another little space-saving trick while trying to maxify matchmaking. It turns out that arguments beginning with a digit end at the last digit. So e.g. f 9a=g a a is a valid way of writing f 9 a = g a a!

  1. If common function such as map,foldl,take,drop appearing multiple times, you may also assign a single letter to it.

More specifically, if its name is c characters long and it's used t times, then it's taking up c*t characters, whereas a single letter name would take up 2+c+t characters, as you need 2+c characters to define the name. As

          2+c+t <  c*t 
iff       2+c   <  (c-1)*t 
iff (2+c)/(c-1) <  t 

the number of function usages that you need before you start saving space is:

Name Length Minimum Required Usages
2 5
3 3
4 3
5+ 2
hyunsooda commented 1 year ago

Thank you for sharing. I was curious to find out how to automate the process of minifying scripts. I assumed that there would be readily available solutions for this, similar to those offered for JavaScript, but it appears that this may not be the case for Haskell. This could be due to a lower demand for such solutions in the Haskell community compared to front-end development. Regardless, I appreciate your contributions. If there are no further comments, I will consider this discussion closed.

OsePedro commented 1 year ago

I assumed that there would be readily available solutions for this, similar to those offered for JavaScript

Bear in mind that it's impossible to write an optimal minifying script though (if it was, then Kolmogorov complexity would be computable). So even when using one that seems very effective, there's a good chance that there will be things that you could do to make the code even smaller.

simonmichael commented 1 year ago

@hyunsooda please give minify.hs a try and let us know your experience.

TristanCacqueray commented 1 year ago

I use a similar approach as @simonmichael :

In tsp I used this technique to minify r = if p then x else y with o=True;r|p=x|o=y.

I run ghcid -T test game.hs for testing, and ghcid --command "ghci -pgmL markdown-unlit" ./README.lhs for the final version.

fgaz commented 1 year ago

The top-level minify.hs didn't work for me, so I wrote my own in #63

hyunsooda commented 1 year ago

@simonmichael, @fgaz,

I tried both versions and neither worked.

The tested code is:

import System.Info
main = do
    print os
    print arch
    print compilerName
    print compilerVersion

The output of both scripts is the same as follows:

import System.Info main=do print os print arch print compilerName print
 compilerVersion

So, I should manually fix it like below (i.e., adding curly braces and semicolons appropriately):

{import System.Info; main=do{print os; print arch; print compilerName;
print compilerVersion}}
fgaz commented 1 year ago

Yes both scripts need semicolons and braces in their input. It's written in a comment in my minifier and in the global README. You can keep the indentation though; the following code should be minified correctly:

import System.Info;
main = do {
    print os;
    print arch;
    print compilerName;
    print compilerVersion
  }
simonmichael commented 1 year ago

I confirm that /minify.hs and /hackage/brickbreaker.hs both handle fgaz's example above, and produce the same 90-byte output.

simonmichael commented 1 year ago

https://github.com/haskell-game/tiny-games-hs/tree/main/hackage/7up7down has a cool trick: paste minified code into ormolu's online demo to unminify.

hyunsooda commented 1 year ago

Really cool. I was using Ormole for formatting, but I never hacked it to unminify. Work nicely.

TristanCacqueray commented 1 year ago

For what it's worth, I tinkered with ghc-lib-parser to do some pre-processing of the unminified source. Here is what I got so far: simple-haskell-minifier, this tool renames the binders to single letters and it performs some basic transformations, such as inlining single-use declaration.