2024 Clojure 状态调查! 中分享您的想法。

欢迎!请参阅 关于 页面,了解有关该页面如何工作的更多信息。

+3
Clojure
编辑

我已经使用 Clojure 大约 2 年了,并且非常喜欢这个语言。我不想在这里展开静态类型与动态类型之争,我在 Clojure 和 Rust 之间切换项目。Clojure 中我最大的问题是函数接收和返回的关键字和数据形状的可发现性。

例如,当我对代码库工作了一段时间后,我跳回来查看函数时,并不清楚被接受的数据结构。同样,与地图一样,选择正确的关键字(例如,它是 :home-address 还是 :address-home)需要花费更多时间在当前编写之外查找代码。我最近还试图使用 clj-fx,这是一场识别正确关键字的噩梦。(需要挖掘 Java 文档)。

在使用静态类型语言时,我发现能够立即看到参数和返回值的结构/类型,而无需重新学习函数体,这非常有用。我想在 Clojure 中如何最好地解决这种程序员“问题”的可发现性。我知道这不是一个新问题,可以通过多种方法解决。

  • Specs:虽然为每个函数创建一个单独的 spec 定义有点麻烦。我也尝试过使用 defn-spec 和 Ghostwheel/Orchestra,但似乎没有一个能够干净地集成且没有其他问题(例如,Linting)。
  • 富有表达力的文档字符串:有效,但与注释脱节的问题一样。
  • 丰富的注释块。我喜欢这种方法,并且大量使用。虽然它有点重复,但至少它具有多重用途(文档记录 + 通过评估进行探索)。
  • 测试在另一个文件中:不喜欢这种方法,因为它需要切换文件并搜索测试。
  • 在 defn 中进行测试
  • Spec2 在未来

我想知道人们在这个方面认为的最佳实践是什么?我在哪些方面还缺少,因为显然 Clojure 在分布式组织中相对较大的代码库中被广泛使用。有任何展示这些实践的项目的好例子吗?

2 个回答

+1

我总是在函数中使用解构,并且使用带有指定关键字的解构,我可以看到这个关键字在这里有哪些可用。

要了解函数中可用的哪些关键字,我设置了作用域捕获,然后运行测试(最好是集成测试)

作用域捕获有很多种方法,从库到内联定义,tap>> 也是一种作用域捕获。

有了捕获的作用域,我可以在repl中简单地请求(keys argm),它会打印出该函数中所有的可用关键字。

0

对于我来说,有两种情况:要么函数描述了其输入,调用者必须按正确形状提供它们,要么函数利用现有的应用程序实体(数据模型),并应通过名称引用其定义。

在函数没有操作实体的情况下,我确保有好的参数名称,在文档字符串中描述形状,并在接受映射或元组时使用解构,为每个元素命名。有时我也可能会有规范,尽管这只有在我想使用它进行生成测试或验证时才会发生。

在函数操作实体的情况下,我让参数名称与实体定义的名称相同。

我通过构造函数定义实体,或者通过名为 make-foo 的函数创建一个概念类型 foo 的实体实例。该函数将记录其创建的形状,或者你可以从其代码中很容易地推断出来。或者我使用元组定义它们,其中元组定义描述了形状。或者我使用 Spec,其中规范名称是实体的名称。

(defn make-item
  "Makes an item which is a map of key
   :id UUID of the todo-list
   :name - the user readable string name of the item
   :content - the user provided string content of the item"
  [name content]
  {:id (java.util.UUID/randomUUID)
   :name name
   :content content})

(defn make-todo-list
  "Makes a todo-list which is a map of key
    :id - UUID of the todo-list
    :name - the user readable string name of the list
    :items - an ordered vector of item"
  [name & items]
  {:id (java.util.UUID/randomUUID)
   :name name
   :items (vec items)})

(defn insert-item
  "Inserts an item in a todo-list at given 0-based position"
  [todo-list item position]
  (apply conj
    (subvec todo-list 0 position)
    item
    (subvec todo-list position (count todo-list))))

(defn sanitize-input
  "Given an untrusted input string, return a sanitized version.
   Takes an optional options map with options:
     :extra-denylist - a seq of string words to delete on top of default sanitization."
  [input-string & {:keys [extra-denylist] :as options}]
  (some-lib/sanitize input-string extra-denylist)

所以正如你在我的小示例中所看到的,sanitize-input 是一个不操作应用程序域模型的函数示例,因此它只是使用好的参数名称、解构和很好的文档字符串来描述其输入。

另一方面,insert-item 是一个操作应用程序域模型的函数,因此它只使用了作为输入的域实体的名称,其额外的 position 参数仅通过良好的名称和文档字符串进行描述。

最后,域实体通过各自名称的 make- 函数来描述。我可以使用记录代替它们,或者也可以使用 Spec。

...