2
\$\begingroup\$

#!/usr/bin/env stack
{- stack
     --install-ghc
     exec ghci
     --package lens
-}

module Main where

import Control.Lens
import Data.List.Lens
import Debug.Trace
import Text.Printf
import Prelude hiding (Left, Right)

data Direction = Up | Left | Down | Right

data Location = Location Int Int

type Grid = [[Int]]

main = printClockwise 25

printClockwise :: Int -> IO ()
printClockwise n = putStrLn $ concatMap printRow grid
  where
    grid = walk directions center 1 emptyGrid
    emptyGrid = replicate sq (take sq (repeat 0))
    printRow row = concatMap (printf "%4s" . show) row ++ "\n"
    -- Ex. for a 5x5 spiral the start location is (2,2)
    center = Location half half
    half = floor $ (fromIntegral sq) / 2
    -- This will return all directions we should walk through the spiral in.
    -- Something like [Right, Down, Left, Left, Up, Up, Right, Right, Right]
    directions = concatMap (\(direction, steps) -> take steps $ repeat direction) (zip directionOrder steps)
    directionOrder = cycle [Right, Down, Left, Up]
    -- Ex. for a spiral up to 25, the steps are 1, 1, 2, 2, 3, 3, 4, 4, 5
    steps = concatMap (take 2 . repeat) [1 .. sq - 1] <> [sq]
    -- Assume the input is a perfect square of an odd number
    sq = ceiling $ sqrt (fromIntegral n)

walk :: [Direction] -> Location -> Int -> Grid -> Grid
walk (dir : dirs) loc@(Location x y) current grid = do
  let grid' = grid & (ix y . ix x) .~ current
  let loc' = moveLocation dir loc
  walk dirs loc' (current + 1) grid'
walk [] _ _ numbers = numbers

moveLocation Down (Location x y) = Location (x) (y + 1)
moveLocation Right (Location x y) = Location (x + 1) (y)
moveLocation Left (Location x y) = Location (x -1) (y)
moveLocation Up (Location x y) = Location (x) (y - 1)

I came across this problem on reddit and thought it'd be fun to try my hand at it in Haskell. The idea is to generate the grid before printing, and the insight to generate that grid was that the movement follows a predictable pattern, of right, down, left, up, and the # of steps taken in a given direction is 1, 1, 2, 2, 3, 3, 4, 4... , n - 1, n -1, n , where n is the square of the input number. So with that you can walk through the grid and fill in the numbers.

The function has no input checking, it assumes the input is a perfect square (of an odd number).

\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

Not too advanced myself - so take with a grain of salt.

  • Foremost I would separate the logic for constructing the grid and printing. Especially extract the function to construct the grid. Currently its wrapped in printClockwise IMHO it should be prominently its own function.
  • There are a few definitions in printClockwise (emptyGrid, directionOrder and the printing) that I would consider reusable and would make them top level.
  • You only use lens in one place. lens is quite an expensive import - there just was an interesting post. I suppose your intend was to learn lens and therefore it's used - but just for the code I would consider removing the dependency.
  • the fixed "%4s" in printing - it's easy to estimate the required width for larger numbers. Or just make it that max n is 999.

Some stuff to consider that is really personal choice - no need to follow if you disagree:

  • changing the data type to strict mutable array or similar.
  • adding a walk that takes larger strides.
  • split the walk function into one stream of positions in the spiral, and separately setting the values in a second step. This could solve the issue that the input needs to be a perfect square - just take the first n positions.

As last remarks:

  • whenever I see such a spiral I think Ulam spiral. If you want to expand I would consider adding functionality to the printing to allow highlight - in this case for primes.
  • I also checked whether the coordinates are on OEIS - they are A174344. And your algorithm agrees with the algorithm they post along side (the Julia implementation is almost the same, the other use some shenanigans with sin and cosine).
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.