Как сделать гетерогенные списки (aka HLists) с ограниченными элементами?

Я экспериментировал с использованием семейств типов для абстрактного набора инструментов пользовательского интерфейса. Я раскрутился, пытаясь использовать HLists (http://homepages.cwi.nl/~ralf/HList/), чтобы улучшить API.

Первоначально мой API выглядел примерно так:

{-# LANGUAGE TypeFamilies #-}

class UITK tk where
  data UI tk :: * -> *

  stringEntry :: (UITK tk) => UI tk String
  intEntry :: (UITK tk) => UI tk Int

  tuple2UI :: (UI tk a,UI tk b) -> (UI tk (a,b))
  tuple3UI :: (UI tk a,UI tk b,UI tk c) -> (UI tk (a,b,c))
  tuple4UI :: (UI tk a,UI tk b,UI tk c,UI tk d) -> (UI tk (a,b,c,d))

ui :: (UITK tk) => (UI tk (String,Int)) 
ui = tuple2UI (stringEntry,intEntry)

Это работает, но объединитель UI работает с кортежами, поэтому мне нужна другая функция для каждого размера кортежа. Я думал, что могу использовать что-то вроде HLists, но либо это невозможно, (или, надеюсь), мне просто не хватает нужного типа-фу.

Здесь моя попытка:

{-# LANGUAGE TypeFamilies,FlexibleInstances,MultiParamTypeClasses #-}

-- A heterogeneous list type 

data HNil = HNil deriving (Eq,Show,Read)
data HCons e l = HCons e l deriving (Eq,Show,Read) 

-- A list of UI fields, of arbitrary type, but constrained on their
-- tk parameter. The StructV associated type captures the return
-- type of the combined UI

class (UITK tk) => FieldList tk l
  where type StructV tk l

instance (UITK tk) => FieldList tk HNil
  where type StructV tk HNil = HNil

instance (UITK tk, FieldList tk l) => FieldList tk (HCons (UI tk a) l)
  where type StructV tk (HCons (UI tk a) l) = (HCons a (StructV tk l))

fcons :: (UITK tk, FieldList tk l) => UI tk a -> l  -> HCons (UI tk a) l
fcons = HCons

-- Now the abstract ui toolkit definition

class UITK tk where
    data UI tk :: * -> *

    stringEntry :: (UITK tk) => UI tk String
    intEntry :: (UITK tk) => UI tk Int

    structUI :: (FieldList tk l) => l -> (UI tk (StructV tk l))

-- this doesn't work :-(

ui :: (UITK tk) => (UI tk (HCons String (HCons Int HNil)))
ui = structUI (fcons stringEntry
              (fcons intEntry
              HNil ))

Определение в конце дает мне несколько ошибок, первая из которых:

Z.hs:38:6:
    Could not deduce (FieldList
                        tk (HCons (UI tk0 String) (HCons (UI tk1 Int) HNil)))
      arising from a use of `structUI'
    from the context (UITK tk)
      bound by the type signature for
                 ui :: UITK tk => UI tk (HCons String (HCons Int HNil))
      at Z.hs:(38,1)-(40,21)
    Possible fix:
      add (FieldList
             tk
             (HCons
                (UI tk0 String) (HCons (UI tk1 Int) HNil))) to the context of
        the type signature for
          ui :: UITK tk => UI tk (HCons String (HCons Int HNil))
      or add an instance declaration for
         (FieldList tk (HCons (UI tk0 String) (HCons (UI tk1 Int) HNil)))
    In the expression:
      structUI (fcons stringEntry (fcons intEntry HNil))
    In an equation for `ui':
        ui = structUI (fcons stringEntry (fcons intEntry HNil))

Не понимая этого, я думаю, что вижу хотя бы одну из проблем. Я не успеваю сообщить компилятору, что параметры типа 3 tk имеют одинаковый тип (т.е. Он относится к tk, tk0, tk1) выше. Я не понимаю этого: мой конструктор fcons должен поддерживать согласованные параметры UI tk для построенного HList.

Это мой первый опыт работы с типами типов и классами с несколькими параметрами, поэтому, скорее всего, мне не хватает чего-то фундаментального.

Можно ли создать гетерогенные списки с ограниченными элементами? Где я ошибаюсь?

Ответ 1

Ошибка типа из этой цепочки логики: 'ui' имеет 'structui' outermost и 'structUI:: (FieldList tk l) = > ' нуждается в '(FieldList tk l)', где 'tk' и ' l 'должен соответствовать типу, который вы написали для' ui '.

Все, индивидуально, полиморфно в переменной типа "tk".

Контроллер типа дает другой аргумент tk0 аргументу structui/fcons, и только потому, что у вас есть экземпляр с соответствующим tk, это не значит, что я не приду и не сделаю экземпляр FieldList с отличными tk. Таким образом, проверка типа застревает.

Вот как я могу исправить это для проверки типов:

-- Use this instance instead of the one you wrote
instance (UITK tk, FieldList tk l, tk ~ tk') => FieldList tk (HCons (UI tk' a) l)
  where type StructV tk (HCons (UI tk' a) l) = (HCons a (StructV tk l))

-- Now this works :)
ui :: (UITK tk) => (UI tk (HCons String (HCons Int HNil)))
ui = structUI (fcons stringEntry
              (fcons intEntry
              HNil ))

Экземпляр замены соответствует всем возможным комбинациям tk и tk ', а затем требует, чтобы они были одинаковыми. Никто не может прийти и написать другой такой экземпляр без наложения.

Отвечая на комментарий к timbod: Рассмотрим этот код, обратите внимание, что (toEnum 97):: Char является 'a'

class TwoParam a b where
  combine :: a -> b -> (a,b)
  combine = (,)

instance TwoParam c c

t1 :: (TwoParam Char b) => Char -> b -> (Char,b)
t1 = combine

main = print (t1 'a' (toEnum 97))

Это сообщение не выполняется:

No instance for (TwoParam Char b0) arising from a use of `t1'
Possible fix: add an instance declaration for (TwoParam Char b0)
In the first argument of `print', namely `(t1 'a' (toEnum 98))'
In the expression: print (t1 'a' (toEnum 98))
In an equation for `main': main = print (t1 'a' (toEnum 98))
Failed, modules loaded: none.

Почему? Контроллер типа сообщает, что (toEnum 98) имеет некоторый тип Enum, и это может быть Char, но он не сделает вывод, что он должен быть Char. Контроллер типа не будет соответствовать (toEnum 97) на Char, хотя единственный доступный экземпляр (TwoParam Char b) потребует совпадения b с Char. Компилятор здесь корректен, потому что позже я мог бы написать другой экземпляр:

--  instance TwoParam Char Integer

С этим вторым (перекрывающимся) экземпляром уже не очевидно, какой экземпляр следует выбрать. Решение заключается в использовании вышеуказанного "трюка":

-- instance (c ~ d) => TwoParam c d

Контроллер типа смотрит только на "TwoParam c d" при выборе экземпляра, и это соответствует всем. Затем он пытается удовлетворить ограничению

Char ~ typeOf (fromEnum 98)

который будет успешным. С трюком "основные" отпечатки ('a', 'a')