我开始学习Haskell。我对它很陌生,我只是在阅读一些在线书籍,以了解其基本结构。

熟悉它的人经常会想到的“模因”之一谈论的是整个“如果它可以编译,它将可以运行*”的东西-我认为这与类型系统的强度有关。在这方面,它比其他静态类型的语言要更复杂。令人发指的是,您的ArrayList<String>()包含ArrayList<Animal>()等,如果有人输入string-您的编译器将无济于事。 ,如果我确定我的程序不是真的与动物有关,而是与车辆有关,那么我可以更改一个产生elephant, giraffe的函数来产生Mercedes,并且我的IDE应该告诉我哪里有编译中断。

我的假设是,这就是人们所说的强类型系统的含义,但是对我而言,为什么Haskell的更好才是我所不知道的。换句话说,您可以编写好的Java或坏的Java,我假设您可以在Haskell中执行相同的操作(即将东西填充到字符串/整数中,这些字符串/整数实际上应该是一流的数据类型)。

我怀疑我缺少重要/基本的东西。
很高兴看到我的方式的错误!

评论

我会让人们比写真实的答案更有知识,但是要点是:像C#这样的静态类型的语言都有一个试图帮助您编写可辩驳的代码的类型系统。诸如Haskell尝试帮助您编写正确(即可证明)代码的类型的系统。工作的基本原则是将可以检查的内容移到编译阶段。 Haskell在编译时检查更多内容。

我对Haskell不太了解,但是我可以谈论Java。尽管它看起来是强类型的,但是它仍然允许您按照您所说的做“令人发指的事情”。对于Java对其类型系统所做的几乎所有保证,都有一种解决方法。

我不知道为什么所有答案都只会在最后提到。如果我只选择一件事,那就是更多流行的语言应该从Haskell借用。这是一个非常简单的想法(从理论上讲不是很有趣),但是仅此一项就可以使我们的工作变得更加轻松。
这里会有很好的答案,但是为了帮助您研究类型签名。它们允许人类和程序以某种方式来推理程序,这些方式可以说明Java在糊状的中间re:类型中的表现。

为了公平起见,我必须指出,“如果编译的话,它就会起作用”。是的,我们Haskell程序员知道,对于某些有限的正确性概念,通过类型检查器会带来很大的正确性机会,但这肯定不是字面意义上普遍的“真实”声明!

#1 楼

这是Haskell中可用的类型系统功能的无序列表,在Java中则不可用或不太好(据我所知,这在Java的基础上是较弱的)。 Haskell的类型具有相当好的“类型安全”属性。这是非常具体的,但是从本质上讲,它意味着某种类型的值不能随意转换为另一种类型。有时这与可变性不符(请参阅OCaml的值限制)

代数数据类型。 Haskell中的类型与高中数学基本上具有相同的结构。结果非常简单和一致,但事实证明,它可能像您想要的那样强大。它只是类型系统的重要基础。




数据类型通用编程。这与泛型类型不同(请参见泛化)。取而代之的是,由于如上所述的类型结构的简单性,编写在该结构上进行一般操作的代码相对容易。稍后,我讨论Haskell编译器如何自动为用户定义的类型派生类似Equal的内容。本质上,它的实现方式是遍历任何用户定义类型基础的通用简单结构,并在值之间进行匹配-这是结构相等性的非常自然的形式。 />相互递归的类型。这只是编写非平凡类型的必要组成部分。




嵌套类型。这使您可以定义在不同类型上递归的变量的递归类型。例如,data Bt a = Here a | There (Bt (a, a))是一种平衡树。仔细考虑Bt a的有效值,并注意该类型的工作方式。这很棘手!



概括。在类型系统中没有它几乎太愚蠢了(哼,看着你,走)。重要的是要有类型变量的概念,并且要有能力谈论与该变量的选择无关的代码。 Hindley Milner是从System F派生的类型系统。Haskell的类型系统是HM类型的详细说明,而System F本质上是泛化的基础。我的意思是说Haskell有一个很好的概括性故事。

抽象类型。 Haskell在这里的故事不是很好,但也不是不存在的。可以编写具有公共接口但具有私有实现的类型。这使我们都可以在以后接受对实现代码的更改,并且重要的是,由于它是Haskell中所有操作的基础,因此请编写具有明确接口(例如IO)的“魔术”类型。说实话,Java实际上可能有一个更好的抽象类型故事,但是我不认为直到接口变得更流行才真正成为现实。

参数化。 Haskell值没有任何通用运算。 Java在引用相等和哈希等方面违反了这一原则,在强制方面则更为公然。这意味着您可以获得关于类型的自由定理,这些定理使您可以完全从其类型完全了解操作或值的含义-某些类型只能有很少的居民。

编码棘手的事物时,类型较高的类型会显示所有类型。 Functor / Applicative / Monad,可折叠/ Traversable,整个mtl效果输入系统,广义functor定点。清单一直在继续。有很多东西最好用更高种类的东西来表达,而相对很少的类型系统甚至允许用户谈论这些东西。

类型类。如果将类型系统视为逻辑(这很有用),那么通常会要求您证明事物。在许多情况下,这实际上是线路噪声:可能只有一个正确的答案,这对于程序员来说是浪费时间和精力。类型类是Haskell为您生成证明的一种方式。更具体地讲,这使您可以解决简单的“类型方程系统”,例如“我们打算将(+)放在一起是哪种类型?哦,Integer,好吧!现在内联正确的代码!”。在更复杂的系统上,您可能正在建立更多有趣的约束。



约束演算。 Haskell中的约束(进入类型类序言系统的机制)在结构上被类型化。这给出了非常简单的子类型关系形式,使您可以从较简单的约束中组合出复杂的约束。整个mtl库都基于此思想。

派生。为了提高类型类系统的规范性,有必要编写许多通常很简单的代码来描述用户定义类型必须实例化的约束。对于Haskell类型的非常普通的结构,通常可以要求编译器为您完成此样板。

Type类的序言。 Haskell类型类求解器(一种生成我之前提到的“证明”的系统)本质上是Prolog的一种残缺形式,具有更好的语义特性。这意味着您可以在prolog类型中编码真正繁琐的事情,并希望它们在编译时全部处理。一个很好的例子可能是解决一个证明,即如果忘记顺序,两个异构列表是等效的,它们就是等效的异构“集合”。多参数类型类和功能依赖项。这些只是对基本typeclass序言的大量有用的改进。如果您了解Prolog,则可以想象当您编写多个变量的谓词时,表达能力会增加多少。基于Hindley Milner类型系统的语言具有很好的推论。 HM本身具有完整的推断,这意味着您无需编写类型变量。 Haskell 98是Haskell的最简单形式,已经在一些非常罕见的情况下将其排除。通常,现代的Haskell一直在尝试逐步减少完整推理的空间,同时为HM增加更多功能并查看用户何时抱怨。人们很少抱怨-Haskell的推论非常好。

非常非常非常弱的子类型。我在前面提到过,类型类序言中的约束系统具有结构子类型的概念。那是Haskell中唯一的子类型形式。对于推理和推论,子类型化很糟糕。这使得每个问题都变得更加困难(一个不平等的系统而不是一个平等的系统)。这也很容易造成误解(子类化与子类型化一样吗? />

请注意,最近(2017年初),史蒂文·多兰(Steven Dolan)发表了关于MLsub的论文,这是ML和Hindley-Milner类型推论的一种变体,它有一个很好的子类型故事(另请参见)。这并没有消除我在上面写的内容-大多数子类型系统都已损坏并且具有错误的推理-但确实表明我们直到今天才可能发现一些有希望的方法来使完整的推理和子类型完美地结合在一起。现在,非常清楚的是,Java的子类型化概念绝不能利用Dolan的算法和系统。它需要重新考虑子类型的含义。


高等级类型。我之前谈到过泛化,但不仅仅是泛化,谈论其中具有泛化变量的类型非常有用。例如,对那些结构“包含”的东西不了解(请参阅参数性)的高阶结构之间的映射具有类似(forall a. f a -> g a)的类型。在直式HM中,您可以使用这种类型编写函数,但是对于高级类型,则需要此类函数作为参数,例如:mapFree :: (forall a . f a -> g a) -> Free f -> Free g。注意,a变量仅在参数内绑定。这意味着函数mapFree的定义者可以决定使用a时实例化的对象,而不是mapFree的用户。存在类型。较高级别的类型使我们可以讨论通用量化,而存在性类型使我们可以讨论存在性量化:仅存在一些满足某些方程式的未知类型的想法。这最终很有用,并且需要花费更长的时间。

类型家族。有时候类型类机制很不方便,因为我们并不总是在Prolog中考虑。类型族让我们写出类型之间的直接函数关系。



封闭型家庭。默认情况下,类型族是开放的,这很烦人,因为这意味着尽管可以随时扩展它们,但您无法“反转”它们并获得成功的希望。这是因为您无法证明内射性,但可以使用封闭类型族。




建立索引类型和类型提升。在这一点上,我真的变得很异国情调,但是这些有时会被实际使用。如果您想编写一种打开或关闭的句柄,那么您可以做得很好。请注意,在以下代码段中,State是一个非常简单的代数类型,其值也被提升为类型级别。然后,接下来,我们可以讨论诸如Handle之类的类型构造函数,以诸如State之类的特定类型作为参数。理解所有细节令人困惑,但也非常正确。 Java因具有类型擦除功能而臭名昭著,并且在某些人的游行中有雨点。类型擦除是正确的方法,但是,就好像您具有函数getRepr :: a -> TypeRepr一样,您至少会违反参数性。更糟糕的是,如果这是用户生成的函数,该函数用于在运行时触发不安全的强制措施,那么您将面临巨大的安全隐患。 Haskell的Typeable系统允许创建安全的coerce :: (Typeable a, Typeable b) => a -> Maybe b。该系统依赖于在编译器中实现的Typeable(而不是在userland上实现),并且如果没有Haskell的类型机制和保证遵循的法律也无法获得如此好的语义。 Haskell类型系统的价值还与类型如何描述语言有关。这是Haskell的一些功能,它们通过类型系统来驱动价值。



纯度。 Haskell对于“副作用”的非常非常非常宽泛的定义不允许出现任何副作用。这迫使您将更多信息放入类型中,因为类型控制着输入和输出,并且没有副作用,必须在输入和输出中考虑所有内容。




IO。随后,Haskell需要一种讨论副作用的方法-因为任何实际程序都必须包含副作用-所以类型类,更高种类的类型和抽象类型的组合产生了使用称为IO a的特定的超特殊类型的概念。表示产生a类型值的副作用计算。这是嵌入纯语言内部的非常好的效果系统的基础。




null不足。众所周知,null是现代编程语言的十亿美元错误。代数类型,特别是通过将类型A转换为类型Maybe A,仅将“不存在”状态附加到您所拥有的类型上的能力,完全缓解了null的问题。多态递归。这使您可以定义归纳类型变量的归纳函数,尽管在每个归集调用中以自己的归纳在不同类型处使用它们。这很难谈论,但是对于谈论嵌套类型特别有用。从前面回顾一下Bt a类型,并尝试编写一个函数来计算其大小:size :: Bt a -> Int。看起来有点像size (Here a) = 1size (There bt) = 2 * size bt。从操作上讲并不太复杂,但是请注意,最后一个方程式中对size的递归调用是在另一种类型上发生的,但是总体定义具有很好的广义类型size :: Bt a -> Int。请注意,这是一个破坏总推理的功能,但是如果您提供类型签名,那么Haskell会允许它。一些。

评论


空值不是十亿美元的“错误”。在某些情况下,在可能存在任何有意义的意义之前,不可能静态地验证指针是否将不会被取消引用。在这种情况下具有尝试解除引用陷阱的方法通常比要求指针最初标识无意义的对象要好。我认为最大的与null相关的错误是在给定char * p = NULL;的情况下,将捕获在* p = 1234上,但不会捕获到char * q = p + 5678; * q = 1234;

–超级猫
2015年4月17日在20:19



托尼·霍尔(Tony Hoare)直接引述了这一点:en.wikipedia.org/wiki/Tony_Hoare#Apologies_and_retractions。虽然我确定有时指针算术中需要使用null,但我将其解释为说指针算术是承载您语言语义的一个不好的地方,而不是null仍然不是错误。

– J. Abrahamson
15年4月17日在22:27

@supercat,您确实可以编写不带null的语言。是否允许是一个选择。

– Paul Draper
2015年4月18日在2:14

@supercat-该问题在Haskell中也存在,但形式不同。 Haskell通常是惰性的并且不可变的,因此只要不对p求值,就可以编写p = undefined。更有用的是,只要您不对其进行评估,就可以将undefined放入某种可变引用中。更为严重的挑战是可能无法终止的惰性计算,这当然是无法确定的。主要区别在于,这些都是明确的编程错误,并且从不用于表示普通逻辑。

–克里斯蒂安·康克尔(Christian Conkle)
2015年8月7日14:58



@supercat Haskell完全缺少引用语义(这是引用透明性的概念,即通过用引用替换其引用来保留所有内容)。因此,我认为你的问题是不恰当的。

– J. Abrahamson
2015年8月7日15:47

#2 楼


全类型推断。实际上,您实际上可以无所不在地使用复杂类型,而无需像“老兄,我所做的就是编写类型签名。”
类型是完全代数的,这使得表达某些复杂思想非常容易。具有类型类,它们类似于接口,只是您不必将一个类型的所有实现都放在同一位置。您可以为现有的第三方类型创建自己的类型类的实现,而无需访问其源代码。
高阶和递归函数倾向于将更多功能引入类型检查器的范围。以过滤器为例。在命令式语言中,您可能会编写一个for循环以实现相同的功能,但由于for循环没有返回类型的概念,因此您将没有相同的静态类型保证。
子类型的缺乏大大简化了参数型多态

评论


好的答案-您能给我一个简单的高类型类型的例子,以为这将帮助我理解为什么在Java中无法做到这一点。

–phatmanace
15年4月16日在20:16

这里有一些很好的例子。

–卡尔·比勒费尔特(Karl Bielefeldt)
15年4月16日在20:27

模式匹配也非常重要,这意味着您可以使用对象的类型来轻松做出决策。

–本杰明·格伦鲍姆(Benjamin Gruenbaum)
15年4月16日在21:09

@BenjaminGruenbaum我认为我不会将其称为类型系统功能。

–Doval
15年4月16日在21:14

虽然ADT和HKT无疑是答案的一部分,但我怀疑有人问这个问题会知道为什么它们有用,但我建议需要同时扩展这两个部分来解释这一点

– jk。
2015年4月17日下午6:38

#3 楼

a :: Integer
b :: Maybe Integer
c :: IO Integer
d :: Either String Integer
在Haskell中:一个整数,一个可能为null的整数,一个其值来自外界的整数以及一个可能是字符串的整数,都是不同的类型-编译器将强制执行此操作。您不能编译不遵守这些区别的Haskell程序。

(但是,可以省略类型声明。在大多数情况下,编译器可以为变量确定最通用的类​​型,这将导致在成功的编译中。不是很整洁吗?)

评论


在此答案未完成时+1,我认为最好在问题级别上讲

– jk。
15年4月17日在8:19

+1虽然可以帮助解释其他语言也可能具有(例如Java的Optional和Scala的Option),但是在这些语言中,这是一个半生半熟的解决方案,因为您始终可以将null分配给该类型的变量,并让您的程序在运行时爆炸。 Haskell [1]不会发生这种情况,因为没有空值,因此您根本无法作弊。 ([1]:实际上,当您没有Nothing时,可以使用诸如fromJust之类的部分函数来生成与NullPointerException类似的错误,但这些函数可能会被皱眉)。

– Andres F.
2015年4月17日在13:25



“一个其值来自外部世界的整数”-IO Integer会不会更接近“子程序,该子程序在执行时会给出整数”?由于a)在main = c >> c中,第一个c返回的值可能与第二个c返回的值不同,而a不管其位置如何(只要在单个作用域内),其值都相同b)有以下类型表示来自外部的值,以强制执行其规范化(即,不直接放置它们,而是首先检查用户输入是否正确/不是恶意的)。

– Maciej Piechotka
2015年4月17日在16:17

Maciej,那会更准确。我为简单而努力。

– WolfeFan
15年4月17日在16:20

#4 楼

许多人列出了有关Haskell的好东西。但是在回答您的特定问题“为什么类型系统使程序更正确?”时,我怀疑答案是“参数多态性”。

考虑以下Haskell函数:

foobar :: x -> y -> y


实际上只有一种可能的方法来实现此功能。仅仅通过类型签名,我就可以准确地知道此函数的作用,因为它只能做一件事。 [好吧,不完全是,但是差不多!]

停下来,想一想。这实际上是一件大事!这意味着,如果我使用此签名编写一个函数,则该函数实际上除了我想要的目的之外不可能做任何事情。 (当然,类型签名本身仍然可能是错误的。没有编程语言会阻止所有的错误。)

考虑以下功能:

/>此功能是不可能的。您实际上无法实现此功能。我可以从类型签名中看出来。

您可以看到,Haskell类型签名可以告诉您很多东西!


与C#相比。 (对不起,我的Java有点生锈了。)

fubar :: Int -> (x -> y) -> y


该方法可以做很多事情:


返回in2
永远循环,从不返回任何内容。
抛出异常,从不返回任何内容。

实际上,Haskell也具有这三个选项。但是C#还为您提供了其他选项:


返回null。 (Haskell没有null。)
在返回in2之前对其进行修改。 (Haskell没有就地修改。)
使用反射。 (Haskell没有反射。)
执行多个I / O操作,然后返回结果。 (除非在此处声明要执行I / O,否则Haskell不允许您执行I / O。)

反思是一个特别大的锤子。使用反射,我可以凭空构建一个新的TY对象,然后将其返回!我可以检查两个对象,并根据发现的内容执行不同的操作。我可以对传入的两个对象进行任意修改。

I / O的作用类似。该代码实际上可以向用户显示消息,或者打开数据库连接,或者重新格式化硬盘,等等。



Haskell foobar函数只能获取一些数据并返回该数据,保持不变。它无法“查看”数据,因为其类型在编译时未知。它无法创建新数据,因为……好吧,您如何构造任何可能类型的数据?您需要对此进行反思。它不能执行任何I / O,因为类型签名没有声明正在执行I / O。因此它无法与文件系统或网络交互,甚至无法在同一程序中运行线程! (也就是说,它是100%保证线程安全的。)

如您所见,Haskell通过不允许您做大量的工作,可以为您的代码提供非常强有力的保证确实有。实际上,如此紧密,以至于(对于真正的多态代码而言)通常只有一种可能的方法可以将片段组合在一起。不会告诉您太多信息。Int -> Int几乎可以是任何东西。但是即使那样,我们仍然知道相同的输入将始终以100%的确定性产生相同的输出。Java甚至不能保证!)

评论


+1好答案!这是非常强大的功能,并且经常被Haskell的新手忽略。顺便说一句,一个更简单的“不可能”功能是fubar :: a-> b,不是吗? (是的,我知道不安全的强制性。我想我们并不是在谈论名称中带有“不安全”的东西,新来的人也不用担心!:D)

– Andres F.
2015年4月17日在13:38



有很多您不能编写的简单类型签名,是的。例如,foobar :: x几乎无法实现...

–数学兰花
2015年4月17日14:52

实际上,您不能使纯代码成为线程不安全的,但仍可以使它成为多线程的。您的选择是“在评估之前,先评估”,“在评估时,您可能还希望在单独的线程中评估”,以及“在评估时,您可能也希望在另外一个线程中评估在单独的线程中”。默认值为“随心所欲”,这实际上意味着“尽早评估”。

– John Dvorak
2015年4月19日14:52



更一般地说,您可以在in1或in2上调用具有副作用的实例方法。或者,您可以修改全局状态(这是理所当然的,在Haskell中建模为IO操作,但可能不是大多数人认为的IO)。

–道格·麦克林
15年8月8日,0:21

@isomorphismes类型x-> y-> y是完全可实现的。类型(x-> y)-> y不是。类型x-> y-> y接受两个输入,并返回第二个输入。类型(x-> y)-> y具有对x进行运算的函数,因此必须以某种方式使y ...

–数学兰花
16年1月20日在14:05

#5 楼

一个相关的SO问题。


我想你可以在haskell中做同样的事情(即将东西放入真正应该是一流数据类型的字符串/ int中)


不,您真的不能-至少不能以Java的相同方式。在Java中,这种情况会发生:

String x = (String)someNonString;


,Java会很乐意尝试将非String转换为String。 Haskell不允许这种事情,从而消除了整个运行时错误类。

null是类型系统的一部分(与Nothing一样),因此需要明确地进行请求和处理,从而消除了其他所有类别的运行时错误。

其他微妙的好处-特别是在重用和类型类方面-我没有足够的专业知识来进行交流。 。您仅需遵循几个规则就可以完成很多工作。考虑一直存在的Haskell树:

data Tree a = Leaf a | Branch (Tree a) (Tree a) 


您已经用相当容易理解的一行代码定义了整个通用二叉树(和两个数据构造函数)。所有这些都仅使用一些规则(具有求和类型和乘积类型)。在Java中,这是3-4个代码文件和类。

特别是在那些容易敬畏类型系统的人中,这种简洁/优雅非常受重视。

评论


我从您的答案中仅了解NullPointerExceptions。您能否提供更多示例?

– Jesvin Jose
15年4月17日在7:07

不一定正确,JLS§5.5.1:如果T是类类型,则| S | <:| T |或| T | <:| S |。否则,将发生编译时错误。因此,编译器将不允许您转换不可转换的类型-显然有很多解决方法。

–蜘蛛鲍里斯(Boris)
15年4月17日在7:38

我认为,利用类型类优势的最简单方法是,它们就像可以事后添加的接口一样,并且它们不会“忘记”实现它们的类型。也就是说,您可以确保函数的两个参数具有相同的类型,这与两个List 可能具有不同实现的接口不同。从技术上讲,您可以通过在每个接口中添加类型参数来在Java中执行非常相似的操作,但是99%的现有接口不执行此操作,并且会使您的同行困惑。

–Doval
15年4月17日在11:49

@BoristheSpider为true,但是强制转换异常几乎总是涉及从超类向下转换为子类或从接口向下转换为类,并且超类成为Object并不罕见。

–Doval
15年4月17日在13:26

我认为关于字符串的问题的重点与强制转换和运行时类型错误无关,而是事实是,如果您不想使用类型,Java不会使您-实际上,将数据存储在序列化中形式,以任意形式滥用字符串。 Haskell也不会阻止您这样做,因为...嗯,它有字符串。 Haskell可以为您提供工具,但如果您坚持要求Greenspunning足够多的解释器以在嵌套上下文中重新创建null,则它不能强行阻止您执行愚蠢的事情。没有语言可以。

–卢申科
2015年4月18日在1:24



#6 楼


熟悉它的人们经常谈论的“模因”之一就是整个“如果它可以编译,它将起作用*”的东西-我认为这与类型系统的强度有关。 />

对于小型程序,这通常是正确的。 Haskell可以防止您犯其他语言中容易犯的错误(例如,比较Int32Word32并引起爆炸),但这并不能阻止您犯所有错误。更轻松。如果您的程序以前是正确的并且可以进行类型检查,则很有可能在经过较小的修改后它仍然是正确的。在这方面的语言。


Haskell中的类型非常轻量,因为声明新类型很容易。这与Rust等类似的语言形成对比,Rust的所有内容都比较繁琐。


我的假设是,这就是人们所说的强类型系统的含义,但这并不明显对我来说,为什么Haskell的更好呢?它还具有通用的量化类型(例如id :: a -> a)。您还可以创建包含函数的记录类型,该记录类型与Java或Rust这样的语言完全不同。编写类型之间通用的函数。这非常方便,并且比Java中的Java更流畅。

另一个区别是Haskell倾向于具有相对较好的类型错误(至少在撰写本文时)。 Haskell的类型推断非常复杂,而且很少需要提供类型注释才能进行编译。这与Rust形成对比,Rust的类型推断有时可能需要注释,即使编译器原则上可以推断出类型。

最后,Haskell具有类型类,其中包括著名的monad。 Monad恰好是一种处理错误的好方法。它们基本上为您提供了null的几乎所有便利,而无需进行严格的调试,也不会放弃任何类型安全性。因此,在鼓励我们使用这些类型的函数时,实际上对它们起作用的能力就相当重要了!您可以在Haskell中进行同样的操作


这也许是正确的,但它缺少关键点:在Haskell中开始用脚射击自己的点比该点更远当您开始用Java射击自己的脚时。