4

I am looking at some JAVA code and I would like to know how this can be translated into Haskell.


IntStream.range(0, cookedWords.length).parallel().forEach((int i) -> {
  int A = cookedWords[i];

  for (int j = i + 1; j < cookedWords.length; ++j) {
      int B = cookedWords[j];
      if ((A & B) != 0) continue;
      int AB = A | B;

      for (int k = j + 1; k < cookedWords.length; ++k) {
          int C = cookedWords[k];
          if ((AB & C) != 0) continue;
          int ABC = AB | C;

          for (int l = k + 1; l < cookedWords.length; ++l) {
              int D = cookedWords[l];
              if ((ABC & D) != 0) continue;
              int ABCD = ABC | D;

              for (int m = l + 1; m < cookedWords.length; ++m) {
                  int E = cookedWords[m];
                  if ((ABCD & E) != 0) continue;

                  System.out.printf("%s\n%s\n\n",
                          stopwatch.elapsedTime(),
                          decodeWords(A, B, C, D, E));
              }
          }
      }
  }
});

This code is taken from here

I am guessing that there are a few things that need to be done here. unboxed vectors, run parallel etc. but I don't even know how to start with the indexing the way it is done in the imperative code. Or is this where people start to tell me to stay away from Haskell?

What is a literal translation of this code? And is there a better 'Haskell' way of doing something like this?

This is all I can think of doing. But it is obviously inefficient.

[ (a,b,c,d,e) 
  | a <- cookedWords, b <- cookedWords, c <- cookedWords, d <- cookedWords, e <- cookedWords
  , foldl1' (.|.) [a,b,c,d,e] == 0
  ]
matt
  • 1,817
  • 14
  • 35
  • 1
    Please show *some* effort. Share your attempt together with what is not working. – Willem Van Onsem Aug 17 '22 at 09:37
  • 1
    @WillemVanOnsem literally don't know how to start, It is not that I have not tried, but nothing I have done is useful enough to be put on here. I mean voting to close a question because I don't know how to do something seems pretty harsh – matt Aug 17 '22 at 09:45

2 Answers2

6

One could do a pretty literal translation if one were so inclined, but so much work with indices is usually a sign of a failure to understand the real iteration pattern. Here, the most straightforward translation I see is to maintain A, B, C, and so on, and also keep track of the remainder of the list. A monad comprehension using tails to break the lists apart seems simple enough. I imagine something like this:

import Data.Bits
import Data.List (tails)
import Data.Maybe (listToMaybe)
import Control.Monad (guard)

cookedWords :: [Int]
cookedWords = [0..]

match :: [Int] -> Maybe (Int, Int, Int, Int, Int)
match words =
  listToMaybe $ do
    a : as <- tails cookedWords
    b : bs <- tails as
    guard $ a .&. b == 0
    let ab = a .|. b
    c : cs <- tails bs
    guard $ ab .&. c == 0
    let abc = ab .|. c
    d : ds <- tails cs
    guard $ abc .&. d == 0
    let abcd = abc .|. d
    e : es <- tails ds
    guard $ abcd .&. e == 0
    pure (a, b, c, d, e)

Here of course since I've defined words to be [0..], it just finds the first 5 powers of 2 (well okay, 4 of them and a zero):

*Main> match cookedWords
Just (0,1,2,4,8)

But it would be better to generalize this to a function whose input is the list of matching words to search as well as the desired group size, and whose output is a list of such matches.

match :: Int -> [Int] -> [[Int]]
match = go 0
  where go mask 0 _ = [[]]
        go mask n words = do
          x : xs <- tails words
          guard $ mask .&. x == 0
          let mask' = mask .|. x
          (x:) <$> go mask' (n - 1) xs

This way, we don't have to worry about specific named variables for our five intermediate states or the result, we can just work on a list of arbitrary size. And as a bonus, we can get multiple results if we like:

*Main> take 5 $ match 5 cookedWords
[[0,1,2,4,8],[0,1,2,4,16],[0,1,2,4,24],[0,1,2,4,32],[0,1,2,4,40]]

Finding a reasonable abstraction has let us write code that is much simpler and more obviously correct than the Java version.

amalloy
  • 89,153
  • 8
  • 140
  • 205
  • Out of curiosity, can this level of abstraction be used with unboxed vectors as well, or is there nothing to gain in efficiency from trying to do so? – matt Aug 17 '22 at 12:15
  • 2
    What would you put in an unboxed vector here? The list of words? Sure, if you had to you could track an index instead of the remainder of the list. But don't optimize something into ugliness until you've measured the performance of the pretty version and concluded it is too slow for your application. – amalloy Aug 17 '22 at 17:08
  • Ok, I appreciate your help. Just one more question if you can be bothered. The JAVA version seems to be in parallel. Is it a big step to do the same with your code? – matt Aug 17 '22 at 21:09
  • You could attach some parallel strategies on the code to evaluate in parallel. https://hackage.haskell.org/package/parallel-3.2.2.0/docs/Control-Parallel-Strategies.html for details. Not as automatic as slapping `parallel` on a Java stream, though. – Abastro Aug 18 '22 at 03:08
  • 1
    Wouldn't using `tailas (filter (\n -> a .&. n == 0)` avoid needing to redo that check every timer using `guard`? – Axman6 Aug 29 '22 at 06:21
  • 1
    (Responding to myself above) Yes, filtering before passing to tails significantly improves performance. A synthetic benchmark which finds the first million solutions using `cookedWords=filter ((5==) . popCount) (takeWhile (<2^27) [0..])` takes the time from 62.75s to 4.75s. – Axman6 Aug 29 '22 at 06:49
2

You can interleave filtering and enumerating, just like in the imperative code:

[ (a,b,c,d,e) 
  | a <- cookedWords
  , b <- cookedWords
  , a .&. b == 0
  , let ab = a .|. b
  , c <- cookedWords
  , ab .&. c == 0
  , let abc = ab .|. c
  , d <- cookedWords
  , abc .&. d == 0
  , let abcd = abc .|. d
  , e <- cookedWords
  , abcd .&. e == 0
]

This will however still yield symmetrical results: indeed, it can pick a = 2 and b = 4, but also a = 4 and b = 2. We can use tails :: [a] -> [[a]] to prevent this with:

import Data.List(tails)

[ (a,b,c,d,e) 
  | (a:as) <- tails cookedWords
  , (b:bs) <- tails as
  , a .&. b == 0
  , let ab = a .|. b
  , (c:cs) <- tails bs
  , ab .&. c == 0
  , let abc = ab .|. c
  , (d:ds) <- tails cs
  , abc .&. d == 0
  , let abcd = abc .|. d
  , e <- ds
  , abcd .&. e == 0
]
Willem Van Onsem
  • 443,496
  • 30
  • 428
  • 555