Spectacular Future with clojure.spec

129 Views

September 20, 17

スライド概要

clojure.specを活用して変更に強いClojureコードを書こう(*> ᴗ •*)ゞ
cf. https://github.com/lagenorhynque/spec-examples

profile-image

「楽しく楽にcoolにsmartに」を理想とするprogrammer/philosopher/liberalist/realist。 好きな言語はClojure, Haskell, Python, English, français, русский。 読書、プログラミング、語学、法学、数学が大好き! イルカと海も大好き🐬

シェア

またはPlayer版

埋め込む »CMSなどでJSが使えない場合

関連スライド

各ページのテキスト
1.

Spectacular Future with clojure.spec

2.

Self-introduction lagénorhynque /laʒenɔʁɛ k ̃ / カマイルカ (defprofile lagénorhynque :name "Kent OHASHI" :languages [Clojure Haskell Python Scala English français Deutsch русский] :interests [programming language-learning mathematics] :contributing [github.com/japan-clojurians/clojure-site-ja])

3.

「Clojureをプロダクトに導⼊した話」

4.

Clojure

5.

Contents 1. Clojure Quick Intro 2. New Feature: clojure.spec

6.

Clojure Quick Intro

7.

Clojure Lisp S式, マクロ, etc. REPL駆動開発 関数型プログラミング⾔語 動的⾔語 JVM⾔語 (cf. ClojureScript) ⇒ シンプルで強⼒な⾔語

8.

リテラル type example string "abc" character \a number 1, 2.0, 3N, 4.5M, 6/7, 8r10 boolean true, false nil nil keyword :a, :user/a, ::a, ::x/a symbol 'a, 'user/a, `a, `x/a

9.

type example list '(1 2 3), '(+ 1 2 3) vector [1 2 3] set #{1 2 3} map {:a 1 :b 2}, #:user{:a 1 :b 2}, #::{:a 1 :b 2}, #::x{:a 1 :b 2} function (fn [x] (* x x))

10.

シンタックス (op arg1 arg2 ... argn) オペレータ 関数 マクロ 特殊形式

11.

New Feature: clojure.spec

12.

例: 直⽅体の体積計算 user> (defn cuboid-volume [{:keys [side-a side-b side-c]}] (* side-a side-b side-c)) #'user/cuboid-volume user> (cuboid-volume {:side-a 1 :side-b 2 :side-c 3}) 6 直⽅体の体積 = 辺a × 辺b × 辺c

13.
[beta]
エラー
;; マップの値の型が数値ではなく⽂字列
user> (cuboid-volume {:side-a 1 :side-b "2" :side-c 3})
ClassCastException java.lang.String cannot be cast to java.lang.
Number clojure.lang.Numbers.multiply (Numbers.java:148)
;; マップのキー名でtypo
user> (cuboid-volume {:side-a 1 :side-d 2 :side-c 3})
NullPointerException
clojure.lang.Numbers.ops (Numbers.java:10
18)
;; マップに必須のキーがない
user> (cuboid-volume {:side-a 1 :side-c 3})
NullPointerException
clojure.lang.Numbers.ops (Numbers.java:10
18)

Javaのスタックトレースが\(^o^)/……

14.

問題点 動的⾔語なのでコンパイル時に引数の型の不整合が 検出されない 少なくとも実⾏時に分かりやすいエラーになって ほしい マップのkey-valueに対するチェックがない 動的なデータを柔軟に表現したい

15.
[beta]
cf. core.typed
漸進的型付け(gradual typing)
Clojureに静的型システムを追加
コンパイル時に型チェック
(require '[clojure.core.typed :as t])
(t/ann cuboid-volume [(t/HMap :mandatory {:side-a t/Num
:side-b t/Num
:side-c t/Num})
:-> t/Num])
(defn cuboid-volume [{:keys [side-a side-b side-c]}]
(* side-a side-b side-c))

16.

cf. schema データ記述/バリデーションDSL 独⾃DSLでデータ構造を表現 実⾏時にバリデーション (require '[schema.core :as s]) (s/defn cuboid-volume :- s/Num [{:keys [side-a side-b side-c]} :- {:side-a s/Num :side-b s/Num :side-c s/Num}] (* side-a side-b side-c))

17.

clojure.spec 述語(predicate)を組み合わせて仕様(spec)を書いて ドキュメント バリデーション 詳細なエラー報告 パースと分配束縛 データ⽣成 プロパティベーストテスト などを実現する仕組み

18.

dependency [org.clojure/clojure "1.9.0-beta1"] REPL user> (require '[clojure.spec.alpha :as s] '[clojure.spec.test.alpha :as stest] '[clojure.spec.gen.alpha :as gen]) nil

19.

「⻑さ」をspecで表現 user> (s/def ::length number?) :user/length user> (doc ::length) ------------------------:user/length Spec number? nil s/def でキーワード :user/length に述語 number? を登録

20.

「⻑さ」に⼀致する値を調べる user> (s/conform ::length 3) 3 user> (s/conform ::length "3") :clojure.spec.alpha/invalid s/conform で ::length のspecに具体的な値が ⼀致するか確認 ⼀致しなければ ::s/invalid

21.

⼀致しない原因を調べる user> (s/explain ::length "3") val: "3" fails spec: :user/length predicate: number? :clojure.spec.alpha/spec :user/length :clojure.spec.alpha/value "3" nil s/explain で⼀致しない詳細原因を確認 ここでは 値 "3" がspec :user/length の述語 number? に不⼀致

22.

「⻑さ」のサンプルを⽣成 user> (s/gen ::length) #clojure.test.check.generators.Generator{:gen #function[clojure. test.check.generators/such-that/fn--13745]} user> (gen/sample (s/gen ::length)) (0.5 -1.0 -1 0.5 3.25 0 -3 0.6875 0.25 0) s/gen で test.check 互換なジェネレータを取得 gen/sample でサンプルを⽣成

23.

「⻑さ」に制約を加える user> (s/def ::length (s/and number? pos?)) :user/length user> (doc ::length) ------------------------:user/length Spec (and number? pos?) nil user> (gen/sample (s/gen ::length)) (0.5 3.0 0.5 1.375 1.6875 3.0 0.625 1.25 0.41015625 0.25) 論理演算⼦で制約を追加 s/and は論理積 cf. s/or

24.

「⻑さ」で辺a, b, cを表現 user> (s/def ::side-a ::length) :user/side-a user> (doc ::side-a) ------------------------:user/side-a Spec (and number? pos?) nil user> (s/def ::side-b ::length) :user/side-b user> (s/def ::side-c ::length) :user/side-c

25.

辺a, b, cを持つマップとして直⽅体 を表現 user> (s/def ::cuboid (s/keys :req [::side-a ::side-b ::side-c]) ) :user/cuboid user> (doc ::cuboid) ------------------------:user/cuboid Spec (keys :req [:user/side-a :user/side-b :user/side-c]) nil s/keys で必須のキーを持つマップを表現

26.
[beta]
直⽅体に⼀致する値を調べる
user> (s/conform ::cuboid #::{:side-a 1
:side-b 2
:side-c 3})
#:user{:side-a 1, :side-b 2, :side-c 3}
user> (s/conform ::cuboid #::{:side-a 1
:side-b "2"
:side-c 3})
:clojure.spec.alpha/invalid

27.
[beta]
⼀致しない原因を調べる
user> (s/explain ::cuboid #::{:side-a 1
:side-b "2"
:side-c 3})
In: [:user/side-b] val: "2" fails spec: :user/length at: [:user/
side-b] predicate: number?
:clojure.spec.alpha/spec :user/cuboid
:clojure.spec.alpha/value #:user{:side-a 1, :side-b "2", :sidec 3}
nil

:user/side-b の値 "2" がspec :user/length
の述語 number? に不⼀致
マップのキーに対応するspecもチェックされる

28.
[beta]
user> (s/explain ::cuboid #::{:side-a 1
:side-d 2
:side-c 3})
val: #:user{:side-a 1, :side-d 2, :side-c 3} fails spec: :user/c
uboid predicate: (contains? % :user/side-b)
:clojure.spec.alpha/spec :user/cuboid
:clojure.spec.alpha/value #:user{:side-a 1, :side-d 2, :side-c
3}
nil

値 #:user{:side-a 1, :side-d 2, :side-c
3} がspec :user/cuboid の述語 (contains? %
:user/side-b) に不⼀致

29.
[beta]
user> (s/explain ::cuboid #::{:side-a 1
:side-c 3})
val: #:user{:side-a 1, :side-c 3} fails spec: :user/cuboid predi
cate: (contains? % :user/side-b)
:clojure.spec.alpha/spec :user/cuboid
:clojure.spec.alpha/value #:user{:side-a 1, :side-c 3}
nil

値 #:user{:side-a 1, :side-c 3} がspec
:user/cuboid の述語 (contains? %
:user/side-b) に不⼀致

30.
[beta]
直⽅体のサンプルを⽣成
user> (gen/sample (s/gen ::cuboid))
(#:user{:side-a 0.5, :side-b 1.0, :side-c 0.625} #:user{:side-a
0.5, :side-b 1.5, :side-c 2} #:user{:side-a 2.0, :side-b 4, :sid
e-c 1} #:user{:side-a 1.5, :side-b 1.0, :side-c 1} #:user{:sidea 0.5, :side-b 12, :side-c 1.125} #:user{:side-a 49, :side-b 0.3
59375, :side-c 0.765625} #:user{:side-a 2, :side-b 1, :side-c 1.
25} #:user{:side-a 3.0, :side-b 0.5, :side-c 2.0} #:user{:side-a
1.3125, :side-b 1.0, :side-c 1.0} #:user{:side-a 5.265625, :sid
e-b 1.0625, :side-c 2.73828125})

複合的なデータのサンプルも⽣成できる

32.

仕様を満たすように関数を実装 user> (defn cuboid-volume [{::keys [side-a side-b side-c]}] (* side-a side-b side-c)) #'user/cuboid-volume

33.

引数に対するチェックを有効化 stest/instrument user> (doc cuboid-volume) ------------------------user/cuboid-volume ([#:user{:keys [side-a side-b side-c]}]) Spec args: (cat :cuboid :user/cuboid) ret: number? nil user> (stest/instrument) [user/cuboid-volume]

34.

user> (cuboid-volume #::{:side-a 1 :side-b 2 :side-c 3}) 6 引数のspecを満たす値に適⽤すると期待した計算 結果が得られる

35.
[beta]
user> (cuboid-volume #::{:side-a 1
:side-b "2"
:side-c 3})
ExceptionInfo Call to #'user/cuboid-volume did not conform to sp
ec:
In: [0 :user/side-b] val: "2" fails spec: :user/length at: [:arg
s :cuboid :user/side-b] predicate: number?
:clojure.spec.alpha/spec #object[clojure.spec.alpha$regex_spec_
impl$reify__1200 0x57e771b6 "clojure.spec.alpha$regex_spec_impl$
reify__1200@57e771b6"]
:clojure.spec.alpha/value (#:user{:side-a 1, :side-b "2", :side
-c 3})
:clojure.spec.alpha/args (#:user{:side-a 1, :side-b "2", :sidec 3})
:clojure.spec.alpha/failure :instrument
:clojure.spec.test.alpha/caller {:file "form-init41134222269549
81451.clj", :line 188, :var-scope user/eval14130}
clojure.core/ex-info (core.clj:4744)

パス [0 :user/side-b] の値 "2" がspec
:user/length の述語 number? に不⼀致 ⇒ 例外

36.
[beta]
関数の動作確認
user> (s/exercise-fn `cuboid-volume)
([(#:user{:side-a 2.0, :side-b 0.5, :side-c 0.5}) 0.5] [(#:user{
:side-a 0.75, :side-b 1.5, :side-c 0.75}) 0.84375] [(#:user{:sid
e-a 2.0, :side-b 1, :side-c 1.75}) 3.5] [(#:user{:side-a 4, :sid
e-b 4, :side-c 2}) 32] [(#:user{:side-a 2, :side-b 6, :side-c 1.
0}) 12.0] [(#:user{:side-a 1, :side-b 3, :side-c 6}) 18] [(#:use
r{:side-a 20, :side-b 1.0, :side-c 99}) 1980.0] [(#:user{:side-a
12, :side-b 890, :side-c 1.25}) 13350.0] [(#:user{:side-a 4, :s
ide-b 0.99609375, :side-c 2.0}) 7.96875] [(#:user{:side-a 3, :si
de-b 6.0, :side-c 9}) 162.0])

s/exercise-fn で引数のspecを満たすランダム
な値で動作確認
結果はベクター [引数 戻り値] のシーケンス

37.
[beta]
⾃動プロパティベーストテスト
stest/check

user> (stest/check `cuboid-volume)
({:spec #object[clojure.spec.alpha$fspec_impl$reify__1215 0x765acd43 "c
:cause "integer overflow"
:via
[{:type java.lang.ArithmeticException
:message "integer overflow"
:at [clojure.lang.Numbers throwIntOverflow "Numbers.java" 1526]}]
:trace
[[clojure.lang.Numbers throwIntOverflow "Numbers.java" 1526]
[clojure.lang.Numbers multiply "Numbers.java" 1892]
[clojure.lang.Numbers$LongOps multiply "Numbers.java" 472]
[clojure.lang.Numbers multiply "Numbers.java" 148]
[user$cuboid_volume invokeStatic "form-init4113422226954981451.clj" 1
[user$cuboid_volume invoke "form-init4113422226954981451.clj" 155]
[clojure.lang.AFn applyToHelper "AFn.java" 154]
[clojure.lang.AFn applyTo "AFn.java" 144]
[clojure.core$apply invokeStatic "core.clj" 657]
[clojure.core$apply invoke "core.clj" 652]
[clojure.spec.test.alpha$check_call invokeStatic "alpha.clj" 292]

38.
[beta]
:smallest [(#:user{:side-a 1, :side-b
23021144, :side-c 400647858198})]
user> (cuboid-volume #::{:side-a 1
:side-b 23021144
:side-c 400647858198})
ArithmeticException integer overflow clojure.lang.Numbers.throw
IntOverflow (Numbers.java:1526)

ここでは
乗算でinteger overflowが発⽣しうることが判明

39.

user> (defn cuboid-volume [{::keys [side-a side-b side-c]}] (*' side-a side-b side-c)) #'user/cuboid-volume user> (cuboid-volume #::{:side-a 1 :side-b 23021144 :side-c 400647858198}) 9223372036867738512N integer overflowしないように関数 * を *' に変更

40.
[beta]
user> (stest/check `cuboid-volume)
({:spec #object[clojure.spec.alpha$fspec_impl$reify__1215 0x765a
cd43 "clojure.spec.alpha$fspec_impl$reify__1215@765acd43"], :clo
jure.spec.test.check/ret {:result true, :num-tests 1000, :seed 1
505803853835}, :sym user/cuboid-volume})
user> (stest/summarize-results *1)
{:sym user/cuboid-volume}
{:total 1, :check-passed 1}

デフォルト1000回の試⾏で正常にテストをパス

41.
[beta]
最終結果
(ns spec-examples.geometry
(:require [clojure.spec.alpha :as s]))
;; specs
(s/def ::length (s/and number? pos?))
(s/def ::side-a ::length)
(s/def ::side-b ::length)
(s/def ::side-c ::length)
(s/def ::cuboid (s/keys :req [::side-a ::side-b ::side-c])
(s/fdef cuboid-volume
:args (s/cat :cuboid ::cuboid)
:ret number?)
;; implementation
(defn cuboid-volume [{::keys [side-a side-b side-c]}]
(*' side-a side-b side-c))

42.

述語(predicate)で仕様が書ける 値に対する制約が柔軟に表現できる Clojureの動的な性質と親和性が⾮常に⾼い コンパイル時ではなく実⾏時 REPL駆動開発とプロパティベーストテストで制 約を満たしていることを保証する戦略 漸進的型付け/静的⾔語化とは異なる未来 ⾃動プロパティベーストテストが便利

43.

clojure.specを活⽤して 変更に強いClojureコードを書こう (*> ᴗ •*)ゞ

44.

Vive les S-expressions ! Long live S-expressions!

45.

Further Reading example code lagenorhynque/spec-examples clojure.spec vs core.typed vs schema

46.

of cial site clojure.spec - Rationale and Overview spec Guide clojure/clojure at clojure-1.9.0-beta1 clojure/spec.alpha clojure.spec - Clojure v1.9 API documentation clojure/core.specs.alpha clojure/core.typed plumatic/schema

47.

video Spec-ulation Keynote - Rich Hickey "Agility & Robustness: Clojure spec" by Stuart Halloway clojure.spec - David Nolen Clojure spec Screencast Series book Programming Clojure, Third Edition