• This repository has been archived on 16/Mar/2019
  • Stars
    star
    212
  • Rank 180,136 (Top 4 %)
  • Language
    Clojure
  • Created over 11 years ago
  • Updated over 10 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

A library for doing stuff to other stuff.

"At some point as a programmer you might have the insight/fear that all programming is just doing stuff to other stuff."

In idiomatic clojure data is not hidden behind classes and methods, but instead left lying around in a homogenous heap of stuff. Assumptions about the shape of stuff are implicitly encoded in the functions used to operate on it. When your stuff is the wrong shape things blow up far down the line in an unhelpful fashion.

(defn f [{:keys [x y] :as z}]
      [x y z])

(f {:x 1 :y 2})
;; [1 2 {:x 1 :y 2}]

(f nil)
;; [nil nil nil]

(f (list 1 2 3 4))
;; [nil nil {1 2 3 4}]

Strucjure is a library for describing stuff in an executable manner. It gives you pattern matching (with first-class patterns), validators, parsers, walks and lenses (and eventually generators). The shape of your data is immediately apparent from your code and errors are clearly reported.

(require '[strucjure.sugar :as s :refer [_]])

(defn g [input]
  (s/match input
         ^z (s/keys x y) [x y z]))

(g {:x 1 :y 2})
;; [1 2 {:x 1 :y 2}]

(g nil)
;; strucjure.view.Failure:
;; Failed test (clojure.core/map? input6214) in pattern {:x #strucjure.pattern.Name{:name x, :pattern #strucjure.pattern.Any{}}, :y #strucjure.pattern.Name{:name y, :pattern #strucjure.pattern.Any{}}} on input nil

(g (list 1 2 3 4))
;; strucjure.view.Failure:
;; Failed test (clojure.core/map? input6214) in pattern {:x #strucjure.pattern.Name{:name x, :pattern #strucjure.pattern.Any{}}, :y #strucjure.pattern.Name{:name y, :pattern #strucjure.pattern.Any{}}} on input (1 2 3 4)

And the whole library is well under 500 loc.

Concision

Pattern matching tends to be far more concise than imperative style chains of boolean tests which we still use in clojure every day.

Compare the imperative approach...

private void adjustAfterInsertion(Node n) {
        // Step 1: color the node red
        setColor(n, Color.red);

        // Step 2: Correct double red problems, if they exist
        if (n != null && n != root && isRed(parentOf(n))) {

            // Step 2a (simplest): Recolor, and move up to see if more work
            // needed
            if (isRed(siblingOf(parentOf(n)))) {
                setColor(parentOf(n), Color.black);
                setColor(siblingOf(parentOf(n)), Color.black);
                setColor(grandparentOf(n), Color.red);
                adjustAfterInsertion(grandparentOf(n));
            }

            // Step 2b: Restructure for a parent who is the left child of the
            // grandparent. This will require a single right rotation if n is
            // also
            // a left child, or a left-right rotation otherwise.
            else if (parentOf(n) == leftOf(grandparentOf(n))) {
                if (n == rightOf(parentOf(n))) {
                    rotateLeft(n = parentOf(n));
                }
                setColor(parentOf(n), Color.black);
                setColor(grandparentOf(n), Color.red);
                rotateRight(grandparentOf(n));
            }

            // Step 2c: Restructure for a parent who is the right child of the
            // grandparent. This will require a single left rotation if n is
            // also
            // a right child, or a right-left rotation otherwise.
            else if (parentOf(n) == rightOf(grandparentOf(n))) {
                if (n == leftOf(parentOf(n))) {
                    rotateRight(n = parentOf(n));
                }
                setColor(parentOf(n), Color.black);
                setColor(grandparentOf(n), Color.red);
                rotateLeft(grandparentOf(n));
            }
        }

        // Step 3: Color the root black
        setColor((Node) root, Color.black);
    }

...to the declarative approach.

(defrecord Red [value left right])
(defrecord Black [value left right])

(defn balance [tree]
  (s/match tree
         (s/or
          (Black. ^z _ (Red. ^y _ (Red. ^x _ ^a _ ^b _) ^c _) ^d _)
          (Black. ^z _ (Red. ^x _ ^a _ (Red. ^y _ ^b _ ^c _)) ^d _)
          (Black. ^x _ ^a _ (Red. ^z _ (Red. ^y _ ^b _ ^c _) ^d _))
          (Black. ^x _ ^a _ (Red. ^y _ ^b _ (Red. ^z _ ^c _ ^d _))))
         (Red. y (Black. x a b) (Black. z c d))

         ^other _
         other))

First-class patterns

Patterns in strucjure are first-class. The pattern part of the match statement is not a special langauge but just clojure code that is evaluated at compile-time and returns an instance of the Pattern and View protocols. This means you can easily extend the pattern language.

(match {:a 1 :b 2}
       {:a ^a _ :b ^b _} [a b])

;; too verbose, let's fix it

(defn my-keys* [symbols]
  (for-map [symbol symbols]
           (keyword (str symbol))
           (s/name symbol _)))

(defmacro my-keys [& symbols]
  `(my-keys* '~symbols)))

(s/match {:a 1 :b 2}
       (my-keys a b) [a b])

Even the recursive patterns used in parsing are first-class data structures which can be modified and composed.

(def expr
  (s/letp [num (s/or succ zero)
           succ (s/case ['succ num] (inc num))
           zero (s/case 'zero 0)
           expr (s/or num add)
           add (s/case ['add ^a expr ^b expr] (+ a b))]
          expr))

(match '(add (succ zero) (succ zero))
       ^result expr result)
;; 2

(def expr-with-sub
  (-> expr
      (update-in [:refers 'expr] #(s/or % (->Refer 'sub)))
      (assoc-in [:refers 'sub] (s/case ['sub ^a expr ^b expr] (- a b)))))

(s/match '(sub (add (succ zero) (succ zero)) (succ zero))
       ^result expr-with-sub result)
;; 1

Error reporting

The errors produced by failing matches contain a list of every point at which the match backtracked (in reverse order).

(s/match [1 2 3]
         [1 2] :nope
         [1 2 3 4] :nope
         [1 :x] :oh-noes)
;; strucjure.view.Failure:
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern :x on input 2
;; Failed test (clojure.core/not (clojure.core/nil? input6214)) in pattern 4 on input nil
;; Failed test (clojure.core/nil? input6214) in pattern nil on input (3)

(s/match '(add (sub (succ zero) (succ zero)) (succ zero))
         ^result expr result)
;; strucjure.view.Failure:
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern add on input sub
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (sub (succ zero) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input sub
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (sub (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add

If that isn't enough to locate the failure you can also run the match with tracing enabled:

(with-out-str
  (match-with trace-let '(add (add (succ zero) (succ zero)) (succ zero))
              expr expr))
;;  => expr (add (add (succ zero) (succ zero)) (succ zero))
;;    => num (add (add (succ zero) (succ zero)) (succ zero))
;;     => succ (add (add (succ zero) (succ zero)) (succ zero))
;;     XX succ #<Failure strucjure.view.Failure:
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;     => zero (add (add (succ zero) (succ zero)) (succ zero))
;;     XX zero #<Failure strucjure.view.Failure:
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (add (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;    XX num #<Failure strucjure.view.Failure:
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (add (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;    => add (add (add (succ zero) (succ zero)) (succ zero))
;;     => expr (add (succ zero) (succ zero))
;;      => num (add (succ zero) (succ zero))
;;       => succ (add (succ zero) (succ zero))
;;       XX succ #<Failure strucjure.view.Failure:
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (add (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;       => zero (add (succ zero) (succ zero))
;;       XX zero #<Failure strucjure.view.Failure:
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (succ zero) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (add (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;      XX num #<Failure strucjure.view.Failure:
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (succ zero) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (add (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;      => add (add (succ zero) (succ zero))
;;       => expr (succ zero)
;;        => num (succ zero)
;;         => succ (succ zero)
;;          => num zero
;;           => succ zero
;;           XX succ #<Failure strucjure.view.Failure:
;; Failed test (strucjure.view/seqable? input6214) in pattern [succ #strucjure.pattern.Name{:name num, :pattern #strucjure.pattern.Refer{:name num}}] on input zero
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (succ zero) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (add (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;           => zero zero
;;           <= zero 0
;;          <= num 0
;;         <= succ 1
;;        <= num 1
;;       <= expr 1
;;       => expr (succ zero)
;;        => num (succ zero)
;;         => succ (succ zero)
;;          => num zero
;;           => succ zero
;;           XX succ #<Failure strucjure.view.Failure:
;; Failed test (strucjure.view/seqable? input6214) in pattern [succ #strucjure.pattern.Name{:name num, :pattern #strucjure.pattern.Refer{:name num}}] on input zero
;; Failed test (strucjure.view/seqable? input6214) in pattern [succ #strucjure.pattern.Name{:name num, :pattern #strucjure.pattern.Refer{:name num}}] on input zero
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (succ zero) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (add (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;           => zero zero
;;           <= zero 0
;;          <= num 0
;;         <= succ 1
;;        <= num 1
;;       <= expr 1
;;      <= add 2
;;     <= expr 2
;;     => expr (succ zero)
;;      => num (succ zero)
;;       => succ (succ zero)
;;        => num zero
;;         => succ zero
;;         XX succ #<Failure strucjure.view.Failure:
;; Failed test (strucjure.view/seqable? input6214) in pattern [succ #strucjure.pattern.Name{:name num, :pattern #strucjure.pattern.Refer{:name num}}] on input zero
;; Failed test (strucjure.view/seqable? input6214) in pattern [succ #strucjure.pattern.Name{:name num, :pattern #strucjure.pattern.Refer{:name num}}] on input zero
;; Failed test (strucjure.view/seqable? input6214) in pattern [succ #strucjure.pattern.Name{:name num, :pattern #strucjure.pattern.Refer{:name num}}] on input zero
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (succ zero) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern zero on input (add (add (succ zero) (succ zero)) (succ zero))
;; Failed test (clojure.core/= literal__6312__auto__ input6214) in pattern succ on input add>
;;         => zero zero
;;         <= zero 0
;;        <= num 0
;;       <= succ 1
;;      <= num 1
;;     <= expr 1
;;    <= add 3
;;   <= expr 3

Performance

The aim for the 1.0 release is for every match to execute at least as fast as the equivalent idiomatic clojure code.

(= {:a 1 :b 2}
   {:a 1 :b 2})
;; 173 ns

(let [{:keys [a b]} {:a 1 :b 2}]
  (and (= a 1) (= b 2)))
;; 464 ns (really?)

(match {:a 1 :b 2}
       {:a 1 :b 2} :ok)
;; 159 ns

Binding variables in a match is currently expensive relative to normal clojure destructuring (due to using proteus.Container$0 to fake mutable variables).

(let [{:keys [a b]} {:a 1 :b 2}]
  [a b])
;; 123 ns

(s/match {:a 1 :b 2}
         (s/keys a b) [a b])
;; 648 ns :(

Other performance disparities are less clear.

(defn f [pairs]
  (if-let [[x y & more] pairs]
    (cons (clojure.core/+ x y) (f more))
    nil))

(f (range 10))
;; 3.5 us

(defn g [pairs]
  (match pairs
         [^x _ ^y _ ^more (& _)] (cons (clojure.core/+ x y) (g more))
         [] nil))

(g (range 10))
;; 7.1 us

(defn h [pairs]
  (match pairs
         (letp [p (case [^x _ ^y _ (& p)] (cons (clojure.core/+ x y) p)
                        [] nil)]
               p)))

(h (range 10))
;; 9.7 Β΅s

Nethertheless, there is no reason why pattern matching shouldn't eventually be faster, especially since they allow complex parsing with only a single pass over the input data.

More

See the tests for detailed examples of the various patterns available.

License

Distributed under the GNU Lesser General Public License.

More Repositories

1

dida

Differential dataflow for mere mortals
Zig
507
star
2

imp

Various experiments in relational programming
Zig
270
star
3

focus

Minimalist text editor
Zig
228
star
4

jams

Zig
182
star
5

springer-recommendations

'People who downloaded this paper also downloaded...'
Python
51
star
6

concerto

Multiplayer clojure!
Clojure
28
star
7

erl-telehash

TeleHash is a p2p overlay that aims to remove the pain from developing p2p applications
Erlang
22
star
8

streaming-consistency

Demonstrations of (in)consistency in various streaming systems.
Java
21
star
9

texsearch

A search index specialised for LaTeX equations. Developed for latexsearch.com.
OCaml
16
star
10

hugo-a-go-go

A go ai for the 2013 clojure cup
Clojure
11
star
11

mutant

Zig
9
star
12

preimp

Zig
8
star
13

JuliaCon2018

JavaScript
5
star
14

dissertation

My MSc dissertation 'Design and Analysis of a Gossip Algorithm'
Python
5
star
15

bounded-live-eval

JavaScript
4
star
16

rust-tagless

Rust
4
star
17

monolog

Clojure
3
star
18

neon-dom

A minimal example showing how to manipulate the dom from neon in electron.
Rust
3
star
19

scampy

Scampy is an email chatbot designed to engage 419 scammers and distract their attention from real victims.
Python
3
star
20

relational-crdts

Julia
2
star
21

droplet

Datalog in time and space - see http://www.bloom-lang.net
Clojure
2
star
22

binmap

Compact bitmaps based on https://github.com/gritzko/swift/blob/master/doc/binmaps-alenex.pdf
OCaml
2
star
23

understanding-software-dynamics

HTML
1
star
24

jamii.github.com

HTML
1
star
25

shackles

An extensible constraint solver supporting both finite-domain and logic constraints. Just a toy - use core.logic instead
Clojure
1
star
26

bigcheck.rs

Simple quickcheck clone
Rust
1
star