malli와 clj-kondo로 바꾸기(\63;)Clojure 개발 환경

31432 단어 ClojureVimtech
메리 크리스마스!!🎄🎄
이 보도는 Clojure Advent Calendar 2020 25일째를 대상으로 한다.

TL;DR



아직 멜리가 정의한 규격을 clj-kondo에서 모두 처리할 수 없습니다.
그래서 지금 내구성이 있는지 없는지는 그렇지 않다.
없다기보다는 개발 체험을 개선할 수 있는 수준이 생겼다.

무엇


metosin/malli는 Clojure/ClojureScript에서 데이터를 정의하는 규격으로 데이터가 이 규격에 맞는지 확인하는 기능을 제공하는 프로그램 라이브러리입니다.
https://github.com/metosin/malli
부근의 프로그램 라이브러리clojure.specplumatic/schema가 있지만 멜리의 특징은 모든 규격을'데이터'로 정의할 수 있다는 것이다.
(require '[malli.core :as m])

(def IntMap
  ;; key がキーワード、 value が整数であるマップの定義
  ;; キーワードとシンボルからなるただのデータ
  [:map-of :keyword 'int?])

(m/validate IntMap {:foo 1}) ; => true
(m/validate IntMap {:foo "bar"}) ; => false
데이터의 정의는 물건에 따라 표현력도 어렵지만 귀속식의 규격 정의에 대응할 수 있고malli도 높은 규격 정의를 할 수 있다.
(def IntCons
  [:schema {:registry {::int-cons [:maybe [:tuple pos-int? [:ref ::int-cons]]]}}
   ::int-cons])

(m/validate IntCons [10 [20 [30 nil]]]) ; => true
(m/validate IntCons [10 [-20 [30 nil]]]) ; => false
기본적으로 데이터의 검증이 주요 기능이지만 일찍이plumatic/schema의s/defn와clojure가 시작되었다.스펙fdef과 같은 계약 프로그래밍에 사용되는 함수에 대한 규격 정의가 간절히 기대됩니다.
https://github.com/metosin/malli/issues/125

clj-kondo 연합


이러한 함수에 대한 규격 정의#306에서 clj-kondo와의 합작(좋은 의미에서)은 예상치 못한 형식으로 합병되었다.
(현재 버전0.2.1에 포함되지 않음master 분기 테스트가 필요함)
clj-kondo에 대해서는 Clojure Advent Calendar 2020제9일의 보도을 참조하십시오.plumatic/schema, clojure.spec에서 개인의 불만은'평가를 하지 않을 때 오류가 검출되지 않는다'는 것이다.
동적 유형 언어로서 당연한 것이지만 평가/집행 전에 가장 잘 알 수 있는 것은 규격을 정의해도 이 함수를 집행하는 테스트가 존재하지 않으면 검사가 흔들리지 않고 의미가 없다는 것이다.
그러나 clj-kondo는 코드를 정태적으로 분석하고 경고 등을 보내기 때문에 평가 여부는 무관하다.
Clojure의 개발 경험을 바꿀 수 있지 않을까요!?내 생각엔

구조


그렇다면 실제 시도와 함께 clj-kondo가 어떻게 구성된 조합인지 살펴볼 것이다.
말리는 위에서 말한 대로 마스터Leiningen를 사용하고 싶어서 사용하지 않는다Clojure CLI.
우선 준비deps.edn.
metosin/malli는 이 글을 쓸 때 가장 최근에 제출한 SHA를 지정합니다.
deps.edn
{:paths ["src"]
 :deps {org.clojure/clojure {:mvn/version "1.10.1"}
        metosin/malli {:git/url "https://github.com/metosin/malli"
                       :sha "4c854a283697fbc22856145e514be7c2b1853edf"}}}
다음은src/foo/core.clj.
우선 plus 정수를 간단하게 덧붙인 함수를 예로 삼아 입력과 출력을 정의한다.
src/foo/core.clj
(ns foo.core
  (:require
   [malli.core :as m]))

(defn plus
  [a b]
  (+ a b))

;; ここで plus は引数として整数を2つ受け取り、整数を返す旨を定義
(m/=> plus [:=> [:tuple int? int?] int?])

(comment
  (plus 1 2) ; => 3
  (plus 1 "2") ; java.lang.String cannot be cast to java.lang.Number
  )
이때 멜리는 아직 clj-kondo와 합작하지 않았다.
$ clj-kondo --lint src
linting took 35ms, errors: 0, warnings: 0
에서 deps.edn이 있는 디렉터리clj에서 명령에서 다음 코드를 평가하십시오.
$ clj
Clojure 1.10.1
user=> (require 'malli.clj-kondo)
nil
user=> (require 'foo.core)
nil
user=> (malli.clj-kondo/emit!)
nil
이면 .clj-kondo/configs/malli/config.edn 이런 파일을 만들 수 있을까.이것은 malli가 정의한 foo.core/plus에 대한 유형 정의를 clj-kondo가 설명할 수 있는 유형 정의로 변환한 것이다.
https://github.com/borkdude/clj-kondo/blob/master/doc/types.md
네.즉,malli의 clj-kondo 협업(malli.clj-kondo/emit!)을 통해 clj-kondo에 대한 설정을 동적으로 생성한다..clj-kondo/configs/malli 아래 설정은 자동으로 읽을 수 없기 때문에 읽을 수 있도록 설정.clj-kondo/config.edn을 추가합니다.
.clj-kondo/config.edn
;; configs/malli 配下の設定も利用するための設定
{:config-paths ["configs/malli"]}
이 기초 위에서 다시 clj-kondo를 실행하다.기대하지 않는 매개 변수에서 함수를 호출할 때 오류가 발생한 것을 알 수 있습니다.
$ clj-kondo --lint src
src/foo/core.clj:14:11: error: Expected: integer, received: string.
linting took 16ms, errors: 1, warnings: 0
이 시점에서 정의를 추가하거나 변경할 때마다malli.clj-kondo/emit! 수동으로 실행하지 않으면 설정이 업데이트되지 않아 번거로운 인상을 줄 수 있지만 편집기 측의 설정이 어떻게 되는지 잠시 눈을 감아 주십시오.
만약 정말 마음에 든다면 글 끝에 Vim과vim-id의 예만 실렸으니 참고하세요.

조금 신나는 예.

foo.core/plus의 예는 너무 단순해서 기쁜 마음을 전달하기 어려워서 기쁜 예를 다시 생각해 보고 싶어요.
자주 곤란한 것은 함수의 매개 변수로 지도를 받아들이는데 어떤 키/value를 지정하는 것이 좋을지 모른다는 것이다.
(defn plus-pos
  "引数の p1, p2 はそれぞれ :x, :y キーに数値が紐付くマップ
  のようなことを書いておかないと忘れてしまう関数"
  [p1 p2]
  {:x (+ (:x p1) (:x p2))
   :y (+ (:y p1) (:y p2))})
이런 경우plumatic/schema, clojure.spec도 없앨 수 있지만 앞에서 말한 대로 평가를 하지 않으면 모르기 때문에 코드를 쓰는 도중에 신속하게 알아볼 수 있다면 매우 기쁠 것입니다.
그럼 malli로 규격을 정의합시다.
;; 引数として受け取るマップの定義
(def PosMap
  [:map
   [:x int?]
   [:y int?]])

;; 関数の定義
(m/=> plus-pos [:=> [:tuple PosMap PosMap] PosMap])
이 정의emit!를 clj-kondo로 실행할 때 다음과 같은 오류가 발생합니다.
(plus-pos {:z 1 :y 2} {:x 3 :y 4})
;; error: Missing required key: :x

(plus-pos {:x "1" :y 2} {:x 3 :y 4})
;; error: Expected: integer, received: string.
글의 첫머리에 붙인 애니메이션은 편집기에 이 오류 검출을 연합하여 표시하는 예이다.

이렇게 하면 테스트 코드를 쓰지 않아도 매개 변수의 오류를 검출할 수 있다
나는 Clojure의 개발 체험에서 편집기에 그것을 표시할 수 있다는 것이 상당히 새롭다고 생각한다.

명백한 문제점


내가 이렇게 좋은 곳만 소개했으니 당연히 문제가 있지.

반환 값에 대한 오류가 감지되지 않았습니다.


함수의 실제 반환값과 기대 반환값을 정의하는 사이의 차이는 검출되지 않습니다.
(defn plus
  [a b]
  ;; 実際の戻り値は文字列
  (str a b))

;; 定義上の戻り値は int?
(m/=> plus [:=> [:tuple int? int?] int?])

(plus 1 2) ;; エラーは検知されない
생성된 clj-kondo의 정의만 보면 다음과 같은 결과가 기대하는 내용을 정의했다.
{plus {:arities {2 {:args [:int :int], :ret :int}}}}
이것은 다른 함수 호출을 검사할 때 사용됩니다.즉, 함수의 실제 반환값과 정의는 일치하는 전제이다.
(seq (plus 1 2))
;; error: Expected: seqable collection, received: integer.

malli의 모든 규격에 따라 검측할 수 없습니다


malli는 항상 정의된 규격을 clj-kondo를 위한 설정으로 변환하기 때문에 malli로 정의할 수 있어도 clj-kondo가 지원하지 않는 설정은 변환할 수 없습니다.
따라서 아래와 같은 :enum 또는 :fn의 규격은malli에서만 검증할 수 있고 clj-kondo에서는 검측할 수 없다.
;; Enum
;; ----------
(def Animal
  [:enum "Cat" "Dog" "Wani"])

(defn greet
  [x]
  (str "Hello " x))

;; 生成される clj-kondo 向け設定: {:arities {1 {:args [nil], :ret :string}}}
(m/=> greet [:=> [:tuple Animal] string?])

(m/validate Animal "Snake") ; => false
(greet "Snake") ;; エラーは検知できない

;; Fn
;; ----------
(def PosFnMap
  [:and
   [:map
    [:x int?]
    [:y int?]]
   [:fn (fn [{:keys [x y]}] (> x y))]])

;; 生成される clj-kondo 向け設定: {:arities {1 {:args [:any], :ret :string}}}
(m/=> format-pos-map [:=> [:tuple PosFnMap] string?])

(m/validate PosFnMap {:x 2 :y 1}) ; => true
(m/validate PosFnMap {:x 1 :y 2}) ; => false

(format-pos-map {:x 1 :y 2}) ;; エラーは検知できない

데이터 변환을 통해 감지할 수 없음

plus-pos의 정의로 돌아가 보세요.원래의 예에서 지도는 함수에 직접 건네주지만 기본적으로let 등 속박된 물건을 함수에 건네주는 경우가 많다.
일반적으로 아래와 같이 데이터를 조작한 후 함수에 전달하지만 정확하게 검출되지 않는 경우가 많다.
(defn plus-pos
  [p1 p2]
  {:x (+ (:x p1) (:x p2))
   :y (+ (:y p1) (:y p2))})

(def PosMap
  [:map [:x int?] [:y int?]])
(m/=> plus-pos [:=> [:tuple PosMap PosMap] PosMap])

;; これは正しくチェックされる
(let [p1 {:x 1 :y 2}
      p2 {:x 3 :y 4}]
  (plus-pos p1 p2))

;; p2 に :y が無いことは検知されない
(let [p1 {:x 1 :y 2}
      p2 (dissoc p1 :y)]
  (plus-pos p1 p2))

;; p2 に :y が無い扱いでエラーになってしまう
(let [p1 {:x 1 :y 2}
      p2 {:x 3}
      p2 (assoc p2 :y 4)]
  (plus-pos p1 p2))

최후


malli의 clj-kondo 협업을 통해 clj-kondo 정적 해석이'형'에 국한되지 않는'규격'정의의 설정을 생성하고 Clojure의 약점 중 하나인 코드의 평가를 거치지 않고 유형 오류와 데이터 구조의 오류를 검출할 수 있습니다.
이로써 지금까지 평가된 주의하기 어려운 오류/오류를 조속히 발견하고 개발 효율과 더욱 안전한 인코딩 가능성을 높일 수 있다.
다만 전술한 바와 같이 아직 문제점이 많은데, 감지되지 않은 오류와 오류의 오류 검출이 있어 당장 적용할 수 없는 점도 사실이다.(이 때문에 발매되지 않았나 봐?)
하지만 클로져의 개발 체험이 완전히 바뀔 수 있다고 생각해 피해가 없는지 주목해보자.

참조: 자동 emit!의 예


위에서 말한 바와 같이malli가clj-kondo에 대한 정의는 동적 생성이기 때문에 어느 곳에서 생성된 코드에 대한 평가가 필요하다.
하지만 수공으로 하는 것은 번거로움 외에는 아무것도 없어 어렵게 얻은 좋은 개발 체험을 몽롱하게 만들었다.
따라서 기본적으로(malli.clj-kondo/emit!)의 동적 생성은 편집기 측면의 설정이나 무언가를 통해 자동으로 하는 것이 가장 좋다.

Vim(vim-id)의 설정 예


Vim으로 설정할 때 BufWritePre 등 파일을 저장할 때 autoocmd를 터치합니다. emit 오류:!하면 돼.
졸작 플러그인vim-iced을 사용할 때 다음과 같은 설정을 통해 파일을 저장할 때 자동으로 emit를 생성합니다!네.
function! s:emit() abort
  if !iced#nrepl#is_connected()
    return
  endif

  call iced#nrepl#eval('(do (require ''malli.clj-kondo) (malli.clj-kondo/emit!))',
        \ {_ -> iced#message#info_str('emitted')})
endfunction

aug MyMalliSetting
  au!
  au BufWritePre *.clj,*.cljs,*.cljc,*.edn  call s:emit()
aug END
malli를 아직 사용하지 않은 프로젝트에서 이 설정이 장애thinca/vim-localrc가 되면 이러한 방법을 이용하여 프로젝트의 로컬 설정으로 정의할 수 있다.

좋은 웹페이지 즐겨찾기