11

I have a class as follows:

class Token a where
 symbol :: a -> String

I also want all instances of Token to have a function convert which returns a parametrised type. The conversion alone works fine:

class Token a b where
 convert :: a -> b
data Egal = One | Two
instance Token Egal Int where
 convert One = 111
 convert Two = 222
main = print $ show (convert One :: Int)

But when I try to use both symbol and convert I get errors about ambiguity. This is my code:

class Token a b where
 convert :: a -> b
 symbol :: a -> String
data Egal = One | Two
instance Token Egal Int where
 convert One = 111
 convert Two = 222
 symbol One = "one"
 symbol Two = "two"
main = print $ show (convert One :: Int)

What am I doing wrong?


EDIT: Reading my own question I started wondering: Should these be two distinct classes and my data Egal show instanciate both?

6
  • Do you want every token to convert to only one type or to multiple types? Commented Sep 14, 2023 at 3:31
  • Perhaps you need Token a b | a->b (read up on functional dependencies). Commented Sep 14, 2023 at 3:56
  • @FyodorSoikin Thanks for your comment. To only one type. But that type is not known beforehand. Commented Sep 14, 2023 at 4:09
  • 1
    Can you add the error message the compiler gives? It helps make this question more discoverable. Commented Sep 14, 2023 at 5:39
  • 1
    The error message tells you exactly what language extension to enable. Commented Sep 14, 2023 at 7:03

2 Answers 2

17

As you have defined things here, you can have instances with the same a but conflicting bs. Like this:

instance Token Char Char where
 convert = id
 symbol c = [c]
instance Token Char Bool where
 convert = (>'m')
 symbol c = [c, c, c, c]

Now, should symbol 'x' be "x" or "xxxx"? Both are possible depending which of the above instances gets chosen; it is ambiguous which instance should be used for symbol, and therefore which answer you should get. There are various ways to fix this. One is to simply allow the ambiguity, and give yourself the ability to specify which instance to use at call sites. You can turn on the AllowAmbiguousTypes and TypeApplications extensions; then:

> symbol @_ @Char 'x' -- or, more explicitly, symbol @Char @Char 'x'
"x"
> symbol @_ @Bool 'x' -- explicitly, symbol @Char @Bool 'x'
"xxxx"

But in many cases, you really want the compiler to check that you haven't made multiple instances with conflicting as. Then you can use either the FunctionalDependencies extension:

class Token a b | a -> b where
 convert :: a -> b
 symbol :: a -> String
instance Token Char Char where {- ... -}

or the TypeFamilies extension:

class Token a where
 type Conversion a
 convert :: a -> Conversion a
 symbol :: a -> String
instance Token Char where
 type Conversion Char = Char
 {- ... -}

They have more or less the same effect in most cases: conflicting instances are flagged, and there is no ambiguity left.

answered Sep 14, 2023 at 6:46
Sign up to request clarification or add additional context in comments.

Comments

14

In the comments, you wrote that a type a can only be converted to a single type b. Your current design allows to have many bs for the same a.

instance Token A B1 where
 convert = ...
 symbol = ...
instance Token A B2 where
 convert = ...
 symbol = ...

Why GHC complains? Above, both definitions of symbol have type A -> String, so GHC can't choose which instance to use from types only.

One (non ideal solution) is to enable the "ambiguous types" Haskell extension and choose the instance manually, every time symbol is called.

main = putStrLn $ symbol @A @B1 someAvalue

It would be better to instead tell Haskell that type b is determined from type a alone, so there's no ambiguity. This can be done using the functional dependencies extension, but in my opinion it's best achieved using the type families extension.

class Token a where -- no b here
 type Converted a -- the type after conversion
 convert :: a -> Converted a -- we return that type
 symbol :: a -> String
data Egal = One | Two
instance Token Egal where
 type Converted Egal = Int -- here we chose Int
 convert One = 111
 convert Two = 222
 symbol One = "one"
 symbol Two = "two"
main = print $ convert One -- no annotation needed
answered Sep 14, 2023 at 6:47

Comments

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.