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