I was experimenting with phantom types in Haskell. My goal is to convert the LangCode Type to it's corresponding Phantom Type representation via Type Classes for example DE to Lang DE.
module Main (main) where
import Data.Proxy (Proxy(..))
data DE
data EN
data LangCode
= DE
| EN
deriving (Eq, Show)
type Lang a = Proxy a
de :: Lang DE
de = Proxy
en :: Lang EN
en = Proxy
class ToLangCode a where
toLangCode :: Lang a -> LangCode
instance ToLangCode DE where
toLangCode _ = DE
instance ToLangCode EN where
toLangCode _ = EN
class FromLangCode a where
fromLangCode :: LangCode -> Lang a
instance FromLangCode DE where
fromLangCode DE = Proxy
instance FromLangCode EN where
fromLangCode EN = Proxy
main :: IO ()
main = do
print $ de -- Output => Proxy
print $ en -- Output => Proxy
print $ toLangCode de -- Output => DE
print $ toLangCode en -- Output => EN
-- works
print $ (fromLangCode DE :: Lang DE) -- Output => Proxy
print $ (fromLangCode EN :: Lang EN) -- Output => Proxy
-- throws an error
print $ fromLangCode DE -- Output => Proxy
print $ fromLangCode EN -- Output => Proxy
With a type annotation it works fine. But without it I get this error.
[1 of 1] Compiling Main ( main.hs, main.o )
main.hs:50:11: error:
* Ambiguous type variable `a0' arising from a use of `fromLangCode'
prevents the constraint `(FromLangCode a0)' from being solved.
Probable fix: use a type annotation to specify what `a0' should be.
These potential instances exist:
instance FromLangCode DE -- Defined at main.hs:32:10
instance FromLangCode EN -- Defined at main.hs:34:10
* In the second argument of `($)', namely `fromLangCode DE'
In a stmt of a 'do' block: print $ fromLangCode DE
In the expression:
do print $ de
print $ en
print $ toLangCode de
print $ toLangCode en
....
|
50 | print $ fromLangCode DE -- Output => Proxy
| ^^^^^^^^^^^^^^^
main.hs:51:11: error:
* Ambiguous type variable `a1' arising from a use of `fromLangCode'
prevents the constraint `(FromLangCode a1)' from being solved.
Probable fix: use a type annotation to specify what `a1' should be.
These potential instances exist:
instance FromLangCode DE -- Defined at main.hs:32:10
instance FromLangCode EN -- Defined at main.hs:34:10
* In the second argument of `($)', namely `fromLangCode EN'
In a stmt of a 'do' block: print $ fromLangCode EN
In the expression:
do print $ de
print $ en
print $ toLangCode de
print $ toLangCode en
....
|
51 | print $ fromLangCode EN -- Output => Proxy
| ^^^^^^^^^^^^^^^
exit status 1
My question is. Is it possible to implement it in a way so that the type annotations are no longer needed?
Updated version
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE KindSignatures #-}
{-# LANGUAGE StandaloneDeriving #-}
{-# LANGUAGE FlexibleInstances #-}
module Main (main) where
data LangCode = DE | EN deriving (Eq, Show)
data SLangCode a where
SDE :: SLangCode DE
SEN :: SLangCode EN
data Lang (a :: LangCode) where
LangDE :: Lang 'DE
LangEN :: Lang 'EN
deriving instance Show (Lang 'DE)
deriving instance Show (Lang 'EN)
slcDE :: SLangCode a -> Lang 'DE -> Lang a
slcDE SDE t = t
slcDE SEN _ = LangEN
slcEN :: SLangCode a -> Lang 'EN -> Lang a
slcEN SEN t = t
slcEN SDE _ = LangDE
class ToLangCode a where
toLangCode :: Lang a -> LangCode
instance ToLangCode 'DE where
toLangCode _ = DE
instance ToLangCode 'EN where
toLangCode _ = EN
class FromLangCode a where
fromLangCode :: SLangCode a -> LangCode -> Lang a
instance FromLangCode 'DE where
fromLangCode SDE DE = LangDE
instance FromLangCode 'EN where
fromLangCode SEN EN = LangEN
main :: IO ()
main = do
print $ toLangCode LangDE -- Output => DE
print $ toLangCode LangEN -- Output => EN
print $ fromLangCode SDE DE -- Output => LangDE
print $ fromLangCode SEN EN -- Output => LangEN
Here's the problem. It's a fundamental property of types that if the term-level expressions
e1ande2have the same typet, then substitutinge1withe2in a program doesn't change any of the program's types. That's what makes them types.You want the expression (with no explicit type signature):
to have type
Lang EN, and that's easy enough. But if the term-level expressionsENandDEare constructors from the same sum type,LangCode, then substituting one for the other won't change any types, so the expression:will still have type
Lang EN, which is clearly not what you want.So, if you want two different inferred types:
then any solution is going to require that the term-level expressions
ENandDEhave different types, which means that you can't have:So, that's the short answer -- you can't freely convert between a sum type
LangCodeand proxies of typeLang lang. Or rather, you can easily convert from typeLang langto termLangCodeusing a type class (likeToLangCode), but you can't really convert back.There are many solutions to this "problem", but it kind of depends on what you're trying to do, which is why folks in the comments are asking you questions about "use cases" and "expected behavior".
A Simple Solution that Accomplishes Nothing
One simple solution is to write:
Here, the term-level expressions
ENandDEhave different types (ENandDErespectively). This allows you to easily implement the desired interface using yourmainfunction verbatim:If you're suspicious of this "solution", you're right to be. It doesn't really accomplish anything, since the term-level expressions
ENandDEare already distinct at the type level, and this program is really just converting between one type level representation (typesENandDE) and another (typesLang ENandLang DE).Using a GADT
One way to kind of do what you want is to use a GADT. If we define
LangCodeas a "generalized" sum type:everything works more or less like my previous example, with a few minor changes to type signatures, and
mainleft unchanged, as below:So, we can freely convert between this generalized sum type and a phantom type representation.
This really isn't a big improvement over the previous example. The two constructors are now formally part of a generalized sum type, but the term-level expressions
ENandDEare already distinct at the type level, and we're just converting between one type-level representation (typesLangCode ENandLangCode DE) and another (typesLang ENandLang DE).However, the same criticism could be levelled at your "updated example". By introducing singletons (a generalized sum type), you too are already making the expressions
fromLangCode SDE DEandfromLangCode SEN ENdistinct at the type-level in that first, singleton argument. The second, term-level argument plays no useful role here and could be eliminated, so you're just converting from one type-level representation (SLangCode DEversusSLangCode EN) to another (Lang DEversusLang EN).Existental Types
Actual useful conversion between term and type-level representations usually involves an existential type somewhere in the mix. It helps to consider a slightly more realistic example. Suppose you might want to be able to use the type system to help avoid inappropriately mixing languages. For example:
But, you might also like to make a run-time decision about which language is being used in a particular expression:
This doesn't work because the type of
mytextcan't depend on a runtime computation. That is, there's no simple way to convert the runtime term-level valuelanguage :: LangCode(a sum type) to a type-level valueLang language, the desired type ofmytext.The usual solution in is to use an existential type:
Here, the type
SomeTextrepresents text in some unspecified language (i.e., a value of typeText langfor some unspecified typelang). Now,mytextcan be assigned text in a runtime-determined language by wrapping it with theSomeTextconstructor.We are limited in what we can do with a
SomeTextvalue likemytext-- we can't do anything that depends on knowing the language, like applyingpluralizeorfixßor whatever. However, one thing we can do is extract the string, since that works for any language:which allows us to write a useful
main:Here's the full working example:
What we've done here is successfully converted a term-level value from a sum type (
language) to a type-level valueText languageby wrapping it in aSomeTextconstructor.Applied to Your Example
We can use the same technique to convert freely between a sum type and type-level proxies, by shielding the type-level proxies in an existential. It might look something like this. Note how I've used a custom
Showinstance to differentiate proxies of different types, to prove we're doing something useful.