Haskell在编译时捕获哪些类型的错误,Java不能?

时间:2014-08-19 01:22:12

标签: java haskell types

我刚刚开始学习Haskell并继续看到对其强大类型系统的引用。我看到许多实例,其中推理比Javas强大得多,但也暗示它可以在编译时捕获更多错误,因为它具有优越的类型系统。所以,我想知道是否有可能解释Haskell在Java编译时可以捕获的错误类型。

4 个答案:

答案 0 :(得分:26)

说Haskell的类型系统可以捕获比Java更多的错误,这有点误导。让我们解开一点。

静态类型

Java和Haskell都是静态类型的语言。我的意思是他们在编译时已知语言中给定表达式的 type 。这对于Java和Haskell都有许多优点,即它允许编译器检查表达式是否为#sa;",以便对sane进行一些合理的定义。

是的,Java允许某些"混合类型"表达式,如"abc" + 2,有些人可能认为是不安全或不好,但这是一个主观选择。最后,它只是Java语言提供的功能,无论好坏。

不变性

要了解Haskell代码如何被认为比Java(或C,C ++等)代码更不容易出错,您必须考虑类型系统与语言的不变性。在纯(普通)Haskell代码中,没有副作用。也就是说,一旦创建,程序中的任何值都不会改变。当我们计算某些东西时,我们正在从旧结果中创建一个新结果,但我们不会修改旧值。事实证明,从安全角度来看,这有一些真正方便的后果。当我们编写代码时,我们可以确定程序中的任何其他地方都不会影响我们的功能。事实证明,副作用是许多编程错误的原因。一个例子是C中的共享指针,它在一个函数中被释放,然后在另一个函数中被访问,从而导致崩溃。或者是在Java中设置为null的变量,

String foo = "bar";
foo = null;
Char c = foo.charAt(0); # Error!

在普通的Haskell代码中不会发生这种情况,因为foo一旦定义,就无法改变。这意味着它无法设置为null

输入类型系统

现在,您可能想知道类型系统如何在所有这些中发挥作用,这毕竟是您所询问的。好吧,就像不变性一样好,事实证明,你可以做的很少有趣的工作,没有任何变异。从文件中读取?突变。写入磁盘?突变。与网络服务器交谈?突变。那么我们该怎么办?为了解决这个问题,Haskell使用其类型系统将变量封装在一个名为 IO Monad 的类型中。例如,要从文件中读取,可以使用此功能,

readFile :: FilePath -> IO String

IO Monad

请注意,结果的类型不是String ,而是IO String。从外行人的角度来看,这意味着结果将IO(副作用)引入程序。在一个格式良好的程序中,IO只会在IO monad中进行,这样我们就可以非常清楚地看到可能出现副作用的地方。此属性由类型系统强制执行。进一步的IO a类型只能在程序的main函数内产生其副作用的结果。因此,现在我们已经非常巧妙地将危险的副作用隔离到程序的受控部分。当你得到IO String的结果时,任何都可以发生,但至少这不会发生任何地方,只有{{1} }函数,仅作为main类型的结果。

现在要明确,您可以在代码中的任何位置创建IO a值。您甚至可以在IO a函数之外操作它们,但在main函数的主体中要求结果之前,实际上不会执行任何操作。例如,

main

此函数从文件中读取输入,复制该输入并将复制的输入附加到原始输入的末尾。因此,如果文件包含字符strReplicate :: IO String strReplicate = readFile "somefile that doesn't exist" >>= return . concat . replicate 2 ,则会创建内容为abc的{​​{1}}。您可以在代码中的任何位置调用此函数,但Haskell实际上只会在String函数中找到表达式时尝试读取该文件,因为它是abcabc main的实例。像这样,

IO

这几乎肯定会失败,因为您请求的文件可能不存在,但它只会失败。您只需担心副作用,而不是像在许多其他语言中那样担心代码中的任何地方。

一般来说,IO和Monads都比我在这里介绍的要多得多,但这可能超出了你的问题的范围。

类型推断

现在还有一个方面。 类型推断

Haskell使用非常高级Type Inference System,允许您编写静态类型的代码,而无需编写类型注释,例如{ Java中的{1}} GHC可以推断几乎任何表达式的类型,甚至非常复杂的表达式。

这对我们的安全讨论意味着,在程序中使用Monad的任何实例时,类型系统将确保它不能用于产生意外的副作用。您无法将其投射到main :: IO () main = strReplicate >>= putStrLn ,只需将结果输出到您想要的地方/时间。 您必须在String foo函数中明确引入副作用。

易于动态打字的静态打字安全性

Type inference系统还有一些其他不错的属性。人们常常喜欢编写脚本语言,因为他们不必像Java或C那样为类型编写所有样板文件。这是因为脚本语言是动态类型或类型表达式的计算仅在解释器运行表达式时计算。这使得这些语言可能更容易出错,因为在运行代码之前,您不知道自己是否有错误的表达式。例如,您可能会在Python中说出类似的内容。

IO a

问题在于Stringmain 可以是任何。所以这没关系,

def foo(x,y):
  return x + y

但这会导致错误,

x

我们现在有办法检查这是无效的,直到它运行。

了解所有静态类型语言没有此问题非常重要,包括 Java 。在这个意义上,Haskell 比Java更安全。 Haskell和Java都让你安全从这种类型的错误,但在Haskell你不必编写所有类型以确保安全,他们类型系统可以推断类型。通常,在Haskell中为函数的类型添加注释被认为是一种好的做法,即使您不必这样做。但是,在函数体中,您很少需要指定类型(有一些奇怪的边缘情况)。

结论

希望这有助于说明Haskell如何保护您的安全。关于Java,您可能会说在Java中您必须使用类型系统来编写代码,但在Haskell中类型系统适合您

答案 1 :(得分:5)

类型转换

一个区别是Java允许动态类型转换,例如(愚蠢的例子如下):

class A { ... }
static String needsA(A a) { ... }

Object o = new A();
needsA((A) o);

类型转换会导致运行时类型错误,这可能被视为类型不安全的原因。当然,任何优秀的Java程序员都会将强制转换视为最后的手段,并依赖类型系统来确保类型安全。

在Haskell中,(大致)没有子类型,因此没有类型转换。演员阵容的最接近的特征是(不经常使用的)Data.Typeable库,如下所示

foo :: Typeable t => t -> String
foo x = case cast x :: A of          -- here x is of type t
        Just y  -> needsA y          -- here y is of type A
        Nothing -> "x was not an A"

大致对应

String foo(Object x) {
   if (x instanceof A) {
      A y = (A) x;
      return needsA(y);
   } else {
      return "x was not an A";
   }
}

Haskell和Java之间的主要区别在于,在Java中,我们有单独的运行时类型检查(instanceof)和强制转换((A))。如果检查无法确保转换成功,则可能会导致运行时错误。

我记得在引入仿制药之前,演员阵容是Java的一个大问题,例如使用集合迫使你执行大量的演员表。使用泛型,Java类型系统得到了极大的改进,而现在Java中的转换应该不那么常见,因为它们不太常用。

Casts and Generics

回想一下,泛型类型在Java中被运行时擦除,因此代码如

if (x instanceof ArrayList<Integer>) {
  ArrayList<Integer> y = (ArrayList<Integer>) x;
}

不起作用。由于我们无法检查ArrayList的参数,因此无法完全执行检查。同样由于这种擦除,如果我没记错的话,即使x是一个不同的ArrayList<String>,强制转换也可以成功,只会在以后导致运行时类型错误,即使代码中没有出现强制转换。 / p>

Data.Typeable Haskell机制在运行时不会删除类型。

更强大的类型

Haskell GADT和(Coq,Agda,...)依赖类型扩展了传统的静态类型检查,以便在编译时对代码强制执行更强大的属性。

考虑例如zip Haskell函数。这是一个例子:

zip (+) [1,2,3] [10,20,30] = [1+10,2+20,3+30] = [11,22,33]

这在两个列表中以“逐点”方式应用(+)。它的定义是:

-- for the sake of illustration, let's use lists of integers here
zip :: (Int -> Int -> Int) -> [Int] -> [Int] -> [Int]
zip f []     _      = []
zip f _      []     = []
zip f (x:xs) (y:ys) = f x y : zip xs ys

但是,如果我们传递不同长度的列表,会发生什么?

zip (+) [1,2,3] [10,20,30,40,50,60,70] = [1+10,2+20,3+30] = [11,22,33]

较长的一个被静默截断。这可能是一种意外的行为。可以将zip重新定义为:

zip :: (Int -> Int -> Int) -> [Int] -> [Int] -> [Int]
zip f []     []     = []
zip f (x:xs) (y:ys) = f x y : zip xs ys
zip f _      _      = error "zip: uneven lenghts"

但提高运行时错误只是略微好一点。我们需要的是在编译时强制执行列表的长度相同。

data Z       -- zero
data S n     -- successor 
-- the type S (S (S Z)) is used to represent the number 3 at the type level    

-- List n a is a list of a having exactly length n
data List n a where
   Nil :: List Z a
   Cons :: a -> List n a -> List (S n) a

-- The two arguments of zip are required to have the same length n.
-- The result is long n elements as well.
zip' :: (Int -> Int -> Int) -> List n Int -> List n Int -> List n Int
zip' f Nil         Nil         = Nil
zip' f (Cons x xs) (Cons y ys) = Cons (f x y) (zip' f xs ys)

请注意,编译器能够推断出xsys的长度相同,因此递归调用是静态良好类型的。

在Java中,您可以使用相同的技巧对类型中的列表长度进行编码:

class Z {}
class S<N> {}
class List<N,A> { ... }

static <A> List<Z,A> nil() {...}
static <A,N> List<S<N>,A> cons(A x, List<N,A> list) {...}

static <N,A> List<N,A> zip(List<N,A> list1, List<N,A> list2) {
   ...
}

但是,据我所知,zip代码无法访问两个列表的尾部,并将它们作为两个相同类型List<M,A>的变量提供,其中M直观N-1。 直觉上,访问两个尾部会丢失类型信息,因为我们不再知道它们的长度是均匀的。要执行递归调用,需要进行强制转换。

当然,可以不同地重新编写代码并使用更传统的方法,例如在list1上使用迭代器。不可否认,上面我只是试图以直接的方式在Java中转换Haskell函数, 编码Java的错误方法(通过直接转换Java代码来编写Haskell)。尽管如此,我还是用这个愚蠢的例子来说明Haskell GADT如何在没有不安全的强制转换的情况下表达一些需要使用Java进行强制转换的代码。

答案 2 :(得分:4)

关于Haskell的一些事情使它更安全&#34;比Java。类型系统是显而易见的系统之一。

没有类型广播。 Java和类似的OO语言允许您将一种类型的对象转换为另一种类型。如果你无法说服类型系统让你做任何你想做的事情,你总是可以把所有东西都投到Object(虽然大多数程序员会立刻认出这是纯粹的邪恶)。问题是,现在您处于运行时类型检查领域,就像在动态类型语言中一样。哈斯克尔不会让你做这些事情。 (除非你明确地试图获得它;并且几乎没有人这样做。)

可用的泛型。泛型有Java,C#,Eiffel和一些其他OO语言。但是在Haskell中他们实际上工作。在Java和C#中,尝试编写通用代码几乎总会导致模糊的编译器消息有关&#34;哦,你不能这样使用它#34;。在Haskell中,通用代码是 easy 。你可以偶然写下它的工作方式与你期望的一样。

<强>便利即可。你可以在Haskell中做一些在Java中付出太多努力的事情。例如,为原始用户输入和已清理的用户输入设置不同的类型。您可以完全在Java中执行此操作。但是你没赢。它的样板太多了。如果它对您的应用程序绝对至关重要,那么您只会这么做。但在Haskell中,它只是少数几行代码。它简单。人们这样做是为了好玩!

<强>万即可。 [我没有更具技术性的术语。]有时,函数的类型签名可以让您知道函数的作用。我并不是说你可以弄清楚这个函数是做什么的,我的意思是这个类型的函数只有一个可能正在做或者它不会编译。这是一个非常强大的财产。 Haskell程序员有时会说&#34;当它编译时,它通常没有错误&#34;,这可能是直接的结果。

虽然不是类型系统的严格属性,但我可能还会提到:

  • 显式I / O 。函数的类型签名会告诉您它是否执行任何I / O.不执行I / O的函数是线程安全的,非常容易测试。

  • 显式为空。除非类型签名这样说,否则数据不能为空。当您使用数据时,必须显式检查null。如果您忘记&#34;,则类型签名不会匹配。

  • 结果而非例外。 Haskell程序员倾向于编写返回&#34;结果的函数。包含结果数据的对象或解释为什么不能生成结果的对象。而不是抛出异常并希望有人记得抓住它。与可空值一样,结果对象与实际结果数据不同,如果您忘记检查失败,类型系统会提醒您。

说完了所有这些,Java程序通常会死于空指针或数组索引异常; Haskell程序往往会死于臭名昭着的&#34; head []&#34;。

答案 3 :(得分:0)

对于一个非常基本的例子,虽然这在Java中是允许的:

public class HelloWorld {

    public static void main(String[] args) {
        int x = 4;
        String name = "four";

        String test = name + x;
        System.out.println(test);
    }

}

同样的事情会在Haskell中产生编译错误:

fourExample = "four" + 4

Haskell中没有隐式类型转换有助于防止像"four" + 4这样的愚蠢错误。您必须明确告诉它,您要将其转换为String

fourExample = "four" ++ show 4