【问题标题】:Return one of two types of same typeclass返回相同类型类的两种类型之一
【发布时间】:2013-05-11 17:46:28
【问题描述】:

我有以下类型类

class MyClass c where
  aFunction :: c -> Bool

两个不同数据类型的实例

data MyDataType1 = MyDataType1

instance MyClass MyDataType1 where
  aFunction c = True

data MyDataType2 = MyDataType2

instance MyClass MyDataType2 where
  aFunction c = False

我想编写一个函数,该函数接受类型类 MyClass 的两个参数(可能是相同的数据类型或可能不同并返回其中一个。我正在努力为此制定类型签名,我认为我可能采取了错误的方法。

这是正确的吗?如果不是,我应该改用什么?

chooseOne :: (MyClass a, MyClass b) => a -> b -> ?
chooseOne x y = if (aFunction x) then x else y

【问题讨论】:

  • 您是否尝试过不使用类型签名并查看编译器是否能够推断出一个?提示:为什么你必须将ab 声明为MyClass 的单独实例?函数可以返回两种可能不同的类型吗?

标签: haskell types typeclass


【解决方案1】:

你的返回值可以是任何一种类型,所以编译器会报错,除非你对两者使用相同的类型,给

chooseOne :: (MyClass a, MyClass a) => a -> a -> a

这不是你的意思。

要将两种可能不同的类型合并为一个,您可以使用 Either 数据类型:

data Either a b = Left a | Right b

所以你会有

chooseOne :: (MyClass a, MyClass b) => a -> b -> Either a b
chooseOne x y = if (aFunction x) then Right x else Left y

但我宁愿这样写

chooseOne :: (MyClass a, MyClass b) => a -> b -> Either a b
chooseOne x y | aFunction x = Right x 
              | otherwise   = Left y

【讨论】:

  • “但我宁愿写那个”。我是初学者,您偏爱的原因是什么?我一直试图注意什么时候可以使用第一种形式而不是第二种形式。我应该停止吗?谢谢。
  • 这只是风格,而不是其他任何东西。我喜欢看守们如何清楚地说明情况。对我来说感觉更像 Haskellish,而且我发现它更容易阅读。但是,我觉得这并不重要。使用你喜欢的任何东西。
  • @AdrewC,我并不特别喜欢 if 语句,所以我将停止尝试识别任何一种情况,而只使用警卫。谢谢。
  • 这是有道理的。我总是将 Either 与错误消息相关联,但这更有意义。
【解决方案2】:

你正在编写的函数在 Haskell 中是不可能的——返回类型必须在编译时是固定的并且是已知的。因此,要编写您感兴趣的内容,您需要Either

chooseOne :: (MyClass a, MyClass b) => a -> b -> Either a b
chooseOne x y = if (aFunction x) then Left x else Right y

最终,即使在动态语言中,您也必须有一些代码可以同时处理ab 类型。这“消除”了Either 并体现在函数Data.Either.either

either :: (a -> c) -> (b -> c) -> Either a b -> c
either f _ (Left a)  = f a
either _ g (Right b) = g b

对于您的特定情况,由于ab 都是MyClass 的实例,因此感觉我们可以制作一个更方便的消除函数

eitherOfMyClass :: (MyClass a, MyClass b) => (a -> b) -> Either a a' -> b
eitherOfMyClass f (Left a)   = f a
eitherOfMyClass f (Right a') = f a'

但这实际上不会进行类型检查!如果您仔细查看类型,您可能会发现问题 --- 我们传入的处理函数专用于 a,因此不能应用于 EitherRight 侧,即输入b。因此,我们需要使用forall,这是一个由LANGUAGE RankNTypes 启用的扩展。

{-# LANGUAGE RankNTypes #-}

eitherOfMyClass :: (MyClass a, MyClass b) =>
                   (forall x. MyClass x => (x -> c)) -> Either a b -> c
eitherOfMyClass f (Left a)  = f a
eitherOfMyClass f (Right b) = f b

这确保您传递给eitherOfMyClass 的任何函数fMyClass 的任何实例都是真正通用的,因此可以同时应用于Either 中的ab

【讨论】:

  • 但是如果他有f :: MyClass x => x -> c,那么他写useOne x y = if aFunction x then f x else f y不是更简单吗?然后他就不需要 RankNTypes,对于那些没有意识到他们可以使用 Either 的人来说,这可能有点陡峭的学习曲线。
  • 仅当f 固定时。如果您尝试编写useOne f x y = if aFunction x then f x else f y,那么您要么需要RankNTypes,要么将xy 的类型统一在一起。使用单个函数一般消除 Either 确实需要 RankNTypes...虽然我同意少数用例可能在一天结束时绝对需要。
【解决方案3】:

(另见“Haskell Antipattern: Existential Typeclass”。)

data MyType = MyType { aFunction :: Bool }

chooseOne :: MyType -> MyType -> MyType
chooseOne x y = if aFunction x then x else y

警告:您真正的 MyClass 可能不够简单,无法完成这项工作。

n.b.如果您可以将其编写为 OOP 语言中的类,那么您可以使用这种技术。 OOP 构造函数转换为返回 MyType 值的独立函数。

  • 所有类函数都必须将类值作为参数。所以例如如果你真正的类是Monoidmempty :: Monoid a => a 不带参数所以不适合这个框架。
  • 您的类函数不能采用必须属于同一基础类型的多个参数。再次例如如果你的真实类是Monoidmappend :: Monoid a => a -> a -> a 有两个参数,它们可以是实现Monoid 的任何类型,但它们都必须是实现Monoid相同 类型; mappend 也不适合这个框架。

【讨论】:

    【解决方案4】:

    你总是可以这样做向后:你可以接受两个函数作为输入,而不是返回类型x或类型y,并根据你想要的执行一个或另一个“返回”:

    chooseOne :: (x -> z) -> (y -> z) -> x -> y -> z
    chooseOne f1 f2 x y = if aFunction x then f1 x else f2 y
    

    请注意,如果您使用chooseOne Left Right,您现在拥有其他一些人建议的基于Either 的解决方案。您还可以执行chooseOne show show 之类的操作来返回String 作为结果。

    这种方法是好是坏取决于你为什么要首先构建这个类(即你的程序试图做什么)......

    【讨论】:

    • 这是一个有趣的方法。我在这里使用不同的数据类型,因为数据类型将使用我的库作为客户端编写。
    猜你喜欢
    • 2015-09-17
    • 2021-03-22
    • 2022-01-24
    • 2021-12-15
    • 1970-01-01
    • 2021-08-13
    • 2021-11-19
    • 1970-01-01
    • 2021-04-01
    相关资源
    最近更新 更多