Haskell:在不同的模块中添加重叠实例中使用的新数据类型

时间:2013-12-12 19:22:11

标签: haskell types

这是我在Haskell中遇到的一个问题。

背景

我希望能够将数据类型的“东西”转换为字符串。增加的复杂性是,有时结果字符串可能会有所不同,具体取决于所使用的“类型”(也是数据类型)。此外,我希望用户能够在自己的模块中自由添加自己的“东西”和“类型”,而无需修改自己的代码。最后但并非最不重要的是,“事物”可以嵌套,因此“A”类型的“事物”可以包含“B”类型的“事物”。

代码

希望通过一些代码以及我到目前为止所做的工作更加清晰:

正在使用以下GHC扩展程序:

{-# LANGUAGE FlexibleInstances, OverlappingInstances #-}

好的,首先使用带有两个构造函数的数据类型定义“类型”:

  • 默认行为(“DefaultType”)
  • 自定义行为(“MyTypeA”)。
data Types = DefaultType | MyTypeA

然后,定义函数“toString”。根据上述类型的构造函数,它的行为会有所不同:

toString :: (MyTypeAString a, DefaultString a) => Types -> a -> String
toString DefaultType a = toDefaultString DefaultType a
toString MyTypeA a = toMyTypeAString a

现在,我们要创建两个类“MyTypeAString”和“DefaultString”。让我们从“DefaultString”开始:

class DefaultString a where
    toDefaultString :: Types -> a -> String

“a”可能是任何要转换为字符串的东西。让我们创建两个“事物”:

data TheThingA = TheThingA TheThingB
data TheThingB = TheThingB

请注意,“TheThingB”是“TheThingA”的一部分。您将看到DefaultString类实例的实现结果:

instance DefaultString TheThingA where
    toDefaultString myType (TheThingA thingB) = "Thing A has " ++ toString myType thingB

instance DefaultString TheThingB where
    toDefaultString myType thing = "a thing B!"

重要的是调用“TheThingA”实例中的“toString”函数。这个调用是我遇到的问题的根源,我们稍后会看到。

现在,让我们创建“MyTypeA”类:

class MyTypeAString a where
    toMyTypeAString :: (DefaultString a) => a -> String

第一个实例是一般实例(因此使用“FlexibleInstances”GHC扩展名),它将使用“MyTypeA”完全像“toDefaultString”:

instance MyTypeAString a where
    toMyTypeAString thing = toDefaultString MyTypeA thing 

第二个例子如下:

instance MyTypeAString TheThingB where
    toMyTypeAString thing = "a thing B created by MyTypeA"

第二个实例是“MyTypeAsString”的“TheThingB”的特定实现。由于此实例与第一个实例重叠,因此我们使用“OverlappingInstances”。

现在让我们进行一些测试,看看这一切是如何表现的:

test1 = toString DefaultType (TheThingA TheThingB)
> "Thing A has a thing B!"

test2 = toString MyTypeA (TheThingA TheThingB)
> "Thing A has a thing B created by MyTypeA"

到目前为止取得的成就

那么,这是怎么回事?我们为“toString”函数设置了一个基本行为,无论使用“DefaultType”还是“MyTypeA”,它都会以相同的方式运行。您现在可以想象我们不仅有“TheThingA”和“TheThingB”,还有数百种其他数据类型,每种数据类型都有自己的“DefaultString”类专用实例。 另一方面,假设类“MyTypeAString”对于这些数据类型的90%具有完全相同的方式,因此具有非常少的特定实例。使用“OverlappingInstances”,我们可以保存数百行代码,并且仅针对“MyTypeAString”需要不同行为的情况具有特定实例,这非常简洁。

问题

到目前为止,这么好。但现在,我想创建一个新的“类型”,我想称之为“MyTypeB” 这不是太复杂 - 我只能修改数据类型“Types”和“toString”函数类型签名 - 但它不是很干净,在某种意义上我需要更改“Types”数据类型本身和“toString” “功能在同一个模块中。我想要实现的是让用户在自己的模块中定义自己的类型和相关类,而无需修改“toString”函数和“Types”数据类型。 但是,到目前为止,我找不到实现这一目标的方法,所以问题是如何做到这一点?

非常感谢您的帮助: - )

3 个答案:

答案 0 :(得分:0)

我找到了问题的答案。

这个想法首先将“类型”作为类型类而不是数据类型,具有“DefaultType”的实例和“MyTypeA”的实例。然后,我们可以将函数“toString”转换为多参数类型的类。最后但并非最不重要的是,“DefaultString”类型类成为一个多参数,它可以在其实例的头部定义类型约束。这是代码:

{-# LANGUAGE FlexibleContexts
           , FlexibleInstances
           , MultiParamTypeClasses
           , OverlappingInstances
           , UndecidableInstances #-}

data DefaultType = DefaultType
data MyTypeA = MyTypeA

data TheThingA = TheThingA TheThingB
data TheThingB = TheThingB

{- |
    This type class only purpose is to be able to use it as constraint in type 
    signatures. It therefor implements only a dummy function.
-}
class Types a where
    -- | A dummy function.
    getName :: a -> String

instance Types DefaultType where
    getName a = "Default Type"

instance Types MyTypeA where
    getName a = "My Type A"

{- |
     The "toString" function is converted to a type class.
     The Types class is used to constraint type a.
-}
class Stringable a b where
    toString :: Types a => a -> b -> String

{- |
    Constraints are put in the instance head declaration. This way, one can add new Types
    such as MyTypeA, without modifying the toString type signature itself.
-}
instance (DefaultString MyTypeA b, MyTypeAString b) => Stringable MyTypeA b where
    toString a b = toMyTypeAString b

instance DefaultString DefaultType b => Stringable DefaultType b where
    toString = toDefaultString

class DefaultString a b where
    toDefaultString :: Types a => a -> b -> String

{- |
    Multiparam type class allows us to put a constraint on the second parameter of the
    function which is necessary due to the use of the toString function. 
-}
instance Stringable a TheThingB => DefaultString a TheThingA where
    toDefaultString myType (TheThingA thingB) =  "Thing A has " ++ toString myType thingB

instance DefaultString a TheThingB where
    toDefaultString myType thing = "a thing B!"

class MyTypeAString a where
    toMyTypeAString :: DefaultString MyTypeA a => a -> String

instance MyTypeAString a where
    toMyTypeAString thing = toDefaultString MyTypeA thing 

instance MyTypeAString TheThingB where
    toMyTypeAString thing = "a thing B created by MyTypeA"

我们可以运行我们的测试:

test1 = toString DefaultType (TheThingA TheThingB)
> "Thing A has a thing B!"

test2 = toString MyTypeA (TheThingA TheThingB)
> "Thing A has a thing B created by MyTypeA"

更进一步

现在可以创建一个新类型,例如“MyType”作为“类型”的新实例。我们可以通过创建一个新类来定义这种新类型的自定义行为,例如“MyTypeBString”,就像我们为“MyTypeAString”所做的那样。所有这些都可以在一个独立的模块中完成,这为我们提供了很大的灵活性。

如果您找到更好的答案(例如,不依赖于如此多的GHCI扩展或更简单的类型签名),请不要犹豫发布: - )

答案 1 :(得分:0)

我用一个更好的解决方案再次回答我自己的问题,这个解决方案不需要任何扩展和更短的时间!

data DefaultType = DefaultType
data MyTypeA = MyTypeA

data TheThingA = TheThingA TheThingB
data TheThingB = TheThingB

{-|
We define in a type class a specific toString functions for each data type
(TheThingA and TheThingB). The one for TheThingA is our "toString" function.

On top of that, a default implementation is added.
-}    
class Stringable a where
    toString :: a -> TheThingA -> String
    toString a (TheThingA thingB) = "Thing A has " ++ toStringB a thingB

    toStringB :: a -> TheThingB -> String
    toStringB a thing = "a thing B!"

{-|
Since the default implementation covers the behavior for the DefaultType,
there is no need to specify any functions for this instance.
-}
instance Stringable DefaultType where

{-|
For the MyTypeA class, only the toStringB function needs to be defined.
-}    
instance Stringable MyTypeA where
    toStringB a thing = "a thing B created by MyTypeA"

我们可以运行我们的测试:

test1 = toString DefaultType (TheThingA TheThingB)
> "Thing A has a thing B!"

test2 = toString MyTypeA (TheThingA TheThingB)
> "Thing A has a thing B created by MyTypeA"

更进一步

似乎使用记录数据类型可能会有更优雅的解决方案。我也会尝试提供这样一个。

答案 2 :(得分:0)

好的,这是具有数据类型记录的解决方案,这可能是最灵活的:

data TheThingA = TheThingA TheThingB
data TheThingB = TheThingB

{-|
A data record type is created which holds the functions.
-}
data Stringable = Stringable {
      _toString  :: Stringable -> TheThingA -> String
    , _toStringB :: TheThingB -> String
}

{-|
The default function "toString" is implemented.
-}
toString :: Stringable -> TheThingA -> String
toString a (TheThingA thingB) = "Thing A has " ++ (_toStringB a) thingB

{-|
The default type is now a function which returns a Stringable with a specific
toStringB function.
-}
defaultType =
    Stringable toString toStringB
    where
        toStringB b = "a thing B!"

{-|
the myTypeA is the same as the defaultType but with a different toStringB
function.

Note: here we constructed the type from scratch. But using a library such as
Lens, we could re-use the defaultType and modify the only the records for which
we need another function.
-}
myTypeA =
    Stringable toString toStringB
    where
        toStringB b = "a thing B created by MyTypeA"

我们可以看到运行测试会带来相同的结果:

test1 = toString DefaultType(TheThingA TheThingB)

  

“事情A有事B!”

test2 = toString MyTypeA(TheThingA TheThingB)

  

“Thing A有一个由MyTypeA创建的东西”

评论

这可能比使用类型类的前一个答案稍长一些,但这肯定是一种更灵活的方法,因为它允许您使用Lens等库在代码中的任何时刻修改Stringable的行为。 / p>