7

与下面代码等效的 Haskell 会产生正确的答案吗?

可以修复此 Scala 代码以产生正确的答案吗?如果是,如何?

object TypeErasurePatternMatchQuestion extends App {
  val li=List(1,2,3)
  val ls=List("1","2","3")
  val si=Set(1,2,3)
  val ss=Set("1","2","3")
  def whatIsIt(o:Any)=o match{
    case o:List[Int]    => "List[Int]"
    case o:List[String] => "List[String]"
    case o:Set[Int]     => "Set[Int]"
    case o:Set[String]     => "Set[String]"
  }

  println(whatIsIt(li))
  println(whatIsIt(ls))
  println(whatIsIt(si))
  println(whatIsIt(ss))

}

印刷:

List[Int]
List[Int]
Set[Int]
Set[Int]

但我希望它能够打印:

List[Int]
List[String]
Set[Int]
Set[String]
4

3 回答 3

5

您必须了解,通过说o:Any您删除了有关类型的所有特定信息,并且进一步了解类型Any是编译器所知道的关于 value 的全部信息o。这就是为什么从那时起您只能依赖有关类型的运行时信息。

case-expression likecase o:List[Int]是使用 JVM 的特殊instanceof运行时机制解决的。但是,您遇到的错误行为是由这种机制引起的,它只考虑了第一类类型(Listin List[Int])并忽略了参数(Intin List[Int])。这就是为什么它被视为List[Int]等于List[String]。这个问题被称为“Generics Erasure”。

另一方面,Haskell 执行完整的类型擦除,这在Ben 的回答中得到了很好的解释。

所以这两种语言的问题是一样的:我们需要提供关于类型及其参数的运行时信息。

在 Scala中,您可以使用“反射”库来实现这一点,它隐式地解析该信息:

import reflect.runtime.{universe => ru}
def whatIsIt[T](o : T)(implicit t : ru.TypeTag[T]) = 
  if( t.tpe <:< ru.typeOf[List[Int]] ) 
    "List[Int]"
  else if ( t.tpe <:< ru.typeOf[List[String]] ) 
    "List[String]"
  else if ( t.tpe <:< ru.typeOf[Set[Int]] ) 
    "Set[Int]"
  else if ( t.tpe <:< ru.typeOf[Set[String]] ) 
    "Set[String]"
  else sys.error("Unexpected type")  

println(whatIsIt(List("1","2","3")))
println(whatIsIt(Set("1","2","3")))

输出:

List[String]
Set[String]

Haskell对多态性有一种非常不同的方法。最重要的是,它没有子类型多态性(虽然它不是一个弱点),这就是为什么你的例子中的类型切换模式匹配是无关紧要的。但是,可以将上面的 Scala 解决方案非常紧密地翻译成 Haskell:

{-# LANGUAGE MultiWayIf, ScopedTypeVariables #-}
import Data.Dynamic
import Data.Set

whatIsIt :: Dynamic -> String
whatIsIt a = 
  if | Just (_ :: [Int]) <- fromDynamic a -> "[Int]"
     | Just (_ :: [String]) <- fromDynamic a -> "[String]"
     | Just (_ :: Set Int) <- fromDynamic a -> "Set Int"
     | Just (_ :: Set String) <- fromDynamic a -> "Set String"
     | otherwise -> error "Unexpected type"

main = do
  putStrLn $ whatIsIt $ toDyn ([1, 2, 3] :: [Int])
  putStrLn $ whatIsIt $ toDyn (["1", "2", "3"] :: [String])
  putStrLn $ whatIsIt $ toDyn (Data.Set.fromList ["1", "2", "3"] :: Set String)

输出:

[Int]
[String]
Set String

但是,我必须大胆地指出,这与 Haskell 编程的典型场景相去甚远。该语言的类型系统足够强大,可以解决极其复杂的问题,同时保持所有类型级别的信息(和安全性)。Dynamic仅在低级库中非常特殊的情况下使用。

于 2014-03-13T21:51:34.710 回答
5

GHC 比 JVM 进行更多的类型擦除;在运行时,类型完全消失了(不仅仅是类型参数)。

Haskell 处理类型的方法是在编译时使用它们以保证永远不会执行错误类型的操作,并且由于 Haskell 没有 OO 风格的子类型和动态分派,因此完全没有保留类型的目的. 因此,数据被编译成一个只包含正确值的内存结构,而函数在编译时使用了它们所操作的类型的结构的内置知识1,并且只是盲目地期望它们的参数具有该结构。这就是为什么如果你弄错了,你会得到一些有趣的东西,比如分段unsafeCoerce错误,而不仅仅是运行时异常,说值不是预期的类型;在运行时,Haskell不知道一个值是否属于任何给定的类型。

因此,Haskell 没有为等效程序提供“正确答案”,而是不允许您的程序不安全!Haskell中没有任何Any类型可以转换为您想要的任何类型。

这不是 100% 正确的。在 Haskell 和 Scala 中,都有一些方法可以让类型信息在运行时保持活跃。本质上,它是通过创建表示类型的普通数据结构并将它们一起传递给这些类型的值来完成的,因此在运行时您可以参考类型表示对象以获取有关其他对象类型的信息。两种语言都有库和语言工具,可让您在更高(和更有原则)的级别上使用此机制,从而更容易安全地使用。因为它需要传递类型令牌,所以您必须“选择加入”这些功能,并且您的调用者必须知道它才能向您传递所需的类型令牌(无论令牌的实际生成和传递是隐式或显式完成)。

如果不使用这些特性,Haskell 无法对可能是类型的值进行模式匹配List IntSet String找出它是哪一个。要么您使用的是单态类型,在这种情况下它只能一种类型,而其他类型将被拒绝,或者您使用的是多态类型,在这种情况下,您只能将代码应用于它会做同样的事情2 无论哪种具体类型实例化多态类型。


1除了多态函数,它们假设它们的多态参数,因此除了将它们传递给其他多态函数(具有匹配的类型类约束,如果有的话)之外,基本上不能对它们做任何事情。

2类型类约束的多态类型是唯一的例外。即使这样,如果你有一个值是某个类型类的成员的类型,你所能做的就是将它传递给其他函数,这些函数接受属于该类型类成员的任何类型的值。如果这些函数是在相关类型类之外定义的通用函数,它们将受到相同的限制。只有类型类方法本身才能真正为类中的不同类型“做一些不同的事情”,那是因为它们是一大堆单态的联合对类中的一种特定类型进行操作的定义。您不能编写获取多态值的代码,检查它以查看它是用什么实例化的,然后决定要做什么。

于 2014-03-13T22:29:23.040 回答
3

当然,Haskell 打印出正确的答案:

import Data.Set
import Data.Typeable

main = do
  let li=[1,2,3]
  let ls=["1","2","3"]
  let si=Data.Set.fromList[1,2,3]
  let ss=Data.Set.fromList["1","2","3"]
  print $ typeOf li
  print $ typeOf ls
  print $ typeOf si
  print $ typeOf ss

印刷

[Integer]
[[Char]]
Set Integer
Set [Char]
于 2014-03-13T19:09:03.853 回答