Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Function Type Restrictions

Is it generally preferable to have the strictest or loosest type definition for a function? What are the pros and cons of each approach? I found that when I rewrote my pearson correlation code using strictly doubles, it was easier for me to write, follow, and reason about (this could just be inexperience). But I can also see how having a more broad type definition would make the functions more generally applicable. Would stricter type definitions be characterized as a form of tech debt?

With Typeclasses:

import Data.List

mean :: Fractional a => [a] -> a
mean xs = s / n
    where
        (s , n) = foldl' k (0,0) xs
        k (s, n) x = s `seq` n `seq` (s + x, n + 1)

covariance :: Fractional a => [a] -> [a] -> a
covariance xs ys = mean productXY
  where
   productXY = zipWith (*) [x - mx | x <- xs] [y - my | y <- ys]
   mx        = mean xs
   my        = mean ys

stddev :: Floating a => [a] -> a
stddev xs = sqrt (covariance xs xs)

pearson :: RealFloat a => [a] -> [a] -> a
pearson x y = fifthRound $ covariance x y / (stddev x * stddev y)

pearsonMatrix :: RealFloat a => [[a]] -> [[a]]
pearsonMatrix (x:xs) = [pearson x y | y <- x:xs]:(pearsonMatrix xs)
pearsonMatrix [] = []

fifthRound :: RealFrac a => a -> a
fifthRound x = (/100000) $ fromIntegral $ round (x * 100000)

With Doubles:

import Data.List

mean :: [Double] -> Double
mean xs = s / n
    where
        (s , n) = foldl' k (0,0) xs
        k (s, n) x = s `seq` n `seq` (s + x, n + 1)

covariance :: [Double] -> [Double] -> Double
covariance xs ys = mean productXY
  where
   productXY = zipWith (*) [x - mx | x <- xs] [y - my | y <- ys]
   mx        = mean xs
   my        = mean ys

stddev :: [Double] -> Double
stddev xs = sqrt (covariance xs xs)

pearson :: [Double] -> [Double] -> Double
pearson x y = fifthRound (covariance x y / (stddev x * stddev y))

pearsonMatrix :: [[Double]] -> [[Double]]
pearsonMatrix (x:xs) = [pearson x y | y <- x:xs]:(pearsonMatrix xs)
pearsonMatrix [] = []

fifthRound :: Double -> Double
fifthRound x = (/100000) $ fromIntegral $ round (x * 100000)
like image 577
Jeffrey Brown Avatar asked Aug 03 '26 05:08

Jeffrey Brown


1 Answers

Readability is a matter of opinion. In general, I find that more general type signatures are more readable because there are fewer possible definitions (sometimes there is even only one non-diverging definition). For example, seeing that mean only has a Fractional constraint immediately limits the operations being performed in that function (compared to the Double version which could be performing sqrt operations for all I know). Of course, generalizing types is not always more readable. (And just for fun)

The main disadvantage of having more general versions of functions is that they may remain unoptimized at runtime so that Double's dictionary of the Floating functions has to be passed to mean every time it is called.

You can have the best of all worlds by adding a SPECIALIZE pragma. This tells the compiler to basically duplicate your function code with some of the type variables instantiated. If you know you are going to be calling your mean function pretty much only with Double, then this is what I would do

{-# SPECIALIZE mean :: [Double] -> Double #-}
mean :: Fractional a => [a] -> a
mean xs = s / n
  where
    (s , n) = foldl' k (0,0) xs
    k (s, n) x = s `seq` n `seq` (s + x, n + 1)

And you get to see the specialized version of the signature in your code too! Yay!

like image 101
Alec Avatar answered Aug 05 '26 14:08

Alec



Donate For Us

If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!