How to define polymorphic tagless final lists

79 views Asked by At

After reading the excellent and interesting paper Typed Tagless-Final Interpretations: Introductory Course from Oleg Kiselyov I tried to convert normal Haskell lists to Tagless Final lists and failed:

This is working:

{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}

module TList

where

class TList repr  where
  tnil :: repr
  tcons :: Int -> repr -> repr

tlist0 :: TList repr => repr
tlist0 = tnil

tlist1 :: TList repr => repr
tlist1 = tcons 1 $ tcons 2 $ tcons 3 tnil

instance TList String where
  tnil = "tnil"
  tcons hd tail = "tcons " ++ show hd ++ " $ " ++ tail

view :: String -> String
view = id

Executing view tlist1 in GHCi gives tcons 1 $ tcons 2 $ tcons 3 $ tnil

With the definition above the type of the elements of the list is restricted to Int! I would like to have polymorphic lists and tried:

class PList repr a where
  pnil :: repr a
  pcons :: a -> repr a -> repr a

plist0 :: forall (repr :: * -> *) a. PList repr a => repr a
plist0 = pnil

plist1 :: PList repr Int => repr Int
plist1 = pcons 1 $ pcons 2 $ pcons 3 pnil

This compiles, but I'm unable to define the instance for the interpreter:

instance PList String where
  pnil = "pnil"
  pcons hd tail = "pcons " ++ show hd ++ " $ " ++ tail

This gives the error:

[1 of 1] Compiling TList            ( List/List.hs, interpreted ) [Source file changed]

List/List.hs:30:10: error:
    • Expecting one more argument to ‘PList String’
      Expected a constraint,
        but ‘PList String’ has kind ‘* -> Constraint’
    • In the instance declaration for ‘PList String’
   |
30 | instance PList String where
   |          ^^^^^^^^^^^^

List/List.hs:30:16: error:
    • Expected kind ‘* -> *’, but ‘String’ has kind ‘*’
    • In the first argument of ‘PList’, namely ‘String’
      In the instance declaration for ‘PList String’
   |
30 | instance PList String where
   |                ^^^^^^

So PList should have 2 type arguments! Changing to instance PList String Int where gives:

List/List.hs:30:16: error:
    • Expected kind ‘* -> *’, but ‘String’ has kind ‘*’
    • In the first argument of ‘PList’, namely ‘String’
      In the instance declaration for ‘PList String Int’
   |
30 | instance PList String Int where
   |                ^^^^^^

So I should have a type with kind (* -> *) like Functor or so, but I don't have it. How to fix?

1

There are 1 answers

0
leftaroundabout On BEST ANSWER

It depends on whether or not you want to retain the type information of the "contained" element in the representation. You could certainly keep it, through a phantom argument

newtype TypyString a = TypyString { stringyString :: String }

which has a suitable kind for the current class.

But I think it's more in the spirit of the original to not carry that information around. Then it doesn't make sense for repr a to appear in the signatures. It doesn't need to, you can just have a plain repr there. GHC may balk at you for having ambiguous types, but that's not a problem any longer with the right extension:

{-# LANGUAGE AllowAmbiguousTypes #-}

class PList repr a where
  pnil :: repr
  pcons :: a -> repr -> repr

and then you can define

instance PList String Int where
  pnil = "pnil"
  pcons hd tail = "pcons " ++ show hd ++ " $ " ++ tail

...or more generally,

instance Show a => PList String a where
  ...

The examples will however need to take care of the ambiguity:

{-# LANGUAGE ScopedTypeVariables, TypeApplications, UnicodeSyntax #-}

plist0 :: ∀ repr a . PList repr a => repr
plist0 = pnil @repr @a

plist1 :: ∀ repr . PList repr Int => repr
plist1 = pcons @repr @Int 1
       . pcons @repr @Int 2
       . pcons @repr @Int 3
       $ pnil @repr @Int

This is quite awkward, and also unnecessary because the repr argument isn't actually ambiguous, rather the a one is. It's nicer when the type arguments are swapped:

class PList a repr where
  pnil :: repr
  pcons :: a -> repr -> repr

instance PList Int String where
  ...

plist0 :: ∀ a . PList a => repr
plist0 = pnil @a

plist1 :: PList repr Int => repr
plist1 = pcons @Int 1 . pcons @Int 2 . pcons @Int 3 $ pnil @Int

You wouldn't even need the @Int applications for the pconses if the number-literals where unambiguous, which they aren't in Haskell but could be made to be:

plist1 :: PList Int repr => repr
plist1 = pcons n1 . pcons n2 . pcons n3 $ pnil @Int
 where n1,n2,n3 :: Int
       (n1,n2,n3) = (1,2,3)