Skip to content
This repository has been archived by the owner on Oct 7, 2024. It is now read-only.

Big refactoring to formalize internals #1

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
6 changes: 6 additions & 0 deletions .clj-kondo/config.edn
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{:lint-as
{matchete.core/defnpattern clojure.core/defn
matchete.core/defpattern clojure.core/def}
:linters
{:unresolved-symbol
{:exclude [(matchete.core/formula)]}}}
238 changes: 0 additions & 238 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,239 +1 @@
# matchete [![cljdoc badge](https://cljdoc.org/badge/io.xapix/matchete)](https://cljdoc.org/d/io.xapix/matchete/CURRENT) ![Check code style using clj-kondo](https://github.com/xapix-io/matchete/workflows/Check%20code%20style%20using%20clj-kondo/badge.svg?branch=master) ![Run tests for all environments](https://github.com/xapix-io/matchete/workflows/Run%20tests%20for%20all%20environments/badge.svg?branch=master)

Yet another pattern matching library for Clojure(Script).

## Using

<details><summary>leiningen</summary>
<p>

```
[io.xapix/matchete "1.1.0"]
```

</p>
</details>

<details><summary>boot</summary>
<p>

```
(set-env! :dependencies #(conj % [io.xapix/matchete "1.1.0"]))
```

</p>
</details>

<details><summary>deps.edn</summary>
<p>

```
{:deps {io.xapix/matchete {:mvn/version "1.1.0"}}}
```

</p>
</details>

```clojure
(require '[matchete.core :as m])

;; `match?` function returns true or false to indicate if the data matches the pattern
(m/match? '{:foo ?foo} {:foo 1}) ;; => true
(m/match? '{:bar ?bar} {:foo 1}) ;; => false

;; `matches` function returns lazy sequence with collected bindings
;; empty seq indicates not matched data
(m/matches '{:foo ?foo} {:foo 1}) ;; => '({?foo 1})
(m/matches '{:bar ?bar} {:foo 1}) ;; => '()

;; `matcher` function precompiles pattern into a function
(let [matcher (m/matcher '{:foo ?foo})]
(matcher {:foo 1})) ;; => '({?foo 1})
```

## Match data using data as a pattern

```clojure
(m/match? 42 42) ;; => true
(m/match? "42" "24") ;; => false

;; sequences
(m/match? [1 2 3] [1 2 3]) ;; => true
(m/match? '(1 2 3) [1 2 3]) ;; => true

(m/match? [1 2 3] [1 2 3 4]) ;; => false because pattern expects exactly 3 elements

;; to override this behaviour tail destructuring pattern can be used
(m/match? [1 2 3 & _] [1 2 3 4]) ;; => true, `_` is a placeholder here that will match to any provided data

;; hash-maps
(m/match? {:id 123 :name "Alise"} {:id 123 :name "Alise" :lastname "Cooper"}) ;; => true
(m/match? {:id 123 :name "Alise"} {:id 123 :lastname "Cooper"}) ;; => false because `:name` key is missing
```

## Extract data

There are three types of special symbols that can be used in a pattern:

* data bindings - symbols starts with '?'
* memo bindings - symbols starts with '!'
* named rule - symbols starts with '$'

### Data Binding

```clojure
(m/matches '?user {:id 1 :name "Bob"}) ;; => '({?user {:id 1 :name "Bob"}})
(m/matches '{:id ?user-id :name ?user-name}
{:id 1 :name "Bob"}) ;; => '({?user-id 1 ?user-name "Bob"})

(m/matches '[1 ?two 3 & [?four & _]]
[1 2 3 4 5 6]) ;; => '({?two 2 ?four 4})

(m/matches '{:vector [_ {:id ?id}]}
{:vector [{:id 1} {:id 2}]}) ;; => '({?id 2})
```

data bindings can be used as a hash-map keys

```clojure
(m/matches '{?key ?value}
{:foo "foo"
:bar "bar"}) ;; => '({?key :foo ?value "foo"} {?key :bar ?value "bar"})

(m/matches '{?x "foo"
?y "bar"}
{:key-1 "foo"
:key-2 "foo"
:key-3 "bar"}) ;; => '({?x :key-1 ?y :key-3} {?x :key-2 ?y :key-3})
```

### Memo Binding

Collect data into a vector. Order of appearence is not guaranteed.

```clojure
(m/matches '[{:id !ids} {:id !ids} {:id !ids}]
[{:id 1} {:id 2} {:id 3}]) ;; => '({!ids [1 2 3]})
```

## Control sequences

### `not!` predicate

```
(m/matches '{:id (cat (not! 10) ?id)
:name ?name}
{:id 42
:name "Alise"} ;; => '({?id 42 ?name "Alise"})
;; {:id 10
;; :name "Bob"} ;; => '()
)
```

### `cat` combinator

Each pattern will be applied to the same data combine data bindings into one result. Patterns can extend the result or add more sofisticated restrictions.

```clojure
(m/matches '(cat {:id ?id :name ?name} ?user)
{:id 1
:name "Alise"
:lastname "Cooper"}) ;; => '({?id 1 ?name "Alise" ?user {:id 1 :name "Alise" :lastname "Cooper"}})
```

### `alt` combinator

Patterns combined by `alt` will be applied to the same data as long as one of them will match and the matches from that pattern will be the result of matching.

```clojure
(m/matches '(alt {:id ?id} {"id" ?id} {:userId ?id})
{:id 1} ;; => '({?id 1})
;; {"id" 2} ;; => '({?id 2})
;; {:userId 3} ;; => '({?id 3})
)
```

### `each`

#### `(each P)`

Sequentialy match elements of collection in order. Fail if any of elements can not match.

```clojure
(m/matches '(every (and %string? !elements))
["qwe" "rty" "uio"]) ;; => '({!elements ["qwe" "rty" "uio"]})

(m/matches '(every (and %string? !elements))
["qwe" 42 "uio"]) ;; => '()
```

#### `(each index-P value-P)`

2-arity version of `each` where first pattern will match against an index and second - match against value associated with that index.

### `scan`

#### `(scan P)`

Expects one pattern wich will be applied to each item of sequence or hash-map (item will be in the form of tuple: [key, value]).

```clojure
(m/matches '{:foo (scan [?id ?name])}
{:foo [[1 "Alise"] [::empty] [3 "Bob"]]}) ;; => '({?id 1 ?name "Alise"} {?id 3 ?name "Bob"})'
```

#### `(scan index-P value-P)`

Expects two patterns:

1. index matcher (index in sequences and key in hash-maps)
1. value matcher

```clojure
(m/matches '(scan !path (scan !path (scan !path ?node)))
[{:id 1
:user {:name "Alise"
:role :admin}
:actions [{:type :login}]}])
;; => '({!path [0 :user :name] ?node "Alise"}
;; {!path [0 :user :role] ?node :admin}
;; {!path [0 :actions 0] ?node {:type :login}})
```

### Named rule

```clojure
(m/matches '(def-rule $children (scan !path (alt $children ?leaf)))
[{:id 1
:user {:name "Alise"
:role :admin}
:actions [{:type :login}]}])
;; => '({!path [0 :id] ?leaf 1}
;; {!path [0 :user :name] ?leaf "Alise"}
;; {!path [0 :user :role] ?leaf :admin}
;; {!path [0 :actions 0 :type] ?leaf :login})

;; rules can be precompiled
(let [rules {'$children (m/matcher '(scan !path (alt $children ?leaf)))}]
(m/matches '$children rules
[{:id 1
:user {:name "Alise"
:role :admin}
:actions [{:type :login}]}]))
;; => '({!path [0 :id] ?leaf 1}
;; {!path [0 :user :name] ?leaf "Alise"}
;; {!path [0 :user :role] ?leaf :admin}
;; {!path [0 :actions 0 :type] ?leaf :login})
```

Rules can work as predicates:

```clojure
(def rules
{'$string? (fn [matches _ s]
(when (string? s)
(list matches)))})

(m/match? '(each $string?) rules ["qwe" "rty" "uio"]) ;; => true
(m/match? '(each $string?) rules ["qwe" 42 "uio"]) ;; => false
```
7 changes: 5 additions & 2 deletions deps.edn
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{:paths ["src"]

:deps {org.clojure/math.combinatorics {:mvn/version "0.1.6"}}
:deps {org.clojure/math.combinatorics {:mvn/version "0.1.6"}
borkdude/sci {:mvn/version "0.1.1-alpha.1"}}

:aliases
{:+test {:extra-paths ["test"]
Expand All @@ -9,6 +10,8 @@
lambdaisland/kaocha-cljs {:mvn/version "0.0-71"}}}

:+dev {:extra-paths ["dev"]
:extra-deps {criterium {:mvn/version "0.4.5"}}}
:extra-deps {criterium {:mvn/version "0.4.5"}
meander/epsilon {:mvn/version "0.0.421"}
cheshire {:mvn/version "5.10.0"}}}

:+cljs {:extra-deps {org.clojure/clojurescript {:mvn/version "1.10.764"}}}}}
39 changes: 19 additions & 20 deletions dev/example/graph.cljc
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
(ns example.graph
(:require [matchete.core :as m]))
(:require [matchete.core :as ml]))

(def city-to-city-distance
#{["Berlin" #{["New York" 14] ["London" 2] ["Tokyo" 14] ["Vancouver" 13]}]
Expand All @@ -8,33 +8,32 @@
["Tokyo" #{["Berlin" 14] ["New York" 18] ["London" 15] ["Vancouver" 12]}]
["Vancouver" #{["Berlin" 13] ["New York" 6] ["London" 10] ["Tokyo" 12]}]})

;; generates pattern like this:
;; '#{[(cat ?0 !path) #{[?1 $sum]}]
;; [(cat ?1 !path) #{[?2 $sum]}]
;; [(cat ?2 !path) #{[?3 $sum]}]
;; [(cat ?3 !path) #{[?4 $sum]}]
;; [(cat ?4 !path) #{[?0 $sum]}]}
(defn add-distance [distance path]
(+ (or distance 0) path))

(defn generate-pattern [cities-count]
(defn generate-matcher [cities-count]
(let [l (range cities-count)]
(into #{}
(map (fn [[n1 n2]]
[(list 'cat (symbol (str "?" n1)) '!path)
#{[(symbol (str "?" n2)) '$sum]}]))
[(symbol (str "?" n1))
#{[(symbol (str "?" n2)) (ml/aggregate-by add-distance '?distance)]}]))
(take cities-count (map vector (cycle l) (rest (cycle l)))))))

(defn shortest-path
{:test #(do
(assert
(= 46 (first (shortest-path city-to-city-distance)))))}
[db]
(let [{:syms [?distance !path]}
(= 46 (shortest-path city-to-city-distance "Berlin"))))}
[db start]
(let [{:syms [?distance]}
(first
(sort-by #(get % '?distance)
(m/matches (generate-pattern (count db))
{'?0 (ffirst db)}
;; Let's use rule as a reduce to calculate a distance walked so far
{'$sum (fn [matches _rules data]
(list (update matches '?distance (fnil + 0) data)))}
db)))]
[?distance !path]))
(ml/matches (generate-matcher (count db))
{'?0 start}
db)))]
?distance))

(comment

(shortest-path city-to-city-distance "Berlin")

)
Loading