Haskell TypeClass 的调度是动态的吗?

Is the dispatch of a Haskell TypeClass dynamic?

给定以下 Haskell 代码快照:

class Foo a where
   bar  :: a -> ...
   quux :: a -> ...
   ...

其中 a 的值在运行时确定 - class 调度此值。

我假设编译器可以在编译时静态检查类型,并确保没有无效类型可以分派给它。

现在,如果我们将其与 Java 中的动态调度进行比较:

interface Flippable {
  Flippable flip();
}

class Left implements Flippable {
  Right flip();
}

class Right implements Flippable {
  Left flip();
}

class Demo {
  public static void main(String args[]) {
    Flippable flippable = new Right();
    System.out.println(flippable.flip);
  }
}

假设:

我的问题是:Haskell TypeClass 的调度是动态的吗?

这取决于你所说的 "dynamic" 调度是什么意思。 haskell 中没有子类型,因此您的 Java 示例很难翻译。

如果你有类型 class,比如 Show 并且想要将不同的元素放入列表中,你可以使用存在量化:

{-# LANGUAGE ExistentialQuantification #-}

data Showable = forall a. Show a => Showable a

instance Show Showable where
  show (Showable x) = show x

test :: main ()
test = let list = [Showable True, Showable "string"]
       in print list

-- >>> test
-- [True,"string"]

在这里你可以说调度发生了"dynamically"。它的发生方式与 C++ 或 Java 中的方式相同。 Show 字典带有一个对象,如 C++ 中的 vtable(或 Java 中的 class definition ptr,不知道它是如何调用的)。

然而,正如 @MathematicalOrchid 解释的那样,这是一种反模式。


然而,如果您想从 Left 翻转到 Right 并返回,您可以在类型 class 定义中说明。

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FunctionalDependencies #-}

class Flippable a b | a -> b where
  flip' :: Flippable b a => a -> b

newtype INL = INL Int deriving Show
newtype INR = INR Int deriving Show

instance Flippable INL INR where
  flip' (INL x) = INR x

instance Flippable INR INL where
  flip' (INR x) = INL x

-- >>> flip' $ INL 1
-- INR 1
-- >>> flip' $ flip' $ INL 1
-- INL 1

在这种情况下 flip' 调用已在编译时解析。


您也可以 class Flip a where flip' :: a -> Flippable 使用存在量化。然后将动态调度连续调用。一如既往,这取决于您的需求。

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE StandaloneDeriving #-}

class Flip a where
  flip' :: a -> Flippable

-- Show is only for repl purposes
data Flippable = forall a. (Flip a, Show a) => Flippable a

deriving instance Show Flippable

instance Flip Flippable where
  flip' (Flippable x) = flip' x

newtype INL = INL Int deriving Show
newtype INR = INR Int deriving Show

instance Flip INL where
  flip' (INL x) = Flippable (INR x)

instance Flip INR where
  flip' (INR x) = Flippable (INL x)

-- >>> flip' $ flip' $ flip' $ INL 1
-- Flippable (INR 1)

希望这能回答您的问题。

如果您的代码是 Haskell 2010,没有打开任何语言扩展,Haskell 实际上根本不支持 运行 时间多态性

这真是令人惊讶。 Haskell 感觉是一种非常多态的语言。但实际上,所有类型原则上都可以纯粹在编译时决定。 (GHC 选择不这样做,但这是一个实现细节。)

这和C++模板完全一样。当你写类似 std::vector<int> 的东西时,编译器在编译时知道所有涉及的时间。真正令人惊讶的是,您实际上 需要 真正的 运行 时间多态性!

现在,在某些情况下,您希望根据 运行 时间情况 运行 不同的代码。但是在 Haskell 中,你可以通过传递一个函数作为参数来做到这一点;您不需要创建 "class"(在 OOP 意义上)只是为了实现此目的。

现在,如果你打开一些语言扩展(最明显的是 ExistentialQuantification),那么你就会得到真实的,运行-time 多态性。

请注意,人们这样做的主要原因似乎是您可以这样做

class Foo f where
  bar :: f -> Int
  baz :: f -> Bool

data AnyFoo = forall f. Foo => AnyFoo f

my_list :: [AnyFoo]
...

这被广泛认为是 Haskell 反模式。特别是,如果您向上转换 Java 中的内容以将其放入列表中,您稍后会再次 向下转换 。但是上面的代码没有提供任何沮丧的可能性。您也不能使用 运行 时间反射(因为 Haskell 也没有)。所以真的,如果你有一个 AnyFoo 的列表,你可以用它做的 只有 的事情就是调用 foobar 。那么...为什么不只存储 foobar 的结果?

data AnyFoo = AnyFoo {foo :: Int, bar :: Bool}

它可以让您做完全相同的事情,但不需要任何非标准扩展。事实上,在某些方面,它实际上更灵活一些。你现在甚至不需要一个Fooclass,你不需要需要为您可能拥有的每一种 Foo,只是一个为其构建 AnyFoo 数据结构的函数。