学习Clojure:应对动态打字

我的新职位要求我熟悉Clojure语言 。 打算记录我在一系列帖子中学到的内容,以作为我的个人参考笔记。 作为副作用,我希望它对希望走同样道路的其他人也将是有益的。 考虑到我的大部分经验都来自OOP ,因此已经有大量出色的教程可供使用:因此,每篇文章都将专注于特定主题,该主题特定于Clojure。

这是在学习Clojure的焦点series.Other职位包括第2 职位:

  1. 解码Clojure代码,让您不知所措
  2. 学习Clojure:应对动态类型化 (本文)
  3. 学习Clojure:arrow和doto宏
  4. 学习Clojure:动态调度
  5. 学习Clojure:依赖类型和基于合同的编程
  6. 学习Clojure:与Java流进行比较
  7. 关于学习Clojure的反馈:与Java流进行比较
  8. 学习Clojure:换能器

作为Clojure的新手,我的一个大问题是缺乏类型。 这不是Clojure特有的。 我错过了JavaScript,Groovy,Python等中的类型。尽管我重视动态语言在编写脚本时的易用性,但我的主要任务仍然是开发常规应用程序。 考虑到这一点,我更喜欢让编译器捕获与类型相关的错误:这意味着着眼于实际的业务功能,而不是编写测试来捕获这些错误。

尽管Clojure不提供语言语法中的类型,但其设计允许通过构造来构建类似的功能。 更好的是,有一个适当的命名规范spec来处理这个问题 。

自Clojure 1.9起,即可立即使用spec。 早期版本需要显式添加对类路径的依赖。

基本

要开始使用spec,只需在名称空间中要求 clojure.spec.alpha包即可:

1
2
( ns   ch.frankel.blog.clojure.spec
   ( :require   [ clojure.spec.alpha   :as   sparc ]))

下一步是使用def函数定义值的预期类型。 它接受两个参数:

名称 描述

1个

k

符号名称

2

spec-form

谓语

可能会有更多有效的参数值,但这足以开始。

对于简单的值,这非常简单:

1
2
3
( spec/def   ::nil   nil? )         <i class="conum" data-value="1"></i> <b class="raw_b_node">(1)</b>
 ( spec/def   ::bool   boolean? )    <i class="conum" data-value="2"></i> <b class="raw_b_node">(2)</b>
 ( spec/def   ::string   string? )   <i class="conum" data-value="3"></i> <b class="raw_b_node">(3)</b>
1个 ::nil定义为nil
2 ::bool定义为boolean
3 ::string定义为任何string

关键字是自我评估的符号标识符。 它们提供了非常快速的相等性测试。 像符号一样,它们具有名称和可选的名称空间,它们都是字符串。 前导“:”不是名称空间或名称的一部分。

— Clojure文档
https://clojure.org/reference/data_structures#Keywords

::语法是限定关键字 (使用当前名称空间完全限定的关键字 )的快捷方式。 例如,上面的::bool解析为:ch.frankel.blog.clojure.spec/bool

该技术不限于简单类型。 也可以将值限制为枚举:

1
( spec/def   ::direction   # { ::NORTH   ::EAST   ::SOUTH   ::WEST })

规格检查

定义规范后,有两种不同的使用方法。

  1. valid? 函数返回一个boolean ,具体取决于值是否符合spec 例如
    资源 符合吗? 退货
    1
    ( spec/valid?   ::nil   nil )

    true

    1
    ( spec/valid?   ::string   "f" )

    true

    1
    ( spec/valid?   ::nil   "f" )

    false

    1
    ( spec/valid?   ::string   nil )

    false

  2. conform? 函数返回:
    • 值是否符合spec
    • clojure.spec.alpha/invalid如果clojure.spec.alpha/invalid
    资源 符合吗? 退货
    1
    ( spec/conform?   ::nil   nil )

    nil

    1
    ( spec/conform?   ::string   "f" )

    "f"

    1
    ( spec/conform?   ::nil   "f" )

    clojure.spec.alpha/invalid

    1
    ( spec/conform?   ::string   nil )

    clojure.spec.alpha/invalid

自定义规格功能

上面的代码使用了现成的函数, 例如 nil? string? 。 有很多类似的功能。 这是一个示例,摘自clojure.core

功能 检查参数是否为...

keyword?

关键字(显然是...)

symbol?

一个符号(显然也是如此)

ident?

符号关键字

uuid?

一个java.util.UUID实例

uri?

一个java.net.URI实例

虽然涵盖了一些用例,但并未涵盖许多特定用例。 在这种情况下,可以使用任何接受参数并返回boolean函数。

这是一个检查参数是否为LocalDate以及如何使用的函数:

1
2
3
4
5
6
7
8
9
( defn   local-date?
   "Check if the parameter is a java.time.LocalDate instance"
   [ x ]
   ( instance?   LocalDate   x ))

 ( spec/def   ::date   local-date? )

 ( spec/valid?   ::date   ( LocalDate/of   2009   1   1 ))  <i class="conum" data-value="1"></i> <b class="raw_b_node">(1)</b>
 ( spec/valid?   ::date   "f" )                      <i class="conum" data-value="2"></i> <b class="raw_b_node">(2)</b>
1个 评估为true
2 评估为false

规范数据结构

现在我们知道如何指定简单的值,是时候指定更复杂的值了。 在Clojure中,对“实体”建模的一种常用方法是使用数据映射。

我最喜欢的示例是具有以下属性的Person实体:

  • 名字
  • 生日

可以使用keys功能进行指定。 参数允许指定哪些键是必需的,哪些是可选的:

1
2
3
4
5
6
( spec/def   ::first-name   string? )
 ( spec/def   ::last-name   string? )
 ( spec/def   ::birthdate   local-date? )

 ( spec/def   ::person   ( spec/keys   :req   [ ::first-name   ::last-name ]   <i class="conum" data-value="1"></i> <b class="raw_b_node">(1)</b>
                               :opt   [ ::birthdate ]))              <i class="conum" data-value="2"></i> <b class="raw_b_node">(2)</b>
1个 必要值
2 可选值

以下是一些示例以及一些相关的有效性检查:

资源 退货 基本原理
1
2
3
4
( spec/valid?   ::person   {
     ::first-name   "John"
     ::last-name   "Doe"
     ::birthdate   ( LocalDate/of   1970   1   1 )})

true

1
( spec/valid?   ::person   "f" )

false

string不是map

1
2
3
( spec/valid?   ::person   {
     ::last-name   "Doe"
     ::birthdate   ( LocalDate/of   1970   1   1 )})

false

map::first-name键下不包含值

1
2
3
( spec/valid?   ::person   {
     ::first-name   "John"
     ::last-name   "Doe" })

true

不需要birthdate

1
2
3
4
( spec/valid?   ::person   {
     ::first-name   "John"
     ::last-name   "Doe"
     ::birthdate   "Unknown" })

false

birthdate不是LocalDate

1
2
3
4
5
( spec/valid?   ::person   {
     ::first-name   "John"
     ::last-name   "Doe"
     ::birthdate   ( LocalDate/of   1970   1   1 )
     ::title   "Mr" })

true

其他条目也可以

规格集合

下一步是使用规范来验证集合中元素的类型,就像Java中的泛型一样, 例如 ListMapSet 。 这可以通过附加功能来实现:

  • coll-of为“标准”的Clojure集合, vectorlist等。
  • map-of地图

例如,指定LocalDate的列表非常简单:

1
( spec/def   ::dates   ( spec/coll-of   ::date ))

同样,对于keyword / LocalDate的映射:

1
( spec/def   ::map-dates   ( spec/map-of   keyword?   ::date ))

当然,它也适用于数据结构:

1
( spec/def   ::map-persons   ( spec/map-of   keyword?   ::person ))

结论

尽管Clojure是一种动态类型的语言,但可以通过使用spec库来补充类型。 它允许验证简单的类型,枚举,映射和集合,就像使用任何静态类型的语言一样。

更进一步:

  • 规格指南

关注@nicolas_frankel

翻译自: https://blog.frankel.ch/learning-clojure/1/