3
\$\begingroup\$

Background

I've tried to solve /r/DailyProgrammer Challenge #303 [Easy]: Ricochet (summarised below) in Haskell. The code below is now the second Haskell script I've ever written, so I'd consider myself quite the beginner. It works (to a bare minimum), but I'm not particularly happy with it.

My knowledge in Haskell at this point spans as far as the first two chapters of the fantastic Haskell Wikibook. I've not reached functors, monads, or friends, so if the solutions to my queries lie there then it might be wisest for me to settle until I learn more.

My Queries

Whilst I'm sure there are many problems, my main concerns in the code below regard the manner in which I'm dealing with the numerical arguments to ricochet, and the fragile nature of IO.

More particularly, regarding ricochet:

  • I wanted the 5 arguments (namely h, w, m, n and v) all to be any kind of integer (not minding Int or Integer, etc., so long as it's a whole number, mathematically speaking), barring v which can be any number at all (i.e. 1.05).
  • Whilst need for integers is important (as modulo (and thus the algorithm) wouldn't make sense without them), I also want to be able to perform arbitrary arithmetic, including any kind of division.
  • I took me much time and frustration to come up with anything that'd even compile, for what's considerably basic arithmetic, and at the end I still find that function signature and spam fromIntegral nasty. Any advice on dealing with this kind of situation in future?

And regarding IO:

  • What's there now is the bare minimum to work with the challenge. I'm not happy with it because it's so fragile. If anything at all is wrong with the input, the whole program explodes.
  • I'd rather just output a blank line if the input is invalid, but I've no idea how to get that done.

However, I'd of course appreciate any advice at all, even around spacing/indentation!

#!/usr/bin/env stack
-- stack --resolver=lts-8.2 --install-ghc runghc

{-
Ricochet
========

- https://redd.it/5vb1wf
-}

module Main
    ( Corner(..)
    , ricochet
    , main
    ) where

data Corner = UL | UR | LR | LL deriving (Show, Eq)

ricochet :: (Integral a, Fractional b) => a -> a -> a -> a -> b -> (Corner, a, b)
ricochet h w m n v
    = (c,b,d / v)

    where
    -- What's a more idiomatic way of doing this?
    h', w', d :: Num a => a
    h' = fromIntegral (h - m)
    w' = fromIntegral (w - n)
    d  = fromIntegral $ lcm w' h'

    b  = (d * (h' + w')) `div` (h' * w') - 2
    c  =
        case (
            ((d `div` h' - 1) `mod` 2),
            ((d `div` w' - 1) `mod` 2)
        ) of
            (0,0) -> LR
            (0,1) -> LL
            (1,0) -> UR
            (1,1) -> UL

main :: IO ()
main = interact ( unlines
                . map (output . input . words)
                . lines
                )

    where
    -- How to make these less fragile and more idiomatic?
    input (h:w:m:n:v:[]) = ricochet (read h) (read w) (read m) (read n) (read v)
    output (c,b,t) = show c ++ " " ++ show b ++ " " ++ show t

Challenge Summary

Each line input describes a bounding box with dimensions h and w, and an inner box with dimensions m and n (for height and width respectively).

The inner box starts in the top-right corner (0,0) and is travelling diagonally, towards (1,1). You're told the inner box is travelling at a velocity of v units per second.

I've interpreted all of the parameters as whole numbers, other than v, which is a real number.

An example line of input: 2 4 1 2 0.5.

The inner box can ricochet off of the bounding walls as it travels.

For each line of input, calculate the first corner that the inner box reaches (UR (upper right), LR (lower right) or LL (lower left)), along with the number of times the box ricocheted off of a wall before it reached that corner, and the time taken to get there.

\$\endgroup\$
0

1 Answer 1

3
\$\begingroup\$

You can reduce the fromIntegral spam like this:

ricochet :: (Integral a, Fractional b) => a -> a -> a -> a -> b -> (Corner, a, b)
ricochet h w m n v
    = (c,b,fromIntegral d / v)

    where
    h' = h - m
    w' = w - n
    d  = lcm w' h'

The return value of functions like - and lcm is of the same type as their arguments, so the types of h' and the like can be inferred. You do need to write fromIntegral d / v because the compiler doesn't perform automatically the conversion required for the division operator. It's frequent to have these conversions functions at "type cast" boundaries.

But what if you want to keep the explicit signatures, for clarity? You can use "explicit forall" along with the ScopedTypeVariables extension, which allow you to say "this type here is the exact same type as the type in the top-level signature".

{-# LANGUAGE ScopedTypeVariables #-}
module Main
    ( Corner(..)
    , ricochet
    , main
    ) where

data Corner = UL | UR | LR | LL deriving (Show, Eq)

ricochet :: forall a b. (Integral a, Fractional b) 
         => a -> a -> a -> a -> b -> (Corner, a, b)
ricochet h w m n v
    = (c,b,fromIntegral d / v)

    where
    h', w', d :: a
    h' = h - m
    w' = w - n
    d  = lcm w' h'
\$\endgroup\$
1
  • \$\begingroup\$ Thanks for the suggestions! I can confirm that it works like a charm, and certainly looks much cleaner. I'll re-examine the two snippets a few hundred times to try and understand what the functional differences are between the two. \$\endgroup\$
    – Bilal Akil
    Commented Feb 25, 2017 at 12:19

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.