Slide 1

Slide 1 text

Specifying Other People’s Data Structures with Spec An Experience Report Clojure/NYC July 2018 Avi Flax [email protected]

Slide 2

Slide 2 text

This is really just an experience report: Iʼm going to share my experience trying to solve a problem with spec, show what I struggled with, what I tried that didnʼt work, and my final solution that did work. Experience Report

Slide 3

Slide 3 text

Introduction to Spec

Slide 4

Slide 4 text

Whatʼs itʼs goal? A Library

Slide 5

Slide 5 text

* As per Rich Hickey: * * Thatʼs how Rich Hickey describes it... “Clojure has no standard, expressive, powerful and integrated system for specification and testing. clojure.spec aims to provide it.” — Rich Hickey, clojure.spec - Rationale and Overview

Slide 6

Slide 6 text

* This is how I tend to describe it, when someone makes the mistake of asking me what Iʼm excited about in programming. * * So: itʼs Clojureʼs answer to creating large, maintainable, sustainable systems with a lot of code while retaining dynamicity * I.E. Clojureʼs alternative to types * Not a radically original idea * Hickey: “Almost nothing about spec is novel.” * Prior art: Schema, Herbert, contracts, RDF, schemas in general * Thatʼs all the background Iʼll do right now; hopefully this is enough that the code weʼll look at will make some sense. * But if anything doesnʼt make sense, please shout and weʼll fix that. “An excellent tool for specifying and validating data structures, and testing and documenting functions.” — me

Slide 7

Slide 7 text

My Problem

Slide 8

Slide 8 text

explain this { "elements": [ { "type": "Software System", "name": "Uberwriter", "position": "700,100" } ], "styles": [ { "type": "element", "tag": "Database", "shape": "Cylinder" } ] }

Slide 9

Slide 9 text

* Alternate view * For all its flaws, YAML is easy for humans to read * My goal: specify this data structure with spec elements: - type: Software System name: Uberwriter position: '700,100' styles: - type: element tag: Database shape: Cylinder

Slide 10

Slide 10 text

* Biggest challenge: type * To a neophyte, itʼs perplexing how to specify these different data structures that both use type elements: - type: Software System name: Uberwriter position: '700,100' styles: - type: element tag: Database shape: Cylinder

Slide 11

Slide 11 text

* Why is this perplexing? * It starts with the fact that one of specʼs strongly-held opinions (it has many) is that map keys should be namespaced and should be specified independently of the maps theyʼre used in Why?

Slide 12

Slide 12 text

“Map specs should be of keysets only” — One of the problem statements in the spec rationale

Slide 13

Slide 13 text

“Most systems for specifying structures conflate the specification of the key set (e.g. of keys in a map, fields in an object) with the specification of the values designated by those keys.” — from the explanation of that problem statement

Slide 14

Slide 14 text

This is clearer. “Decomplect maps/keys/values” — one of the stated objectives of spec

Slide 15

Slide 15 text

“Keep map (keyset) specs separate from attribute (key→value) specs.” — from the explanation of that objective

Slide 16

Slide 16 text

Why?

Slide 17

Slide 17 text

OK? Now time for some code “Sets (maps) are about membership, that’s it... This is vital for composition and dynamicity.” — one of the guidelines in the spec overview

Slide 18

Slide 18 text

* Most spec tutorials & guides show examples like this * All well and good * Letʼs discuss double-colons for a minute (ns hud.catalog) (s/def !"coords string?) (s/def !"description string?) (s/def !"building (s/keys :req [!"coords !"description]))

Slide 19

Slide 19 text

* So :: syntax is nice * and super convenient * but not always (ns hud.catalog) (s/def !"coords string?) == (ns hud.catalog) (s/def :hud.catalog/coords string?)

Slide 20

Slide 20 text

The latter specs would clobber the earlier ones Wouldn’t work: ; element (s/def !"type !#"Person" "Software System"}) ; style (s/def !"type !#"element" "tag" "relationship})

Slide 21

Slide 21 text

Attempt #1

Slide 22

Slide 22 text

Multiple namespaces, one file (ns fc4c.spec) (s/def !"name string?) (ns fc4c.spec.element (:require [fc4c.spec :as fs])) (s/def !"type !#"Person" "Software System"}) (s/def !"element (s/keys :req [!"fs/name !"type !!$])) (ns fc4c.spec.style (:require [fc4c.spec :as fs])) (s/def !"type !#"element" "relationship"}) (s/def !"style (s/keys :req [!"fs/name !"type !!$]) (ns fc4c.spec.diagram (:require [!!$])) (s/def !"type !#"System Landscape" "System Context" "Container"}) (s/def !"elements (s/coll-of !"fse/element)) (s/def !"styles (s/coll-of !"fss/style)) (s/def !"diagram (s/keys :req [!"elements !"styles !!$]))

Slide 23

Slide 23 text

I thought: this is exactly what namespaces are for, right? To prevent conflicts of things that are named similarly but have different semantics. (ns fc4c.spec) (s/def !"name string?) (ns fc4c.spec.element (:require [fc4c.spec :as fs])) (s/def !"type !#"Person" "Software System"}) (s/def !"element (s/keys :req [!"fs/name !"type !!$])) (ns fc4c.spec.style (:require [fc4c.spec :as fs])) (s/def !"type !#"element" "relationship"}) (s/def !"style (s/keys :req [!"fs/name !"type !!$]) (ns fc4c.spec.diagram (:require [!!$])) (s/def !"type !#"System Landscape" "System Context" "Container"}) (s/def !"elements (s/coll-of !"fse/element)) (s/def !"styles (s/coll-of !"fss/style)) (s/def !"diagram (s/keys :req [!"elements !"styles !!$]))

Slide 24

Slide 24 text

* All in one file! * Seemed to work! * But: ** Convoluted, verbose ** Non-idiomatic to have multi namespaces one file (ns fc4c.spec) (s/def !"name string?) (ns fc4c.spec.element (:require [fc4c.spec :as fs])) (s/def !"type !#"Person" "Software System"}) (s/def !"element (s/keys :req [!"fs/name !"type !!$])) (ns fc4c.spec.style (:require [fc4c.spec :as fs])) (s/def !"type !#"element" "relationship"}) (s/def !"style (s/keys :req [!"fs/name !"type !!$]) (ns fc4c.spec.diagram (:require [!!$])) (s/def !"type !#"System Landscape" "System Context" "Container"}) (s/def !"elements (s/coll-of !"fse/element)) (s/def !"styles (s/coll-of !"fss/style)) (s/def !"diagram (s/keys :req [!"elements !"styles !!$]))

Slide 25

Slide 25 text

Attempt #2

Slide 26

Slide 26 text

* A little better * Only 1 Clojure ns in file * Explicit namespaces — no more :: * One odd thing: copying specs from one ns to another: (ns fc4c.spec) (s/def :fc4c/name string?) !" elements! (s/def :fc4c.element/name :fc4c/name) (s/def :fc4c.element/type !#"Person" "Software System"}) (s/def :fc4c/element (s/keys :req [:fc4c.element/name :fc4c.element/type !!$])) !" styles! (s/def :fc4c.style/name :fc4c/name) (s/def :fc4c.style/type !#"element" "relationship"}) (s/def :fc4c/style (s/keys :req [:fc4c.style/name :fc4c.style/type !!$])) !" diagram! (s/def :fc4c.diagram/type !#"System Landscape" "System Context" "Container"}) (s/def :fc4c.diagram/elements (s/coll-of :fc4c/element)) (s/def :fc4c.diagram/styles (s/coll-of :fc4c/style)) (s/def :fc4c/diagram (s/keys :req [:fc4c.diagram/type :fc4c.diagram/elements :fc4c.diagram/styles !!$]))

Slide 27

Slide 27 text

Explain why... then show (ns fc4c.spec) (s/def :fc4c/name string?) !" elements! (s/def :fc4c.element/name :fc4c/name) (s/def :fc4c.element/type !#"Person" "Software System"}) (s/def :fc4c/element (s/keys :req [:fc4c.element/name :fc4c.element/type !!$])) !" styles! (s/def :fc4c.style/name :fc4c/name) (s/def :fc4c.style/type !#"element" "relationship"}) (s/def :fc4c/style (s/keys :req [:fc4c.style/name :fc4c.style/type !!$])) !" diagram! (s/def :fc4c.diagram/type !#"System Landscape" "System Context" "Container"}) (s/def :fc4c.diagram/elements (s/coll-of :fc4c/element)) (s/def :fc4c.diagram/styles (s/coll-of :fc4c/style)) (s/def :fc4c/diagram (s/keys :req [:fc4c.diagram/type :fc4c.diagram/elements :fc4c.diagram/styles !!$]))

Slide 28

Slide 28 text

This is what I get when I parse one of these YAML files {:elements [{:type "Software System" :name "Uberwriter" :description "UI for Underwriters" :position "700,100"}] :styles [{:type "element" :tag "Database" :shape "Cylinder"}]}

Slide 29

Slide 29 text

* I thought I was going to need to add a namespace to every key in this map * in order for the map to be valid according to my spec * So itʼd look something like this * But when I imagined writing the code that would assign the right namespace to the right key, it felt like a disaster * So that's why I copied the specs; so instead I could do this: {:fc4c.spec/elements [{:fc4c.element/type "Software System" :fc4c.spec/name "Uberwriter" :fc4c.element/position "700,100"}] :fc4c.spec/styles [{:fc4c.style/type "element" :fc4c.spec/tag "Database" :fc4c.style/shape "Cylinder"}]}

Slide 30

Slide 30 text

* I was thinking that when I added the namespaces like this, itʼd be easy, because I could add the same namespace to every key in any given kind of map {:fc4c.spec/elements [{:fc4c.element/type "Software System" :fc4c.element/name "Uberwriter" :fc4c.element/position "700,100"}] :fc4c.spec/styles [{:fc4c.style/type "element" :fc4c.style/tag "Database" :fc4c.style/shape "Cylinder"}]}

Slide 31

Slide 31 text

* Well it turns out that was silly, because spec includes a way to specify a map that contains unqualified keys:

Slide 32

Slide 32 text

(s/def :structurizr/style (s/keys :req [:structurizr.style/type :structurizr.style/tag] :opt [:structurizr.style/color :structurizr.style/shape])

Slide 33

Slide 33 text

(s/def :structurizr/style (s/keys :req-un [:structurizr.style/type :structurizr.style/tag] :opt-un [:structurizr.style/color :structurizr.style/shape])

Slide 34

Slide 34 text

Attempt #3

Slide 35

Slide 35 text

(ns fc4c.spec) (s/def :fc4c/name string?) !" elements! (s/def :fc4c.element/type !#"Person" "Software System"}) (s/def :fc4c/element (s/keys :req-un [:fc4c/name :fc4c.element/type !!%])) !" styles! (s/def :fc4c.style/type !#"element" "relationship"}) (s/def :fc4c/style (s/keys :req-un [:fc4c/name :fc4c.style/type !!%])) !" diagram! (s/def :fc4c.diagram/type !#"System Landscape" "System Context" "Container"}) (s/def :fc4c.diagram/elements (s/coll-of :fc4c/element)) (s/def :fc4c.diagram/styles (s/coll-of :fc4c/style)) (s/def :fc4c/diagram (s/keys :req-un [:fc4c.diagram/type :fc4c.diagram/elements :fc4c.diagram/styles !!%]))

Slide 36

Slide 36 text

* Composition! * Validation! * Generation! * No post-processing! (ns fc4c.spec) (s/def :fc4c/name string?) !" elements! (s/def :fc4c.element/type !#"Person" "Software System"}) (s/def :fc4c/element (s/keys :req-un [:fc4c/name :fc4c.element/type !!%])) !" styles! (s/def :fc4c.style/type !#"element" "relationship"}) (s/def :fc4c/style (s/keys :req-un [:fc4c/name :fc4c.style/type !!%])) !" diagram! (s/def :fc4c.diagram/type !#"System Landscape" "System Context" "Container"}) (s/def :fc4c.diagram/elements (s/coll-of :fc4c/element)) (s/def :fc4c.diagram/styles (s/coll-of :fc4c/style)) (s/def :fc4c/diagram (s/keys :req-un [:fc4c.diagram/type :fc4c.diagram/elements :fc4c.diagram/styles !!%]))

Slide 37

Slide 37 text

End result: a spec that thoroughly specifies a diagram data structure, using straightforward composition (s/def :fc4c/diagram (s/keys :req-un [:fc4c.diagram/type :fc4c.diagram/scope :fc4c/description :fc4c.diagram/elements :fc4c.diagram/relationships :fc4c.diagram/styles :fc4c.diagram/size]))

Slide 38

Slide 38 text

Demo Time!

Slide 39

Slide 39 text

Just a refinement I devised to improve the readability of my specs Refinement

Slide 40

Slide 40 text

* I find this really hard to read — lots of repetitive, verbose characters. * If you recall, I stopped using double- colons because I wanted all my specs in a single file, and I didnʼt want to define multiple Clojure namespaces in a single file, mostly because itʼs non- idiomatic. (s/def :structurizr.relationship/source :structurizr/name) (s/def :structurizr.relationship/destination :structurizr/name) (s/def :structurizr.relationship/order :structurizr/int-in-string) (s/def :structurizr.relationship/vertices (s/coll-of :structurizr/position :min-count 1)) (s/def :structurizr/relationship (s/keys :req-un [:structurizr.relationship/source :structurizr.relationship/destination] :opt-un [:structurizr/description :structurizr/tags]))

Slide 41

Slide 41 text

* Then I made this function namespaces that Iʼm invoking at the top of my file — it creates a bunch of Clojure namespaces and an aliases to each. * So my specs can now look like this: (namespaces '[structurizr :as st] '[structurizr.container :as sc] '[structurizr.element :as se] '[structurizr.relationship :as sr] '[structurizr.style :as ss] '[structurizr.diagram :as sd])

Slide 42

Slide 42 text

* This is a big improvement * Iʼm very happy with this * Turns out I donʼt actually mind creating multiple Clojure namespaces, just to misuse them. * I just didnʼt want to have multiple ns forms in a single file, because that would be super strange and confusing to new maintainers opening the file for the first time. (s/def !"sr/source !"st/name) (s/def !"sr/destination !"st/name) (s/def !"sr/order !"st/int-in-string) (s/def !"sr/vertices (s/coll-of !"st/position :min-count 1)) (s/def !"st/relationship (s/keys :req-un [!"sr/source !"sr/destination] :opt-un [!"st/description !"st/tags]))

Slide 43

Slide 43 text

The End?

Slide 44

Slide 44 text

This is a little embarrasing, but after all this, I finally stumbled across this section of the spec guide — and actually registered it, apparently for the first time: Postscript

Slide 45

Slide 45 text

* So yeah, this might be the solution to all of this — Iʼm not sure. * I dabbled with it a bit and it was non-obvious how to apply it to my situation

Slide 46

Slide 46 text

The End! Discussion Time!