I am having trouble understanding why the following code results in an error. If the second field of Foo
is changed to type Int
, the code runs without errors.
Prelude> data Foo = Foo {foo_a :: Int, foo_b :: Float}
Prelude> let x = Foo 2 3.4
Prelude> unwords $ map (\fn -> (show . fn) x) [foo_a, foo_b]
<interactive>:4:46:
Couldn't match type `Float' with `Int'
Expected type: Foo -> Int
Actual type: Foo -> Float
In the expression: foo_b
In the second argument of `map', namely `[foo_a, foo_b]'
In the second argument of `($)', namely
`map (\ fn -> (show . fn) x) [foo_a, foo_b]'
Why is show
unable to accept arguments of varying types? The following, of course, work:
Prelude> show $ foo_a x
"2"
Prelude> show $ foo_b x
"3.4"
Also, given that this doesn't work, what is the recommended way to apply show
to various fields of a data type?
Thanks.
The problem is that lists in Haskell are homogeneous (all the elements have the same type). And in [foo_a, foo_b]
you are trying to create a list with two different types: Foo -> Int
and Foo -> Float
.
One solution is to move the show
inside the list and create a list of Foo -> String
:
Prelude> data Foo = Foo {foo_a :: Int, foo_b :: Float}
Prelude> let x = Foo 2 3.4
Prelude> unwords $ map (\fn -> fn x) [show . foo_a, show . foo_b]
"2 3.4"
((\fn -> fn x)
can be written as ($ x)
)
Another possibility is to create a data type to unify the the several types you want to put in a list, mimicking a heterogeneous collection. In your case, it could be something like this:
{-# LANGUAGE ExistentialQuantification #-}
data Showable b = forall a . Show a => MkShowable (b -> a)
pack :: Show a => (b -> a) -> Showable b
pack = MkShowable
unpack :: Showable b -> b -> String
unpack (MkShowable f) = show . f
Then, you can do:
*Main> let x = Foo 2 3.4
*Main> unwords $ map (\fn -> unpack fn x) [pack foo_a, pack foo_b]
"2 3.4"
[Update]
I was playing around with the Data.Dynamic
, and it seems more promising than the existential type I created above.
Let's start with:
{-# LANGUAGE DeriveDataTypeable #-}
import Control.Applicative
import Data.Dynamic
import Data.Maybe
data Foo = Foo {foo_a :: Int, foo_b :: Float}
deriving Typeable
get_a :: Dynamic -> Maybe (Foo -> Int)
get_a = fromDynamic
get_b :: Dynamic -> Maybe (Foo -> Float)
get_b = fromDynamic
getAsString :: Dynamic -> (Foo -> String)
getAsString dyn = fromJust $ (show .) <$> get_a dyn
<|> (show .) <$> get_b dyn
<|> error "Type mismatch"
In this case, we can do:
*Main> let x = Foo 2 3.4
*Main> unwords $ map (\fn -> getAsString fn x) [toDyn foo_a, toDyn foo_b]
"2 3.4"
It seems that we had to write more code to achieve the same result, but it actually gives us much more flexibility. While the existential type was only focused in showing the fields as String
, here we are not limited to that. What if now we want to see all the fields as Int
(in the case of Float
we want to round the value)?
getAsInt :: Dynamic -> (Foo -> Int)
getAsInt dyn = fromJust $ get_a dyn
<|> (round .) <$> get_b dyn
<|> error "Type mismatch"
Now we can do:
*Main> let x = Foo 2 3.4
*Main> map (\fn -> getAsInt fn x) [toDyn foo_a, toDyn foo_b]
[2,3]
This is a limitation of the Hindley-Milner type system. Only definitions in let
(and equivalently, in where
and at the top level) can be polymorphic. In particular, arguments to functions cannot be polymorphic. fn
must either have type Foo -> Int
or Foo -> Float
-- there is no Foo -> (Int or Float)
or Foo -> Showable
type.
If you must, you can define a Showable type, which is known as an existential type, but you have to give the typechecker some help in order to use it, and in most code the idea is not used because its inconvenience outweighs its usefulness, and we generally have no trouble expressing what we want without it.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With