haskell 为什么我不能在一个有多态返回类型的函数中返回一个具体类型?

lzfw57am  于 2023-03-03  发布在  其他
关注(0)|答案(3)|浏览(201)

例如,为什么不能定义一个函数-

fun::a
fun = 1

-- OR

someInt::Int
someInt = 3

fun::Num a => a
fun = someInt

这在Java中似乎是可能的-

class App {
    public static void main(String[] args) {
        System.out.println(new SomeClass().hello().sayHi());
    }
}

class SomeClass {
    Hi hello() {
        A a = new A();
        return a;
    }
}

interface Hi {
    String sayHi();
}

class A implements Hi {
    public String sayHi() {
        return "Hi from A";
    }
}

class B implements Hi {
    public String sayHi() {
        return "Hi from B";
    }
}

但 haskell 的等价物不起作用

main = print $ sayHi hello

hello:: Hi a => a
hello = A

class Hi a where
  sayHi::a -> String
  
data A = A
data B = B

instance Hi A where
   sayHi _ = "Hi from A"
   
instance Hi B where
   sayHi _ = "Hi from B"

我试图理解Java中的接口和Haskell中的类型类之间的根本区别?每种方法的限制和好处是什么?

txu3uszq

txu3uszq1#

具有以下功能:

hello :: Hi a => a
hello = A

你向函数的用户承诺,user 可以选择他们想要的 * 任何 * a,只要它是Hi a的一个示例用途:

hello :: A
hello :: B

但这并不是您的实现所显示的:它总是返回一个A对象。在这个意义上,面向对象编程(Java)和函数式编程(Haskell)中的类型约束在某种程度上是彼此的“反向”。在Java中,你承诺它将是Hi的子类,在Haskell中,你承诺它可以返回Hi类型类的任何类型的值。
因此,hello简单地表示为:

hello :: A
hello = A

如果您在一个函数中使用hello,该函数承诺它可以处理任何Hi a => a项,例如:

sayHi :: Hi a => a -> String

因此,它将使aA统一。

eyh26e7m

eyh26e7m2#

这是一个很常见的误解。
多态在Haskell中的工作方式与OO语言完全不同(尽管许多OO语言都有模板/泛型特性,这与Haskell的多态更相似)。
在Java中,多态性基于 * 子类型化 *:通过定义一个接口I,你定义了一个(大的,抽象的)类型,一个示例化这个接口的类定义了I的一个子类型,所以,一个Java接口和一个Java类并没有本质上的不同--事实上,在C++中接口和类使用相同的class关键字;Java只是对如何使用每一种语言进行了一些合理的限制。
这意味着多态函数实际上不需要做普通函数以外的任何事情:它们只有一个引用类型I的签名,类型I包含任何子类型,所以如果I被认为是结果类型,实现者可以自由返回任何子类型。
在Haskell中,没有子类型(尽管有方法模拟它们),类型类根本不定义类型(而是 * 类型集 *;这个区别很重要,原因和区分整数和整数列表很重要一样,所以它们不能像类型一样出现在签名中,Haskell多态函数的工作方式确实与单态函数完全不同,签名实际上非常清楚地表达了这一点:

fun :: Num a => a

表示fun首先接受一个(隐式、类型级)参数Num a,然后生成一个a类型的值。
......事实上,还有更多的事情正在发生:上述签名的完整格式为

{-# LANGUAGE ExplicitForall, UnicodeSyntax #-}

fun :: ∀ a . Num a => a

基本上它需要两个参数

  • 它首先采用类型a
  • 然后,它获取字典Num a,该字典解释了如何将a用作数字类型
  • 然后它会产生一个a类型的值,也就是说,* 确切的类型 * a,而不是某个子类型或其他类型。

当调用fun时,你不需要显式传递这两个参数中的任何一个,编译器可以为你做这件事,但是它需要明确它应该是什么类型,你可以显式传递类型参数,这就是type applications syntax的作用:

{-# LANGUAGE TypeApplications #-}

fun :: ∀ a . Num a => a
fun = 37 + 9

main :: IO ()
main = do
   print (fun @Int)  -- prints 48
   print (fun @Float)  -- prints 48.0

...不要与(fun :: Int)混淆,尽管在本例中它具有完全相同的效果:这里,编译器从上下文推断a必须是Int类型,因为这是预期的结果,而fun @Int显式指示编译器使用Int,并且不允许它适应环境。
这种实现多态性的方法给了调用者很大的能力,但是当然,另一方面是被调用者需要通过支持调用者可能请求的所有类型来支持所有的通用性。
这通常不是一个大问题,只是不要试图把Haskell代码塞进你在Java这样的语言中的思维模式中。Java中的类或接口通常最好对应于一个简单的data结构,或者实际上只是一个已经存在的类型的值,而不是任何类型类。
对于您的示例,为什么不直接用途:

data Hi = A | B

sayHi :: Hi -> String
sayHi A = "Hi from A"
sayHi B = "Hi from B"

我在这里所做的基本上只是将AB声明为类似于Hi类型的子类型--不是首先抽象地定义Hi,然后事后用子类型填充它,而是预先声明HiAB组成。
或者,如果你想保持Hiopen,也就是说,想让下游的人以不可预见的方式定制它...那么,你得到什么类型的字符串也是完全不可预见的,那么拥有类型级抽象的意义何在呢?你可以直接把字符串作为参数,这会使事情变得更简单:

sayHi :: String -> String
sayHi x = "Hi from "++x

[2]这里要准确地说:在Haskell中,*Rank-0类型 * 没有子类型。Rank-0类型也可以被描述为具体类型,而更高级别的类型中有quantor。更高级别的类型确实有子类型(基本上,通过在不同的类上量化)。但这有点回避问题,因为Rank〉0类型已经内置了多态性。

hlswsv35

hlswsv353#

我想我会将您的Java代码翻译如下:

main = putStrLn (sayHi (hello SomeClass))

data SomeClass = SomeClass

hello :: SomeClass -> SomeHi
hello SomeClass = SomeHi A

class Hi a where
  sayHi :: a -> String

data SomeHi = forall a. Hi a => SomeHi a

instance Hi SomeHi where
  sayHi (SomeHi x) = sayHi x

data A = A

instance Hi A where
  sayHi A = "Hi from A"

data B = B

instance Hi B where
  sayHi B = "Hi from B"

但这不是惯用的代码,您可能不需要经常编写这样的代码。

相关问题