【问题标题】:Why is Clojure dynamically typed?为什么 Clojure 是动态类型的?
【发布时间】:2011-07-03 21:01:37
【问题描述】:

我非常喜欢的一件事是阅读不同的编程语言。目前,我正在学习 Scala,但这并不意味着我对 Groovy、Clojure、Python 和其他许多东西不感兴趣。所有这些语言都具有独特的外观和感觉以及一些特征。在 Clojure 的情况下,我不理解这些设计决策之一。据我所知,Clojure 非常强调它的函数范式,并且几乎迫使您尽可能使用不可变的“变量”。那么,如果一半的值是不可变的,为什么语言是动态类型的?

Clojure 网站说:

首先,Clojure 是动态的。这意味着 Clojure 程序不仅仅是您编译和运行的东西,而是您可以与之交互的东西。

嗯,这听起来很奇怪。如果一个程序被编译了,你就不能再改变它了。当然,您可以与它“交互”,这就是 UI 的用途,但该网站当然并不意味着整洁的“动态”GUI。

Clojure 如何从动态类型中受益

我指的是 Clojure 的特殊情况,而不是动态类型的一般优势。

动态类型系统如何帮助改进函数式编程

再次,我知道不泄漏“int a;”的乐趣到处都是源代码,但类型推断可以减轻很多痛苦。因此,我只想知道动态类型如何支持函数式语言的概念。

【问题讨论】:

  • 请记住,Clojure 是一个 Lisp,而且 Lisp 从那时起就一直是动态类型的,只有极少数例外(例如 Typed Racket) - 无论如何都没有引起太多关注。
  • 变量的不变性和变量的动态类型化是两个非常不同的概念...不确定我是否明白您的第一段.
  • 我使用 Python 和 Javascript 以及 Pascal、Java、C++ 和 C# 进行了编程。我当然知道静态类型和动态类型之间的区别。据了解,我没有任何偏好,对我来说只有语言本身很重要。因此,我想知道为什么 clojure 的设计者选择动态而不是静态。他们网站上的奇怪声明让我感到困惑,现在我想知道动态类型对 clojure(不是一般情况)和一般函数式编程范式的优势。
  • 我认为您误读了网站上的那句话。它说“Clojure 是动态的”,而不是说 Clojure 是动态类型的(尽管它是动态类型的)。那是不同的侧重点。它说 Clojure 中的大多数东西都是具体化的,可以在运行时更改(例如命名空间)。这是一种务实的选择,如果您添加一些类型限制(参见例如类型提示),没有人会反对。 Rich Hickey 指出,他希望类型系统/限制是可插入的,即与其他语言设计选择正交。这对我来说听起来很明智。
  • 你可以在 Lisp 中插入你自己的类型系统,这就是 clojure.typed 和 TypedRacket 的样子。我正在尝试研究如何使用类型推断将单元测试,即 (=(myadd a b) (+ a b)) 转换为 clojure.typed 的类型注释。 Lisp 是如此动态,以至于不可能拥有适用于所有代码的类型,但宏在编译时运行,允许进行静态分析。 Hickey 在 Haskell 中给出了示例,如果你从 (Maybe -> a) 或 (a -> Maybe a) 转换函数,你必须编写 fromJust Just 大部分代码(尽管编译器会告诉你在哪里,它可以是自动)。

标签: dynamic functional-programming clojure language-design paradigms


【解决方案1】:

(我改写原来的答案,因为它产生了太多的误解)

保持 Clojure(和任何 Lisp)动态类型化的原因之一是为了简化宏的创建。简而言之,宏处理抽象语法树(AST),它可以包含许多不同类型的节点(通常是任何对象)。理论上,可以制作完整的静态类型宏系统,但在实践中,此类系统通常受到限制且分布稀疏。请参阅下面的示例和线程中的扩展讨论。


EDIT 2020:哇,距离我发布这个答案已经过去了 9 年,人们仍在添加 cmets。我们都留下了多少遗产!

有些人在 cmets 中指出,拥有静态类型语言并不妨碍您将代码表达为数据结构。而且,严格来说,这是真的——联合类型允许表达任何复杂的数据结构,包括语言的语法。然而,我声称要表达语法,你必须要么降低表达能力,要么使用如此广泛的联合,以至于你失去了静态类型的所有优势。为了证明这一说法,我将使用另一种语言 - Julia。

Julia 是可选类型的 - 您可以将任何函数或结构字段限制为具有特定类型,Julia 会检查它。该语言使用 ExprSymbol 类型支持 AST 作为一等公民。表达式定义如下所示:

struct Expr
  head::Symbol
  args::Vector{Any}
end

表达式由一个始终是符号的头部和可能具有任何类型的参数列表组成。 Julia 还支持特殊的Union,它可以将参数限制为特定类型,例如Symbols 和其他Exprs:

struct Expr
  head::Symbol
  args::Vector{Union{Symbol, Expr}}
end

足以表达例如:(x + y):

dump(:(x + y))
Expr
  head: Symbol call
  args: Array{Any}((3,))
    1: Symbol +
    2: Symbol x
    3: Symbol y

但 Julia 还支持许多其他类型的表达式。一个明显且有用的例子是文字:

:(x + 1)

此外,您可以使用插值或手动构造表达式将任何对象放入 AST:

obj = create_some_object()

ex1 = :(x + $objs)
ex2 = Expr(:+, :x, obj)

这些示例不仅仅是一个有趣的实验,它们在实际代码中被积极使用,尤其是在宏中。因此,您不能将表达式参数限制为特定类型的联合 - 表达式可能包含 任何 值。

当然,在设计一种新语言时,您可以对其施加任何限制。也许,将Expr 限制为仅包含SymbolExpr 和一些Literals 在某些情况下会很有用。但这违背了 Julia 和 Clojure 的简单性和灵活性原则,并且会显着降低宏的实用性。

【讨论】:

  • 这将是一个'a -> 'b,其中'a'b 进一步受到(*) 运算符类型的限制。
  • @Tobu:在评估(defn square [x] (* x x)) 之后,这将是square 类型,但我不认为这是朋友们的观点。据我了解,他的观点是,如果您将函数定义传递给 as data,那么类型会是什么。
  • @Tobu:我问的不是结果类型,而是作为符号列表的表达式类型。 IE。 (list 'defn 'square ['x] '(* x x))的类型是什么
  • 顺便说一句:据我了解,类型提示是为了提高性能,而不是以任何方式保证类型安全。
  • 糟糕,它不起作用。尽管如此,我的观点是 Lisp 有很多编译和运行阶段,在这些阶段你可以使用多种类型的对象。静态类型会引入很多复杂性,所以携带类型会让编写宏代码变得很麻烦。
【解决方案2】:

首先,Clojure 是一个 Lisp,而 Lisp 传统上一直是动态类型的。

第二,作为你引用的摘录,Clojure 是一种动态语言。这意味着,除其他外,您可以在运行时定义新函数,在运行时评估任意代码等等。在静态类型语言中,所有这些事情都很难或不可能做到(无需到处抹灰)。

另一个原因是宏可能会使调试类型错误变得非常复杂。我想为宏生成的代码产生的类型错误生成有意义的错误消息对于编译器来说将是一项艰巨的任务。

【讨论】:

  • 我不知道在编译语言中评估任意代码是可能的。这就解释了“交互式”。除此之外,我更关心那些糟糕的编译器,而不是那些打算进行调试的人;-)
  • @lhk:我的意思是,由于这对编译器来说是一项艰巨的任务,因此很可能会产生低于标准的结果——这让进行调试的可怜人感到沮丧。想想 C++ 中的模板错误消息 shudder
  • 回复:“所有这些事情在静态类型语言中都很难或不可能做到(无需到处抹灰)。”这可能适用于许多静态类型语言的某些实现;但是,这种限制不受静态类型语言的限制。
  • 回复:“在运行时定义新函数”。静态类型语言可以选择允许在运行时定义函数。例如,这可能发生在 REPL 中。
  • 并且要明确一点:您是否需要强制转换来做某事或给定的代码是否合法是语言的属性,而不是实现。如果一种语言的规则说print(eval("12 + 13") * 2) 是合法的,那么该语言的所有实现都必须接受该代码,否则它们不是该语言的有效实现。如果规则说除非您添加强制转换,否则该代码是错误类型的,那么所有实现都必须需要强制转换。
【解决方案3】:

我同意,纯函数式语言仍然可以具有交互式读取-评估-打印-循环,并且可以更轻松地进行类型推断。我假设 Clojure 想通过成为“jvm 的 lisp”来吸引 lisp 程序员,并选择像其他 lisp 一样动态。另一个因素是类型系统需要设计为语言的第一步,语言实现者跳过这一步会更快。

【讨论】:

  • 成为 Lisp 可能是其中的一部分,但 Rich Hickey 经常提倡动态类型本身具有很多优点,即使他没有选择这样做,他也可能会做出这个决定让 Clojure 成为 Lisp。
  • “[T] 类型系统需要被设计为语言的第一步”——并非如此。 Typed Racket 在无类型函数语言 (Racket) 之上构建类型系统。
  • Typed Clojure 也做了同样的事情——你甚至可以混合使用静态和动态类型的代码。
  • 虽然 Haskell(我的 Pythonista 的硬语言)默认使用类型推断的静态类型,但您也可以使用 hackage.haskell.org/package/dynamic 将其更改为动态类型。您也可以将其设置为忽略类型错误,并且 Template Haskell 可以做 Lisp 宏所做的事情,但一开始要困难得多,但是一旦完成,您就会得到静态类型保证(我想这就像 Haskell 的其余部分...... )。静态类型的好处允许你使用 hoogle 和 _(holes)来帮助你为你编写代码,有点像“通用自动完成”。
【解决方案4】:

如果一个程序被编译了,你就不能再改变它了。

这是错误的。在基于图像的系统中,如 Lisp(Clojure 可以看作是一种 Lisp 方言)和 Smalltalk,您可以更改编译环境。使用这种语言进行开发通常意味着在正在运行的系统上工作,添加和更改函数定义、宏定义、参数等(添加意味着编译和加载到图像中)。

这有很多好处。一方面,所有工具都可以直接与程序交互,无需猜测系统的行为。您也没有任何长时间的编译暂停,因为每个编译单元都非常小(重新编译所有内容非常罕见)。 NASA JPL 曾经在数十万公里外的太空探测器上纠正了一个正在运行的 Lisp 系统。

对于这样的系统,在运行时提供类型信息是很自然的(这就是动态类型的含义)。当然,没有什么能阻止您在编译时进行类型推断和类型检查。这些概念是正交的。现代 Lisp 实现通常可以两者兼得。

【讨论】:

  • 什么是基于图像的系统?
  • @lhk 见here。它基本上是一个系统,可让您将其当前状态转储到映像中,您可以将其用于任何目的(例如,制作可执行文件:将系统转储到加载它的可执行文件中 + 运行入口点)。
  • @Svante,这些概念不是正交的。动态类型不仅仅允许在运行时读取类型——类型可以改变(即加法和减法)。静态类型语言将运行时错误转化为编译时错误。
  • 一个类型没有方法type 只是一组值。但无论如何,是的,还有进一步的影响和权衡。对我来说,不允许小的改动,总是重新编译世界是一个很大的缺点。最后甚至在 e 中。 G。 Idris,您仍然必须使用类型系统,这并非易事,但任何不完美的东西都可以从面对错误时的优雅运行时行为中受益匪浅。
【解决方案5】:

因为这是世界/市场所需要的。建造已经建造的东西毫无意义。

我听说 JVM 已经有了静态类型语言;)

【讨论】:

  • Scala 过于复杂,有很多像 C++ 这样的极端情况。
  • @HamishGrubijan 很有趣,我发现 Scala 方式比 Clojure 更简单。
  • 科特林。现在是 2018 年
  • Clojure 现在是 2118 年...等得太早了 ;-)
猜你喜欢
  • 2014-09-27
  • 2011-11-15
  • 1970-01-01
  • 2015-01-03
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-10-25
  • 1970-01-01
相关资源
最近更新 更多