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

Haskell:在不同的模块中添加重叠实例中使用的新数据类型,haskell,types,Haskell,Types,我在哈斯克尔遇到了一个问题 背景 我希望能够将数据类型的“东西”转换为字符串。增加的复杂性是,根据所使用的“类型”(也是数据类型),有时结果字符串可能会有所不同。此外,我希望用户能够在自己的模块中自由添加自己的“东西”和“类型”,而无需修改自己的代码。最后但并非最不重要的一点是,“things”可以嵌套,因此一个类型为“A”的“thing”可以包含一个类型为“B”的“thing” 代码 希望通过一点代码和我到目前为止所做的工作,它会更清晰: 正在使用以下GHC扩展: {-# LANGUAGE F

我在哈斯克尔遇到了一个问题

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

代码 希望通过一点代码和我到目前为止所做的工作,它会更清晰:

正在使用以下GHC扩展:

{-# LANGUAGE FlexibleInstances, OverlappingInstances #-}
好的,首先用一个数据类型定义“类型”,该数据类型有两个构造函数:

  • 默认行为(“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”开始:

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

请注意,“thingb”是“thinga”的一部分。您将在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”类:

第一个实例是一个通用实例(因此使用“FlexibleInstances”GHC扩展),它的作用与使用“MyTypeA”的“toDefaultString”完全相同:

第二种情况如下:

instance MyTypeAString TheThingB where
    toMyTypeAString thing = "a thing B created by MyTypeA"
第二个实例是“MyTypeAsString”的“thingB”的具体实现。由于此实例与第一个实例重叠,因此我们使用“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”,该函数的行为都是相同的。您现在必须想象,我们不仅有“thinga”和“thingb”,还有一百种其他数据类型,每种数据类型都有自己的“DefaultString”类的专用实例。 另一方面,假设类“MyTypeAString”对90%的数据类型的行为完全相同,因此只有很少的特定实例。使用“OverlappingInstances”,我们可以节省数百行代码,并且只有在“MyTypeAString”需要不同行为的情况下才有特定的实例,这非常简洁

问题 到目前为止,一切顺利。但是现在,我想创建一个新的“类型”,我想称之为“MyTypeB” 这并不复杂-我可以只修改数据类型“Types”和“toString”函数类型签名-但它不是很干净,因为我需要更改“Types”数据类型本身和同一模块中的“toString”函数。我想要实现的是让用户在自己的模块中定义自己的类型和相关类,而不修改“toString”函数或“types”数据类型。 然而,到目前为止,我还没有找到一种方法来实现这一点,所以问题是如何做到这一点


非常感谢您的帮助:-)

我找到了问题的答案

其思想是首先将“类型”作为类型类而不是数据类型,一个实例为“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"
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”,作为“Types”的新实例。我们可以为这个新类型定义自定义行为,方法是创建一个新类,比如“MyTypeBString”,就像我们为“MyTypeAString”所做的那样。所有这些都可以在一个单独的模块中完成,这为我们提供了很大的灵活性


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

我用一个更好的解决方案再次回答了我自己的问题,该解决方案不需要任何扩展,而且更短

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"
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"
进一步
似乎还有更优雅的解决方案使用记录数据类型。我也会尝试提供这样的解决方案。

好的,下面是一个包含数据类型记录的解决方案,它可能是最灵活的:

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(thingA和thingB)

“东西A有东西B!”

test2=将MyTypeA(thingA和thingB)串入

“对象A有一个由MyTypeA创建的对象B”

评论
这可能比前面使用类型类的答案要长一点,但这肯定是一种更灵活的方法,因为它允许您在代码中随时使用库(如Lens)修改Stringable的行为。

Holy long question蝙蝠侠!添加TLDR或缩短问题可能有助于获得答案:)欢迎来到StackOverflow删除了大约100行,但仍然很长时间。。。感谢您的欢迎:-)
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"
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"