Haskell 如何使用受约束的元素创建异构列表(又称HLists)?

Haskell 如何使用受约束的元素创建异构列表(又称HLists)?,haskell,Haskell,我一直在尝试使用类型族来抽象UI工具包。当我试图利用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工具包。当我试图利用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之类的东西,但要么不可能,要么希望如此,我只是缺少必要的fu类型

以下是我的尝试:

{-# 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构造函数旨在保持构建的HList的UI tk参数一致

这是我第一次体验类型族和多参数类型类,所以我可能遗漏了一些基本的东西


是否可以使用受约束的元素构造异构列表?我哪里出错了?

类型错误来自此逻辑链:“ui”的最外层为“structui”,而“structui::(FieldList tk l)=>“需要”(FieldList tk l),其中“tk”和“l”必须与您为“ui”编写的类型签名相匹配

在类型变量“tk”中,每样东西都是多态的

类型检查器为structui/fcons的参数提供了一个不同的tk0,并且仅仅因为您有一个具有匹配tk的实例,并不意味着我不会出现并创建一个具有不同tk的FieldList实例。因此,类型检查器被卡住

以下是我如何为类型检查器修复此问题:

-- 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的评论: 考虑这个代码,请注意(TeNUM 97):查尔是“A”< /P>
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))
此操作失败,并显示以下消息:

为什么??类型检查器推断(toEnum 98)有一些枚举类型,这可能是Char,但它不会推断它必须是Char。即使(TwoParam Char b)的唯一可用实例需要将b与Char匹配,类型检查器也不会将(toEnum 97)与Char匹配。编译器在这里是正确的,因为我可以稍后编写另一个实例:

--  instance TwoParam Char Integer
对于第二个(重叠)实例,应该选择哪个实例不再明显。解决方法是使用上述“技巧”:

-- instance (c ~ d) => TwoParam c d
类型检查器在选择实例时只查看“TwoParam c d”,这与所有内容都匹配。然后它尝试满足约束

Char~typeOf(来自Enum 98)


这会成功的。使用技巧“main”打印('a','a')

您可以添加
mapUI::(a->b)->UI-tk-a->UI-tk-b
;然后,您只需要
zipUI::UI-tk-a->UI-tk-b->UI-tk(a,b)
,就可以使用
mapUI
展平嵌套的元组。当然,这相当于要求
UI-tk
成为
Applicative
的一个实例(假设可以提供
pure
),这是我的建议。以上是我真正API的简化-我已经有了mapUI。我也使用过类似嵌套元组的方法,但在实践中效果不太好,因为构建具体UI的代码需要一次看到所有子对象,而不是一次看到一对子对象。。。ie构建复合UI需要是一个n元函数的单一应用,而不是二元函数的重复应用。这很公平。我仍然建议在实例头中需要
Functor(UI-tk)
,而不是定义自己的
mapUI
函数。实际上,只要看一下我的代码,我就意识到mapUI需要是双向的,并处理错误。所以我的签名是:mapUI::(a->ErrVal b)->(b->a)->UI-tk-a->UI-tk-b。它不可能是函子。谢谢你的建议。啊,我明白了。这让我想起,;如果你还没有看的话,你可能想看一看。用一个过度多态的实例头延迟统一可能是我最喜欢的类型级技巧。它非常有用。我同意这感觉像是骗局。最终,在类型/实例声明中编程只是为了指导类型推断引擎中的统一。虽然在这种情况下,问题更多的是实例选择工作方式的局限性。但是一般来说,很多类型级别的东西都没有简单的一级表示,所以你必须这样做才能间接得到你想要的。谢谢!我不知道这样的“平等约束”。我仍然有点不清楚为什么我必须将这两个参数声明为不同的名称(tk,tk'),然后将它们等同于“tk~tk”。如果我统一使用一个名字,为什么不起作用?
-- instance (c ~ d) => TwoParam c d