【问题标题】:Dynamic dispatch in HaskellHaskell 中的动态调度
【发布时间】:2012-10-17 21:14:51
【问题描述】:

用 Java 编写的程序在很大程度上依赖于动态调度。

这些程序如何用 Haskell 等函数式语言表达?

换句话说,Haskell 在“动态调度”下表达思想的方式是什么?

【问题讨论】:

  • (也许简单的解决方法是 s/动态调度/虚拟方法/g?)
  • 动态分派是 Java 解决动态类型问题的方法。它在编译时不知道数据是什么,因此它用更多数据标记所有数据,说明它是什么类型,然后查找在运行时应用哪种方法。这很慢。在静态类型语言中没有必要,因为编译器在编译时知道什么类型的东西,并且可以编译正确的代码或切换到正确的函数。 (有一些方法可以让事情变得更加动态 - 请参阅答案。)Java 接口成为类型类,Java 泛型是从参数多态中复制而来的。

标签: haskell functional-programming dynamic-dispatch


【解决方案1】:

也许您需要 ADT 和模式匹配?

data Animal = Dog {dogName :: String}
            | Cat {catName :: String}
            | Unicorn

say :: Animal -> String
say (Dog {dogName = name}) = "Woof Woof, my name is " ++ name
say (Cat {catName = name}) = "Meow meow, my name is " ++ name
say Unicorn = "Unicorns do not talk"

【讨论】:

    【解决方案2】:

    Ad-hoc polymorphism 是通过typeclasses 完成的。更多类似OOP的DD用existential types模拟。

    【讨论】:

    • 我想补充一下这个正确答案,GADT 可以提供与存在类型相同的特征,但没有笨拙的语法。此外,值得学习功能性的做事方式,而不是尝试在 FP 中重新创建 OOP,因此绝对是类型类,而且,函数是一流数据这一事实意味着您可以获得很多寻求的灵活性,而不必将其绑定到某些数据。
    【解决方案3】:

    令人惊讶的是,您实际上并不需要动态调度,只需要多态。

    例如,如果您要编写一个对列表中的所有数据进行排序的函数,您希望它是多态的。 (即,您确实希望必须手动为每种类型重新实现此函数。那会很糟糕。)但您实际上并不需要任何东西动态 ;您在编译时就知道要排序的列表或列表中的实际内容。因此,在这种情况下,您实际上根本不需要 run-time 类型查找。

    在 Haskell 中,如果你只是想移动东西而不需要知道或关心它是什么类型,你可以使用所谓的“参数多态”,大致类似于 Java 泛型或 C++ 模板.如果您需要能够将函数应用于数据(例如,对需要排序比较的数据进行排序),您可以将函数作为参数传递给它。或者,Haskell 有一个有点像 Java 接口的东西,你可以说“这个排序函数接受实现这个接口的任何类型的数据”。

    到目前为止,根本没有 动态 调度,只有静态调度。另请注意,由于您可以将函数作为参数传递,因此您可以手动“调度”。

    如果你真的需要实际的动态调度,你可以使用“存在类型”,或者你可以使用Data.Dynamic库,和类似的技巧。

    【讨论】:

    • 5 年后,这个答案和其他答案都很棒 - 我特别喜欢你如何解决很少需要存在类型的问题。
    【解决方案4】:

    答案看似简单:高阶函数。在 OO 语言中具有虚方法的对象只不过是功能与一些本地状态的美化记录。在 Haskell 中,您可以直接使用函数的记录,并将本地状态存储在它们的闭包中。

    更具体地说,一个面向对象的对象包括:

    • 指向 vtable(虚拟方法表)的指针 (vptr),其中包含对象类的虚拟方法的实现。换句话说,一堆函数指针;功能记录。值得注意的是,每个函数都有一个隐藏参数,即对象本身,它是隐式传递的。
    • 对象的数据成员(本地状态)

    很多时候,对象和虚函数的整个大厦感觉就像是一个精心设计的解决方法,因为缺少对闭包的支持。

    例如考虑 Java 的 Comparator 接口:

    public interface Comparator<T> {
        int compare(T o1, T o2); // virtual (per default)
    }
    

    假设您想使用它根据字符串的第 N 个字符对字符串列表进行排序(假设它们足够长)。您将定义一个类:

    public class MyComparator implements Comparator<String> {
        private final int _n;
    
        MyComparator(int n) {
            _n = n;
        }
    
        int compare(String s1, String s2) {
            return s1.charAt(_n) - s2.charAt(_n);
        }
    }
    

    然后你使用它:

    Collections.sort(myList, new MyComparator(5));      
    

    在 Haskell 中你会这样做:

    sortBy :: (a -> a -> Ordering) -> [a] -> [a]
    
    myComparator :: Int -> (String -> String -> Ordering)
    myComparator n = \s1 s2 -> (s1 !! n) `compare` (s2 !! n)
    -- n is implicitly stored in the closure of the function we return
    
    foo = sortBy (myComparator 5) myList
    

    如果你不熟悉 Haskell,下面是它在一种伪 Java 中的大致样子:(我只打算这样做一次)

    public void <T> sortBy(List<T> list, Ordering FUNCTION(T, T) comparator) { ... }
    
    public (Ordering FUNCTION(String, String)) myComparator(int n) {
        return FUNCTION(String s1, String s2) {
            return s1[n].compare(s2[n]);
        }
    }
    
    public void foo() {
        sortBy(myList, myComparator(5));
    }
    

    请注意,我们没有定义任何类型。我们使用的只是函数。在这两种情况下,我们传递给排序函数的“有效负载”都是一个接受两个元素并给出它们相对顺序的函数。在一种情况下,这是通过定义实现接口的类型、以适当的方式实现其虚函数并传递该类型的对象来实现的;在另一种情况下,我们只是直接传递了一个函数。在这两种情况下,我们都在传递给排序函数的事物中存储了一个内部整数。在一种情况下,这是通过向我们的类型添加一个私有数据成员来完成的,在另一种情况下,只需在我们的函数中引用它,使其保留在函数的闭包中。

    考虑更详细的带有事件处理程序的小部件示例:

    public class Widget {
        public void onMouseClick(int x, int y) { }
        public void onKeyPress(Key key) { }
        public void paint() { }
        ...
    }
    
    public class MyWidget extends Widget {
        private Foo _foo;
        private Bar _bar;
        MyWidget(...) {
            _foo = something;
            _bar = something; 
        }
        public void onMouseClick(int x, int y) {
            ...do stuff with _foo and _bar...
        }
    }
    

    在 Haskell 中你可以这样做:

    data Widget = Widget {
        onMouseClick :: Int -> Int -> IO (),
        onKeyPress   :: Key -> IO (),
        paint        :: IO (),
        ...
    }
    
    constructMyWidget :: ... -> IO Widget
    constructMyWidget = do
        foo <- newIORef someFoo
        bar <- newIORef someBar
        return $ Widget {
            onMouseClick = \x y -> do
                ... do stuff with foo and bar ...,
            onKeyPress = \key -> do ...,
            paint = do ...
        }
    

    再次注意,在最初的Widget 之后,我们没有定义任何类型。我们只写了一个函数来构造函数的记录并将事物存储在它们的闭包中。在大多数情况下,这也是在 OO 语言中定义子类的唯一原因。与我们之前示例的唯一区别是,不是一个函数,而是多个函数,在 Java 案例中,通过简单地将多个函数放入接口(及其实现)中进行编码,而在 Haskell 中,通过传递函数记录而不是单一功能。 (在前面的示例中,我们可以传递包含单个函数的记录,但我们不喜欢这样。)

    (值得注意的是,很多时候您不需要动态调度。如果您只想根据类型的默认顺序对列表进行排序,那么您只需使用sort :: Ord a =&gt; [a] -&gt; [a],它使用为给定的a 类型定义的Ord 实例,该类型是静态选择的。)

    基于类型的动态调度

    Java 方法和上面的 Haskell 方法之间的一个区别是,在 Java 方法中,对象的行为(除了它的本地状态)是由它的类型决定的(或者更不仁慈的是,每个实现都需要一个新类型) .在 Haskell 中,我们以我们喜欢的方式记录函数。大多数情况下,这是一个纯粹的胜利(获得了灵活性,没有损失任何东西),但假设出于某种原因我们希望它采用 Java 方式。在这种情况下,正如其他答案所提到的那样,要走的路是类型类和存在。

    继续我们的Widget 示例,假设我们希望Widget 的实现遵循其类型(每个实现都需要一个新类型)。我们定义一个类型类来将类型映射到它的实现:

    -- the same record as before, we just gave it a different name
    data WidgetImpl = WidgetImpl {
        onMouseClick :: Int -> Int -> IO (),
        onKeyPress   :: Key -> IO (),
        paint        :: IO (),
        ...
    }
    
    class IsWidget a where
        widgetImpl :: a -> WidgetImpl
    
    data Widget = forall a. IsWidget a => Widget a
    
    sendClick :: Int -> Int -> Widget -> IO ()
    sendClick x y (Widget a) = onMouseClick (widgetImpl a) x y
    
    data MyWidget = MyWidget {
        foo :: IORef Foo,
        bar :: IORef Bar
    }
    
    constructMyWidget :: ... -> IO MyWidget
    constructMyWidget = do
        foo_ <- newIORef someFoo
        bar_ <- newIORef someBar
        return $ MyWidget {
            foo = foo_,
            bar = bar_
        }
    
    instance IsWidget MyWidget where
        widgetImpl myWidget = WidgetImpl {
                onMouseClick = \x y -> do
                    ... do stuff with (foo myWidget) and (bar myWidget) ...,
                onKeyPress = \key -> do ...,
                paint = do ...
            }
    

    我们有一个类只是为了获取函数的记录,这有点尴尬,然后我们必须单独取出函数。我这样做只是为了说明类型类的不同方面:它们也只是函数的美化记录(我们在下面使用)以及一些魔法,编译器根据推断的类型插入适当的记录(我们在上面使用,并在下面继续使用)。让我们简化一下:

    class IsWidget a where
        onMouseClick :: Int -> Int -> a -> IO ()
        onKeyPress   :: Key -> a -> IO ()
        paint        :: a -> IO ()
        ...
    
    instance IsWidget MyWidget where
        onMouseClick x y myWidget = ... do stuff with (foo myWidget) and (bar myWidget) ...
        onKeyPress key myWidget = ...
        paint myWidget = ...
    
    sendClick :: Int -> Int -> Widget -> IO ()
    sendClick x y (Widget a) = onMouseClick x y a
    
    -- the rest is unchanged from above
    

    这种风格经常被来自 OO 语言的人采用,因为它更熟悉,更接近于 OO 语言的一对一映射。但对于大多数目的来说,它只是比第一节中描述的方法更复杂,更不灵活。原因是,如果关于各种 Widget 的唯一重要之处是它们实现小部件功能的方式,那么为这些类型创建类型、接口实例,然后通过将它们放入一个存在的包装器:直接传递函数更简单。

    可以想到的一个优点是,虽然 Haskell 没有子类型化,但它确实具有“子类化”(可能更好地称为子接口或子约束)。例如你可以这样做:

    class IsWidget a => IsWidgetExtra a where
        ...additional methods to implement...
    

    然后对于您拥有IsWidgetExtra 的任何类型,您还可以无缝地使用IsWidget 的方法。基于记录的方法的唯一替代方案是记录内记录,这涉及到内部记录的一些手动包装和展开。但这只有在您想显式地模拟 OO 语言的深层类层次结构时才有优势,而这反过来又只有在您想让自己的生活变得困难时才会这样做。 (另请注意,Haskell 没有任何内置方法可以从 IsWidget 动态向下转换为 IsWidgetExtra。但有 ifcxt

    (那么基于记录的方法的优点呢?除了不必每次你想做一个新的东西时都定义一个新的类型,记录是简单的值级别的东西,值比类型更容易操作. 例如,您可以编写一个函数,将Widget 作为参数,并基于它创建一个新的Widget,其中一些不同而另一些保持相同。这有点像从模板参数继承在 C++ 中,不那么令人困惑。)

    词汇表

    • 高阶函数:将其他函数作为参数(或将它们作为结果返回)的函数

    • 记录:结构(具有公共数据成员的类,仅此而已)。也称为字典。

    • 闭包:函数式语言(和许多其他语言)允许您定义本地函数(函数内的函数,lambdas),这些函数引用定义站点范围内的事物(例如,外部函数的参数)您通常不希望被保留,而是在函数的“闭包”中。或者,如果你有一个像plus 这样的函数,它接受两个整数并返回一个整数,你可以将它应用于一个参数,比如5,结果将是一个接受一个整数并返回一个整数的函数,通过向其添加 5 - 在这种情况下,5 也存储在结果函数的闭包中。 (在其他情况下,“闭包”有时也用于表示“带有闭包的函数”。)

    • 类型类:与 OO 语言中的类相同。有点像一个界面,但也非常不同。 See here.

    编辑 29-11-14:虽然我认为这个答案的核心本质上仍然是正确的(Haskell 中的 HOF 对应于 OOP 中的虚拟方法),但自从我编写它以来,我的价值判断已经变得有些细微差别。特别是,我现在认为 Haskell 和 OOP 的方法都不是严格意义上的“更基础”。见this reddit comment

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-01-27
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-09-20
      相关资源
      最近更新 更多