这是我在Haskell中遇到的一个问题。
我希望能够将数据类型的“东西”转换为字符串。增加的复杂性是,有时结果字符串可能会有所不同,具体取决于所使用的“类型”(也是数据类型)。此外,我希望用户能够在自己的模块中自由添加自己的“东西”和“类型”,而无需修改自己的代码。最后但并非最不重要的是,“事物”可以嵌套,因此“A”类型的“事物”可以包含“B”类型的“事物”。
希望通过一些代码以及我到目前为止所做的工作更加清晰:
正在使用以下GHC扩展程序:
{-# LANGUAGE FlexibleInstances, OverlappingInstances #-}
好的,首先使用带有两个构造函数的数据类型定义“类型”:
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”数据类型。 但是,到目前为止,我找不到实现这一目标的方法,所以问题是如何做到这一点?
非常感谢您的帮助: - )
答案 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>