Well, the types certainly aren't exactly equivalent.
The type-checker considers them different types. If you compile with -ddump-types
, you'll see:
TYPE SIGNATURES
f :: Int -> IO [Double]
g :: forall a. (a ~ Double) => Int -> IO [a]
The two types also behave differently at compile time. For example, with the TypeApplications
extension, g @Double 10
is a valid expression while f @Double 10
isn't.
In core, the equality constraint is implemented -- like all constraints -- as an extra dictionary argument, and you'll see this difference in the types reflected in the generated core. In particular, if you compile the following program:
{-# LANGUAGE GADTs #-}
module EqualityConstraints where
import Control.Monad
f :: Int -> IO [Double]
f n = replicateM n readLn
g :: (a ~ Double) => Int -> IO [a]
g n = replicateM n readLn
with:
ghc -ddump-types -ddump-simpl -dsuppress-all -dsuppress-uniques \
-fforce-recomp EqualityConstraints.hs
you'll see core like the following:
-- RHS size: {terms: 12, types: 22, coercions: 3, joins: 0/0}
g = \ @ a $d~ eta ->
case eq_sel $d~ of co { __DEFAULT ->
replicateM
$fApplicativeIO eta (readLn ($fReadDouble `cast` <Co:3>))
}
-- RHS size: {terms: 6, types: 4, coercions: 0, joins: 0/0}
f = \ n -> replicateM $fApplicativeIO n (readLn $fReadDouble)
and this difference will persist in the core even with -O2
.
Indeed, if you prevent inlining, the resulting final core (and even the final STG) will involve some unnecessary dictionary passing, as you can see by playing with:
{-# LANGUAGE GADTs #-}
import Control.Monad
f :: Int -> IO [Double]
{-# NOINLINE f #-}
f n = replicateM n readLn
g :: (a ~ Double) => Int -> IO [a]
{-# NOINLINE g #-}
g n = replicateM n readLn
main = do
f 2
g 2
and compiling with -ddump-simpl
and -ddump-stg
.
As far as I can see, this difference is still visible even in the optimized assembly with -O2
.
So, I think it's safe to say that g
is treated sufficiently polymorphically that the caveats about runtime costs of needless polymorphism do indeed apply.