Haskell的工作方式如下:(由于Haskell不是面向对象的语言,因此与Lippert的陈述并不完全相反)。
警告:一个严肃的Haskell忠实拥护者长期以来的回答。
TL; DR
该示例完全说明了Haskell与C#有何不同。与其将结构构造的后勤工作委托给构造函数,而必须在周围的代码中进行处理。空值(或Nothing
在Haskell中)无法出现在我们期望非空值的地方,因为空值只能出现在称为的特殊包装类型内Maybe
,这些包装类型不能与/不能直接转换为常规非包装材料。可为空的类型。为了使用通过将其包装到中而变为可为空的值Maybe
,我们必须首先使用模式匹配来提取该值,这迫使我们将控制流转移到一个分支中,在该分支中我们肯定知道我们具有非空值。
因此:
我们是否总能知道在任何情况下都不能将非空引用视为无效?
是。 Int
和Maybe Int
是两种完全独立的类型。Nothing
在平原中查找Int
将类似于在字符串中查找字符串“ fish” Int32
。
在引用类型为非空字段的对象的构造函数中该怎么办?
没问题:Haskell中的值构造函数除了将给出的值放到一起再做其他任何事情。所有初始化逻辑都在调用构造函数之前发生。
在这样的对象的终结器中,该对象被终结是因为应该填充引用的代码引发了异常,该怎么办?
Haskell中没有终结器,因此我无法真正解决。但是,我的第一反应仍然有效。
完整答案:
Haskell没有空值,并使用Maybe
数据类型表示可空值。也许是像这样定义的算术数据类型:
data Maybe a = Just a | Nothing
对于那些不熟悉Haskell的人,请将其读为“ A Maybe
是a Nothing
或a Just a
”。特别:
Maybe
是类型构造函数:可以(错误地)认为它是泛型类(a
类型变量在哪里)。C#比喻为class Maybe<a>{}
。
Just
是一个值构造函数:它是一个函数,它接受一个type类型的参数a
并返回一个Maybe a
包含该值的type 值。因此,代码x = Just 17
类似于int? x = 17;
。
Nothing
是另一个值构造函数,但不带任何参数,Maybe
返回的值除“ Nothing”外没有其他值。x = Nothing
类似于int? x = null;
(假设我们将a
Haskell中的约束为Int
,可以通过编写完成x = Nothing :: Maybe Int
)。
既然该Maybe
类型的基础知识已不复存在,Haskell如何避免在OP的问题中讨论的问题?
好吧,Haskell 与到目前为止讨论的大多数语言确实有很大不同,因此,我将首先解释一些基本的语言原理。
首先,在Haskell中,一切都是不可变的。一切。名称是指值,而不是指可以存储值的内存位置(仅此就是消除错误的巨大来源)。不同于C#,其中变量声明和分配是两个独立的操作,在Haskell值通过定义其值被创建(例如x = 15
,y = "quux"
,z = Nothing
),它可以永远不会改变。因此,代码如下:
ReferenceType x;
在Haskell中是不可能的。初始化值没有问题,null
因为必须将所有内容显式初始化为值才能存在。
其次,Haskell不是一种面向对象的语言:它是一种纯粹的功能性语言,因此从严格意义上讲没有对象。取而代之的是,有些简单的函数(值构造函数)会接受其参数并返回一个合并的结构。
接下来,绝对没有命令式样式代码。通过这个,我的意思是大多数语言都遵循这样的模式:
do thing 1
add thing 2 to thing 3
do thing 4
if thing 5:
do thing 6
return thing 7
程序行为表示为一系列指令。在面向对象的语言中,类和函数的声明在程序流中也起着巨大的作用,但实际上,程序执行的“实质”采取了一系列要执行的指令的形式。
在Haskell中,这是不可能的。相反,程序流完全由链接功能决定。甚至命令式的do
注释也只是将匿名函数传递给>>=
运算符的语法糖。所有功能均采用以下形式:
<optional explicit type signature>
functionName arg1 arg2 ... argn = body-expression
body-expression
任何可以求值的值都可以在哪里。显然有更多的语法功能可用,但要点是完全没有语句序列。
最后,而且可能最重要的是,Haskell的类型系统非常严格。 如果我不得不总结Haskell类型系统的中心设计理念,我会说:“在编译时要使尽可能多的事情出错,而在运行时要尽可能少地出错。” 没有任何隐式转换(想将an Int
提升为Double
?Use fromIntegral
函数)。唯一可能在运行时发生无效值的方法是使用Prelude.undefined
(显然,该值必须存在并且不可能删除)。
考虑到所有这些,让我们看一下amon的“残破”示例,并尝试在Haskell中重新表达此代码。首先,数据声明(对命名字段使用记录语法):
data NotSoBroken = NotSoBroken {foo :: Foo, bar :: Bar }
(foo
并且bar
实际上是匿名字段而不是实际字段的访问器函数,但是我们可以忽略此详细信息)。
该NotSoBroken
值构造是不能采取任何操作,而只是采取Foo
和Bar
(这是不可为空),并制作NotSoBroken
了出来。没有放置命令性代码或什至手动分配字段的地方。所有初始化逻辑都必须在其他位置进行,最有可能在专用工厂功能中进行。
在该示例中,Broken
始终构造失败。没有办法以NotSoBroken
类似的方式破坏值构造函数(根本没有地方可以编写代码),但是我们可以创建同样具有缺陷的工厂函数。
makeNotSoBroken :: Foo -> Bar -> Maybe NotSoBroken
makeNotSoBroken foo bar = Nothing
(第一行是类型签名声明:makeNotSoBroken
将a Foo
和a Bar
作为参数并产生a Maybe NotSoBroken
)。
返回类型必须为Maybe NotSoBroken
,而不仅仅是NotSoBroken
因为我们告诉它要计算为Nothing
,这是的值构造函数Maybe
。如果我们写任何不同的东西,这些类型就根本不会排列在一起。
除了绝对没有意义之外,此功能甚至无法实现其实际目的,我们在尝试使用它时会看到。让我们创建一个函数useNotSoBroken
,该函数期望a NotSoBroken
作为参数:
useNotSoBroken :: NotSoBroken -> Whatever
(useNotSoBroken
接受a NotSoBroken
作为参数并产生a Whatever
)。
并像这样使用它:
useNotSoBroken (makeNotSoBroken)
在大多数语言中,这种行为可能会导致空指针异常。在Haskell中,类型不匹配:makeNotSoBroken
返回a Maybe NotSoBroken
,但是useNotSoBroken
期望a NotSoBroken
。这些类型不可互换,并且代码无法编译。
为了解决这个问题,我们可以使用一条case
语句根据Maybe
值的结构进行分支(使用称为模式匹配的功能):
case makeNotSoBroken of
Nothing -> --handle situation here
(Just x) -> useNotSoBroken x
显然,此代码段需要放置在某些上下文中才能进行实际编译,但是它演示了Haskell如何处理可为空值的基础知识。这是上述代码的分步说明:
- 首先,
makeNotSoBroken
对进行评估,以确保产生type值Maybe NotSoBroken
。
- 该
case
语句检查此值的结构。
- 如果值为
Nothing
,则评估“此处的处理情况”代码。
- 如果该值与某个
Just
值匹配,则执行另一个分支。请注意,matching子句如何同时将值标识为Just
构造并将其内部NotSoBroken
字段绑定到名称(在本例中为x
)。x
然后可以像正常值那样使用NotSoBroken
。
因此,模式匹配为实施类型安全性提供了强大的工具,因为对象的结构与控制的分支密不可分。
我希望这是一个容易理解的解释。如果这没有任何意义,请跳入“ 学习Haskell,以求大成!”!,这是我读过的最好的在线语言教程之一。希望您会看到与我一样的这种语言。