是否可以在Haskell中用可调用对象定义类型?

Is it possible to define type with callable objects in Haskell?

我对Haskell还是很陌生,对它的类型系统还不太满意。我想知道,是否有能力定义类型(数据类型?),哪些实例可以称为函数?

模拟是

1
__call__

Python或类方法

中的方法

1
operator()

在c中。 (维基百科中针对"功能对象"一词提供了更多示例)。

此类构造的应用示例为Polynom。该对象由其系数列表定义,即我希望具有以下类型:

1
2
data Num a => Polynom a = Polynom [a]
                          deriving (...)

现在我当然可以定义函数了

1
2
3
callPoly :: Num a => (Polynom a) -> a -> a
callPoly p x = ... -- implementation: extract coefficients of p,
                   -- construct polynomial and call it on x

(这里我不打扰,能够在Floats上调用具有Int系数的多项式...这只是技术上的细节)

现在我可以在我的政策上(在交互式提示中)调用它:

1
2
let myPoly = Polynomial [1 2 3]
let applicationResult = callPoly myPoly 3

但是这种方式并不是太花哨。希望能够直接调用多项式为

1
let applicationResult = myPoly 3

问题是:可以定义这样的多项式类型,可以调用(用作函数)哪些对象(实例)?可能以其他方式实现此模式,而不涉及"数据"吗?可能有些正在使用函数类型或smth。其他吗?

当然,这个想法不仅可以应用于多项式。实际上,对于任何必须"具有类型"并具有"某些附加数据"的函数(在多项式的情况下,它是系数)。

或者如果这不可能,那么是否有某些特定原因或者只是不支持?

P.S .:在我看来,直接方法(如上所述)是不可能的,因为可调用的myPoly必须为(Int-> Int)类型。但是类型(Int-> Int)不能附加任何数据(例如多项式系数)。但我想确保我是对的。


您最好熟悉C"函数对象"的概念,因为它很好地介绍了Haskell关于您可以使用简单的旧函数执行的操作的想法...具体来说,使用currying,部分应用程序并将函数作为参数传递其他功能。

在C中,您的代码应类似于:

1
2
3
4
5
6
7
8
9
class Polynomial {
  int coefficients[];
public:
  Polynomial(int coefficients[]) { /* ... */ }
  int operator() (int value) { /* ... */ }
};

int coefficients[] = {1, 2, 3};
Polynomial(coefficients)(4); // note the syntax here!

从根本上讲,这是一个单一的函数:多项式求值器,它包含一个系数列表和一个值。用C可以很容易地将其表示为:

1
2
3
4
int evaluatePolynomial(int coefficients[], int value);

int coefficients[] = {1, 2, 3};
evaluatePolynomial(coefficients, 4);

但是此表格不像以前的表格那样被管理。关于咖喱形式的好处是您可以说:

1
2
3
4
Polynomial p = Polynomial(coefficients);
p(4);
p(5);
p(6);

而不是:

1
2
3
evaluatePolynomial(coefficients, 4);
evaluatePolynomial(coefficients, 5);
evaluatePolynomial(coefficients, 6);

好的。因此,我们认为"功能对象"是面向对象的编程概念-一个伪装成函数的对象-现在让我们忘记对象。如果您在Haskell中查看它,它只是一个函数,不需要任何用户定义的数据类型即可很好地表达:

1
polynomial :: Num a => [a] -> a -> a

您可以将其称为"通常"(与上面的evaluatePolynomial()一样),一次将其应用于两个参数:

1
polynomial [1, 2, 3] 4

但是由于Haskell函数是经过咖喱处理的,因此您可以部分应用(与Polynomial函数对象一样):

1
2
3
4
5
do
  let p = polynomial [1, 2, 3]
  print (p 4)
  print (p 5)
  print (p 6)

轻松自在。现在,如果您想做的更接近于C,那里有一个表示您的Polynomial函数对象的特定数据类型,您可以这样做...

1
2
newtype Polynomial a = P (a -> a)  -- function object
mkPolynomial :: Num a => [a] -> Polynomial a  -- constructor

...但是这种额外的复杂性实际上并没有提供任何好处。您立即注意到,Polynomial没什么特别的,它只package了一个常规函数,因此您最终不得不再次将其拆开,例如:

1
2
3
4
5
do
  let (P p) = mkPolynomial [1, 2, 3]
  print (p 4)
  print (p 5)
  print (p 6)

简而言之,您越是单纯地根据功能而不是对象来构想,Haskell代码将变得更简单,更惯用。


是的,在Haskell中,您不能拥有可调用对象,因为这将使类型推断变得很混乱。就像在不支持__call__且需要显式方法名称的OO语言中一样,您将必须为函数指定显式名称。

另一方面,部分函数的应用和闭包使从多项式表示中获得多项式函数变得非常容易,因此限制并不那么糟糕。

1
2
let polyF = callPoly myPoly in
(polyF 17) + (polyF 42)

由于我自己不是Haskell的资深人士,所以我在这里可能会用错术语。但是,根据我对Haskell的了解:

由于Haskell不是面向对象的,因此它没有对象或实例(传统意义上就是)。您拥有数据类型的值,而不是数据类型的实例。话虽这么说,由于函数就像整数和字符串一样是数据(值),因此就可以调用的值而言,它们可以携带自己的上下文(例如OO世界中的实例)。

如果您的目标是传递带有PolyNom a的值,则可以简单地对函数callPoly进行部分评估,然后将其视为" Callable PolyNom"。示例:

1
2
3
4
5
6
myPoly = PolyNom [1, 2, 3]
callMyPoly = callPoly myPoly

-- or simply

callMyPoly = callPoly (PolyNom [1, 2, 3])

现在,callMyPoly的类型为:

1
callMyPoly :: Num a => a -> a

,您可以这样称呼它:

1
callMyPoly 5

等效于:

1
callPoly myPoly 5