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

时间:2012-03-01 11:31:39

标签: 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 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构造函数旨在使UI tk参数与构造的HList保持一致。

这是我对类型系列和多参数类型类的第一次体验,所以很可能我缺少一些基本的东西。

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

1 个答案:

答案 0 :(得分:7)

类型错误来自这个逻辑链:'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的评论: 考虑一下这段代码,请注意(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)有一些枚举类型,这可能是Char,但它不会推断它必须是Char。类型检查器将不匹配(toEnum 97)到Char,即使(TwoParam Char b)的唯一可用实例需要与Char匹配b。编译器在这里是正确的,因为我以后可以编写另一个实例:

--  instance TwoParam Char Integer

使用这个第二个(重叠)实例,不再明显应该选择哪个实例。解决方案是使用上面的'技巧':

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

类型检查器在选择实例时只查看'TwoParam c d',这匹配所有内容。然后它试图满足约束

  

Char~typeOf(fromEnum 98)

哪个会成功。使用“主要”打印技巧('a','a')