Iain
Iain

Reputation: 211

Generalized conversion of different types to different instance constructors

I am trying to define type class instances to work with vectors of 1-4 components of Num, represented as tuples. This has been the best solution I have found (from my last question)

{-# LANGUAGE FlexibleInstances #-}

data OneTuple a = OneTuple a deriving Show

class MyClass a where
    prints :: a -> String

instance (Show n, Num n) => MyClass (n, n) where
    prints = show

instance (Show n, Num n) => MyClass (n, n, n) where
    prints (a, b, c) = foldl (++) "" $ map (++" ") $ map show [a,b,c]

instance (Show n, Num n) => MyClass (OneTuple n) where
    prints (OneTuple a) = show a

but this comes with the requirement that prints 1 be written as prints $ OneTuple 1 which is undesirable. I tried implementing a tuplify function for this, but

wrap :: (MyClass a) => (String -> String) -> a -> String
wrap f = f . prints

tuplify :: (MyClass b) => a -> b
tuplify (a,b) = (a,b)
tuplify (a,b,c) = (a,b,c)
tuplify a = OneTuple a

stringFunction :: String -> String
stringFunction = id

vectorToString x = wrap stringFunction $ tuplify x

doesn't compile. I am trying to arrive at something that looks like this

vectorToString (1,2,3)
vectorToString (1,2)
vectorToString 1

Is this even possible? I think I remember reading something about how a function's behavior should not change depending on the input type, which is exactly what tuplify is doing. Is there a different way I could solve this problem? This is the error message from ghci:

question.hs:18:9: error:
    • Couldn't match expected type ‘a’ with actual type ‘(a0, b0)’
      ‘a’ is a rigid type variable bound by
        the type signature for:
          tuplify :: forall b a. MyClass b => a -> b
        at question.hs:17:1-32
    • In the pattern: (a, b)
      In an equation for ‘tuplify’: tuplify (a, b) = (a, b)
    • Relevant bindings include
        tuplify :: a -> b (bound at question.hs:18:1)
   |
18 | tuplify (a,b) = (a,b)
   |         ^^^^^

question.hs:18:17: error:
    • Couldn't match expected type ‘b’ with actual type ‘(a0, b0)’
      ‘b’ is a rigid type variable bound by
        the type signature for:
          tuplify :: forall b a. MyClass b => a -> b
        at question.hs:17:1-32
    • In the expression: (a, b)
      In an equation for ‘tuplify’: tuplify (a, b) = (a, b)
    • Relevant bindings include
        b :: b0 (bound at question.hs:18:12)
        a :: a0 (bound at question.hs:18:10)
        tuplify :: a -> b (bound at question.hs:18:1)
   |
18 | tuplify (a,b) = (a,b)
   |                 ^^^^^

Upvotes: 0

Views: 74

Answers (1)

leftaroundabout
leftaroundabout

Reputation: 120711

I think I remember reading something about how a function's behavior should not change depending on the input type

Sounds like you're thinking of the Liskov substitution principle. But that's something from the OO world, which doesn't apply to Haskell. You can't even really formulate it, because Haskell has no subtypes.

Haskell does have a related principle though: parametrically polymorphic functions can't have different behaviour depending on which types are used. But this is not so much a principle the programmer should obey, as something that's enforced by the language. That's one reason why your different cases for tuplify can't possibly compile: there you try to, like, pattern-match on types. This is not something you can do in Haskell function declarations.

However, it is almost exactly what typeclasses do, and indeed your own typeclass does it already.

There's another problem with your tuplify: the signature MyClass b => a -> b means the caller gets to pick what both a and b are, and then the function would somehow have to be able to convert between them. For example it would have to be able to convert a 2-tuple into a 3-tuple. What you probably intend to express there is that every input type is associated a result type – i.e. a type-level function. We call those type families, and indeed you can write

{-# LANGUAGE TypeFamilies #-}

type family Tuplified t where
  Tuplified (a,b) = (a,b)
  Tuplified (a,b,c) = (a,b,c)
  Tuplified t = t

However, that still doesn't really bring you any further: now you have a type-level function, but no corresponding value-level functions for the individual cases. That function would have to have type

tuplify :: t -> Tuplified t

but you can't define it without resorting to a type class. You could write such a type class, but then you would just run into the same problem you tried to solve in your other question in the first place!

I don't see how any of this makes sense. Probably, your class instances should actually be more like the standard Show class:

instance MyClass Integer where prints = show
instance MyClass Int where prints = show

instance (MyClass a, b~a) => MyClass (a,b) where
  prints (x,y) = '(' : prints x ++ "," ++ prints y ++ ")"

Upvotes: 1

Related Questions