Haskell中的动态调度

时间:2012-10-28 05:44:51

标签: haskell functional-programming dynamic-dispatch

用Java编写的程序很大程度上依赖于动态调度。如何用Haskell等函数式语言表达这些程序?换句话说,Haskell在“动态调度”下表达这个想法的方式是什么?

4 个答案:

答案 0 :(得分:66)

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

更具体地说,OO对象包括:

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

很多时候,对象和虚拟功能的整个大厦感觉就像缺乏对闭包的支持一样精心设计的解决方法。

例如,考虑Java的Comparator接口:

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

并且假设你想用它来根据字符串排序字符串列表&#39;第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));
}

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

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

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 => [a] -> [a],它使用为给定Ord类型定义的a实例,该实例是静态选择的。)

基于类型的动态调度

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语言的做法更熟悉,更接近一对一的映射。但是对于大多数目的而言,它比第一部分中描述的方法更精细,更不灵活。原因是如果关于各种Widgets的唯一重要的事情是它们实现窗口小部件功能的方式,那么在制作类型,这些类型的接口实例,然后再次抽象出底层类型时,没有什么意义。将它们放在一个存在的包装器中:直接传递函数更简单。

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

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

然后使用IsWidgetExtra的任何类型,您还可以无缝地使用IsWidget的方法。基于记录的方法的唯一选择是在记录内记录,其中涉及内部记录的一些手动包装和展开。但是,如果你想明确地模仿OO语言的深层次结构,这只会是有利的,反过来你只有在想让自己的生活变得困难时才会这样做。 (另请注意,Haskell没有任何内置方法可以从IsWidget动态向下转换为IsWidgetExtra。但是有ifcxt

(基于记录的方法的优点怎么样?除了每次想要做一个新事物时都不必定义新类型,记录是简单的价值级事物,值比类型更容易操作例如,您可以编写一个函数,该函数将Widget作为参数并基于它创建一个新的Widget,其中一些不同而其他的保持不变。这有点像子类化来自C ++中的模板参数,只是不那么容易混淆。)

词汇表

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

  • 记录:struct(包含公共数据成员的类,没有别的)。也称为字典。

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

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

编辑29-11-14:虽然我认为这个答案的核心仍然基本上是正确的(Haskell中的HOF对应于OOP中的虚拟方法),但自从我写这篇文章以来,我的价值判断已经变得有些细微差别。特别是,我现在认为Haskell和OOP的方法都不是严格的,更基本的&#34;比另一个。请参阅this reddit comment

答案 1 :(得分:10)

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

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

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

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

如果你真的需要动态动态分派,你可以使用“存在类型”,或者你可以使用Data.Dynamic库和类似的技巧。< / p>

答案 2 :(得分:7)

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

答案 3 :(得分:5)

也许您需要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"