如何在Haskell中建模多态列表?

13

我正在尝试在Haskell中建模一些多态类型的数据。我知道以下代码为什么不能工作,但我希望这能说明我想做什么。我的问题是:用Haskell建模这个有什么惯用方式?(如果有更好的方法,您不需要保留输入格式- 我没有任何现有的代码或数据。)

data Running = Sprint | Jog deriving (Show)
data Lifting = Barbell | Dumbbell deriving (Show)

data Time   = Time   Integer deriving (Show)
data Pounds = Pounds Integer deriving (Show)

data TimedActivity    = TimedActivity Running Time deriving (Show)
data WeightedActivity = WeightedActivity Lifting Pounds deriving (Show)

class Activity a

instance Activity TimedActivity
instance Activity WeightedActivity

-- I have a list of activities
main :: IO ()
main = putStrLn $ show [ TimedActivity Sprint (Time 10)
                       , WeightedActivity Barbell (Pounds 100)
                       ]

-- I then want to apply functions to generate summaries and
-- reports from those activities, i.e.:
extractLifts :: (Activity x) => [x] -> [WeightedActivity]
extractTimes :: (Activity x) => [x] -> [TimedActivity]

它不起作用的第一个原因是你在class Activity a和两个instance Activity行的末尾缺少了一些where。但我正在努力提供一个真正的答案。 - bheklilr
4个回答


8

针对您的具体示例,您可以使用 Either 将两种类型统一在同一个列表中:

both :: [Either TimedActivity WeightedActivity]
both = [ Left $ TimedActivity Sprint (Time 10)
       , Right $ WeightedActivity Barbell (Pounds 100)
       ]

extractLifts :: [Either TimedActivity WeightedActivity] -> [WeightedActivity]
extractLifts = rights

extractTimes :: [Either TimedActivity WeightedActivity] -> [TimedActivity]
extractTimes = lefts

如果超过两种类型,只需定义自己的抽象数据类型来统一它们:

data Multiple = Case1 Type1 | Case2 Type2 | ...

您可以使用以下示例代码来使用Python中的提取函数:

extractCase1 :: [Multiple] -> [Type1]
extractCase1 ms = [t1 | Case1 t1 <- ms]

这个解决方案不具有可扩展性。您的“Activity API”应该事先知道所有“上升类型”。实际上,它不是多态列表(而是具有类型的交叉积的唯一类型)。(我在等待您的书!;) - josejuan
我相当确定这正是我想要的,因为我只关心少量的先验类型。周末会试一下以确保。 - Xavier Shay
@josejuan 我正在解决问题的文本,而不是标题。 :) 完成博士学位后,我将开始着手写作我的书籍。 - Gabriella Gonzalez

3
短答案是,如果你想要多态列表,请使用Python。
长答案是,Haskell有意设计成不支持多态列表。虽然有方法可以实现完全多态的“列表”,但是这些方法难以使用且效率更低。因为它们不属于[a]类型,所以您无法在上面使用普通的列表方法。如果您想要组合两种数据类型,则Either类型非常方便,并且具有许多内置函数,但只要您增加的类型越多,您的类型签名就会变得越不方便。一个好的经验法则是:如果您试图构建多态列表,则您正在做错误的事情。Haskell有很好的方法来封装带有代数数据类型的类型。
这个特定代码的问题在于,虽然WeightedActivity和TimedActivity都是Activity的实例,但是列表仍然必须由单个Activity实例组成。类型为Activity a => [a]的列表并不表示您可以混合不同类型的活动,而是表示列表的所有成员,即所有Activity都是相同类型的Activity。您不能同时拥有Int和Double的列表,因为它们是不同的类型,即使它们都有Num实例。
相反,您可以将TimedActivity、WeightedActivity和Activity组合成一个单一的数据类型,如下所示:
data Activity
    = TimedActivity Running Time
    | WeightedActivity Lifting Pounds
    deriving (Show)

如果你有新的活动要添加,你可以将它们简单地添加到Activity数据类型中。然后使用模式匹配编写extract函数会非常容易。


9
简短回答是,如果你想要多态列表,请使用Python... 你认真的吗 ;) - Ankur
1
@Ankur 完全认真。我在 Haskell 中从未有过真正需要多态列表的需求。此外,这有什么意义呢?当然,你可以使用存在类型来做到这一点,但你真正所做的是制作一个包装其他类型的类型,它并不是真正的多态。只有极少数情况_真正_需要它们,因此如果你试图用它们来解决问题,我认为这是代码异味。 - bheklilr
1
@bheklilr:像所有事物一样,存在类型也有它们的用处。如果你将它们用作面向对象风格的异构集合的替代品,那可能不是理想的选择。但例如,在工作中,我们有一个需要集中缓存对各种(异构)数据源的请求结果的库——对于这个问题,存在类型恰好是正确的选择。 - Jon Purdy
3
@bheklilr:使用动态语言几乎与Python方法相同。除此之外,当有人试图学习Haskell或任何新知识时,请不要以“你应该/可以在 xyz 中这样做”开始回答。 - Ankur
1
就定义而言,我认为你在应该使用“异构(heterogeneous)”时却使用了“多态(polymorphic)”。在Haskell中,标准列表完全是多态的,例如它可以是[Int][Char] - firefrorefiddle
显示剩余2条评论

0

extractLifts :: (Activity x) => [x] -> [WeightedActivity]

这行代码表示你正在尝试构建一个函数,该函数可以匹配列表中单个元素的类型(可能会过滤其他类型)。我认为你不能这样做而不进行hacky。

(我想,那一行应该是extractLifts :: [(Activity x) => x] -> [WeightedActivity] - 微妙的区别在于你的版本“选择类型x”一次用于整个列表,因此列表中的所有元素都是相同类型的,同一个Activity实例,而第二个版本将为每个元素单独选择类型)


1
这不仅需要语言扩展,而且也无法工作。 - firefrorefiddle
我并不是说它一定会起作用,我只是指出签名的含义可能更加通用。 - Sassa NF

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接