Approximating π via Monte Carlo simulation
About those arrows


  I’m interested in a general review, but I’m especially curious about my use of arrows: is there a better way to write lengthRatio?


Compare the following two lines. Both do the same, but which one would you rather see if you need to change your code drunk in three months, with only 5% battery left?

lengthRatio = curry (unsplit (/) &lt;&lt;&lt; both genericLength)

lengthRatio xs ys = genericLength xs / genericLength ys


Also, which one has which type, and which one is more general? 

Arrows are great if you want to abstract functions. But throughout your small script, you're still just working with (-&gt;), not any other instance of Arrow. For a small script like this, Arrow is too much. For example the pointwise definition above is actually a character shorter than the pointfree one. Sure, the pointfree one is clever, but it's also very beginner-unfriendly. 

About randomness

randomPoints introduces a dependency between your point coordinates \$x\$ and \$y\$, since both draw from the same sequence. This usually leads to points on hyperplanes (see disadvantages of LCG and spectral test). Your friends variant doesn't have this immediate problem:

randomTuples :: Int -&gt; IO [(Float, Float)]
randomTuples n = do
  seed1 &lt;- newStdGen
  seed2 &lt;- newStdGen
  let xs = randoms seed1 :: [Float] -- two different
      ys = randoms seed2 :: [Float] -- generators being used
  return $ take n $ zipWith (,) xs ys


However, since newStdGen is merely a split, it's more or less hiding the dependency at another place. Still, it's something to keep in mind, if you don't want to end up with something like this.

But how would you check this? Well, you would run tests, over and over. Here's the second design critique on randomPoints, it doesn't take a RandomGen. Truth be told, if I say that Arrow is too much for a small script, then

randomPoints :: RandomGen g =&gt; g -&gt; [Point Float]


is too much either.

Also, if you know you're going to generate Points, a newtype Point a together with

instance Random a =&gt; Random (Point a) where 


is feasible and doesn't introduce a potential error via chunk2. Keep possible problems with Random in mind, though.

About names

The function isInUnitCircle lies. It's not testing whether the point \$(x,y)\$ lies in the circle with radius \$r = 1\$ with center in the origin, e.g.
$$
\sqrt{x^2 + y^2} \le 1^2 \Leftrightarrow x^2 + y^2 \le 1
$$
but in the circle with diameter \$d = 2r = 1\$ with center in \$(0.5, 0.5)\$. In the following picture, the green region is where you generate your random values. In the left one, you see the regular unit circle, in the right one, you see the circle size you're actually testing (after shifting your values from the green square into the red one):



Therefore, you're not calculating the "usual" fourth of a circle, but instead a circle with a fourth of the original size (\$\pi(\frac{1}{2})^2 = \frac{\pi}{4}\$)). Luckily, it doesn't matter for the convergence.

A real test that checks whether a point is in the unit circle is tremendously easier:

isInUnitCircle :: (Num a, Ord a) =&gt; Point a -&gt; Bool
isInUnitCircle (x, y) = x ^ 2 + y ^ 2 &lt;= 1


About optimization

Last, but not least, there's an issue with approximatePi, or rather the use of lengthRatio on the same list twice. Actually, taking the length of the list again is a litle bit strange, since you know how large the sample is:

numPoints &lt;- read &lt;$&gt; getLine                -- sample size
points    &lt;- take numPoints &lt;$&gt; randomPoints
print $ approximatePi points                 -- sample size still known (?)


But let's say that you don't actually know how many points you have. Let's assume that someone wants to check a many points. Suddenly, the memory usage of your program explodes:


$ echo 10000000 | ./CalcPi +RTS -s
How many points do you want to generate to approximate π?
3.141744
  33,724,505,920 bytes allocated in the heap
   5,288,250,096 bytes copied during GC
   1,319,621,976 bytes maximum residency (17 sample(s))
       5,554,344 bytes maximum slop
            2587 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0     63626 colls,     0 par    1.606s   3.155s     0.0000s    0.0006s
  Gen  1        17 colls,     0 par    2.732s   3.373s     0.1984s    1.2720s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time   17.158s  ( 15.542s elapsed)
  GC      time    4.337s  (  6.528s elapsed)
  EXIT    time    0.019s  (  0.166s elapsed)
  Total   time   21.514s  ( 22.236s elapsed)

  %GC     time      20.2%  (29.4% elapsed)

  Alloc rate    1,965,530,983 bytes per MUT second

  Productivity  79.8% of total user, 77.2% of total elapsed


Even though randoms generates a lazy list, approximatePi needs to hold onto it completely due to lengthRatio. A classic space leak. The altnerative version of lengthRatio won't save you from that. Instead, provide a function to check the ratio of filtered elements:

-- Rational from Data.Ratio
filterRatio :: (a -&gt; Bool) -&gt; [a] -&gt; Rational
filterRatio p xs = -- exercise


That way, you can define a version of approximatePi that works for large lists:

approximatePi :: [Points Float] -&gt; Double
approximatePi points = circleRatio * 4
  where 
    circleRatio = fromRational $ filterRatio isInUnitCircle points



$ echo 10000000 | ./GenPIRatio +RTS -s
How many points do you want to generate to approximate π?
3.1421592
  24,445,866,792 bytes allocated in the heap
      15,555,552 bytes copied during GC
          77,896 bytes maximum residency (2 sample(s))
          21,224 bytes maximum slop
               1 MB total memory in use (0 MB lost due to fragmentation)

                                     Tot time (elapsed)  Avg pause  Max pause
  Gen  0     46874 colls,     0 par    0.012s   0.113s     0.0000s    0.0001s
  Gen  1         2 colls,     0 par    0.000s   0.000s     0.0001s    0.0001s

  INIT    time    0.000s  (  0.000s elapsed)
  MUT     time   10.809s  ( 10.746s elapsed)
  GC      time    0.012s  (  0.113s elapsed)
  EXIT    time    0.000s  (  0.000s elapsed)
  Total   time   10.821s  ( 10.859s elapsed)

  %GC     time       0.1%  (1.0% elapsed)

  Alloc rate    2,261,657,279 bytes per MUT second

  Productivity  99.9% of total user, 99.5% of total elapsed


Summary

Food for thought:


Use the right level of abstraction for your problem. Arrow is an overkill for such a small script, but alright for learning.
Try to decrease the amount of IO wherever possible, but again, that might be too abstract for a small script.


Bad:


Don't lie, give things the right name.
Don't overcomplicate, keep pointfree to a sane minimum.
Major space leak in approximatePi. Read the linked section of RWH and try to define filterRatio or a similar function.


Good:


Type signatures! Yay!
Explicit imports!
Type synonym instead of (a, a) everywhere!


So beside the slight arrow-overkill, well done.