Giter Club home page Giter Club logo

automat's Introduction

Automat is a Clojure and ClojureScript library for defining and using finite-state automata, inspired by Ragel. However, instead of defining a DSL, it allows them to be built using simple composition of functions.

These automata, once compiled, are quite fast. An array with 100 million elements can be processed in 500ms, giving a mean transition time of 5ns. However, Automat isn't just for high throughput use cases; it's meant to be useful wherever an FSM is necessary.

[automat "0.2.4"]

Full documentation can be found here.

defining an FSM

A finite-state machine or finite-state automaton is defined as a series of states and transitions between these states, driven by a sequence of inputs. The automaton begins at a start state, and proceeds through the transitions until it reaches an accept state. If given an input that isn't a valid transition, the automaton may either reject the input sequence or reset to the beginning, depending on the use case.

In Automat, the simplest automaton is simply a vector representing a chain of valid inputs.

> (require '[automat.viz :refer (view)])
nil
> (require '[automat.core :as a])
nil
> (view [1 2 3])

The circle on the left is the start state, and the circle on the right with the double-lined border is the accept state. Note that the transitions don't have to be numbers:

> (view [:foo "bar" 'baz])

Each argument to fsm can either be an input or another automaton.

> (view [1 [2 [3]]])

Note that this is identical to the first automaton. If you want to consume inputs which are vectors without them being flattened, they can be represented as lists:

> (view [1 '(2 (3))])

We can also combine existing automatons using the operators in automat.core:

> (view (a/or [1 2 3] [1 3]))

This represents the union of the two automata, and returns an automaton which will either accept 1, 2, 3 or 1, 3.

If we want to accept a range of inputs, we can use range:

> (view [1 (a/range 2 10) 11])

This will accept 1, 2, 11, 1, 3, 11, and so on. If we subsequently want to narrow this, we can use and:

> (view
    (a/and
      [1 (a/range 2 7) 11]
      [1 (a/range 6 12) 11]))

This represents the intersection of two automata, in this case giving us an automaton that either accepts 1, 6, 11 or 1, 7, 11. Note that if the intersection is empty, this will give us an automaton that cannot accept anything.

> (view (a/difference (a/range 1 10) 2 (a/range 5 6)))

This represents the difference between the automata, in this case an automata that accepts [1,10], less the inputs 2, 5, 6.

The operators *, +, and ? behave as they do in regular expressions:

> (view [(a/? 1) (a/* 2) (a/+ 3)])

This gives us an automaton that accepts zero or one 1 inputs, zero or more 2 inputs, and one or more 3 inputs.

The not operator is equivalent to the regex ^ flag for negating character classes:

> (view [1 (a/not 2) 3])

In this diagram, DEF represents the default transition (in this case, anything but 2), and REJ represents a rejection state. The DEF transition will consume the input, but the transition to the REJ state will not.

using an FSM

Once we've defined an FSM, we can compile it:

(a/compile [1 2 3])

This will optimize the FSM, emit code that processes it, and call eval on it. The resulting compiled FSM can be interacted with via advance, find, greedy-find. These operations are pure, and can be safely called on multiple threads.

Notice that when we visualize a compiled FSM, the states themselves are now labeled:

> (view (a/compile [4 5 6]))

The simplest way to interact with a compiled FSM is (advance fsm state input). It takes a state, which will be discussed later, and a single input.

> (def f (a/compile [1 2 3]))
#'f
> (a/advance f nil 1)
{:accepted? false
 :checkpoint nil
 :state-index 1
 :start-index 0
 :stream-index 1
 :value nil}

This returns a map representing the FSM state produced by the input.

field description
accepted? whether the FSM is at an accept state
state-index the numeric identifier for the current FSM state, as shown by view
start-index the stream index where the FSM last (re)started
stream-index the current stream index
checkpoint the previous match, only non-nil when used with greedy-find
value the current reduce value, explained below

This map can be passed back into advance as the state parameter:

> (def adv (partial a/advance f))
#'adv
> (-> nil (adv 1) (adv 2) (adv 3))
{:accepted? true
 :checkpoint nil
 :state-index 3
 :start-index 0
 :stream-index 3
 :value nil}

Notice that advance either accepts the map descriptor returned by a previous call to advance, or an arbitrary value, representing an initial value at the beginning of the FSM. The reasons for this are shown below.

If we pass an input into advance that isn't accepted by the FSM, it will throw an exception. Alternately, we can pass an additional field into advance specifying what it should return if the input is rejected:

> (a/advance f nil 42)
IllegalArgumentException
> (a/advance f nil 42 :error)
:error

Normal finite-state automata are represented only by the state-index. The additional fields give us some additional capabilities; of special interest is the value field, which allows us to treat the FSM as a pattern-matching reduction over a stream of values.

We can define reduction operations within our FSM using the $ function:

> (def f (a/compile
           [1 2 3 (a/$ :complete)]
           {:reducers {:complete (fn [state input] :completed)}}))
#'f
> (view f)

As shown here, the :complete action is something that's associated with the previous input, in this case 3. Then, when calling compile, we associate a reduce function with that action, which takes the current reduce :value and the latest input. In this case, it simply returns :completed when it gets the sequence [1 2 3].

> (def adv (partial a/advance f))
#'adv
> (-> :incomplete (adv 1) (adv 2) :value)
:incomplete
> (-> :incomplete (adv 1) (adv 2) (adv 3) :value)
:completed

While this is a fairly trivial application, there are a wide variety of ways this can be used. For instance, this means that unlike a normal automaton, we can tell whether a list of parentheses are balanced, by tracking the number of un-closed parens in our :value.

a short example

For a more real-world use case, consider tracking browsing behavior on an online store. We want to see when the customer visits the cart, begins to checkout, but then returns back to the cart without having completed the checkout. Seeing this indecisive behavior, we can make a special offer.

This means that we want to track this pattern:

[:cart :checkout :cart]

However, the user may wander around a bit, so we want to allow for arbitrary pages in between:

> (def pages [:cart :checkout :cart])
#'pages
> (def page-pattern
    (vec
      (interpose (a/* a/any) pages)))
#'page-pattern
> (view page-pattern)

Here we've interposed (a/* a/any) between each transition, which will happily accept zero or more pages that don't match the expected pattern. However, the data that represents our pages is likely more complicated than just a keyword. Likely it will be a map describing the URL, the page contents, and so on.

In this case, we want to define the :signal when compiling the FSM, which is a function that takes the raw input, and returns the signal that will actually be matched on. In this case, let's assume that each pageview is represented by a map with a :page-type field that will contain :cart, :checkout, and others. Let's also assume that we want to keep track of the full data for the cart and checkout pages.

This is all fairly straightforward:

> (def page-pattern
    (->> [:cart :checkout :cart]
         (map #(vector [% (a/$ :save)]))
         (interpose (a/* a/any))
         vec))
#'page-pattern

> (def f
    (a/compile

      [(a/$ :init)
       page-pattern
       (a/$ :offer)]

      {:signal :page-type
       :reducers {:init (fn [m _] (assoc m :offer-pages []))
                  :save (fn [m page] (update-in m [:offer-pages] conj page))
                  :offer (fn [m _] (assoc m :offer? true))}}))
#'f
> (view f)

First, we take each of the notable pages and add a :save action to them. Then we interpose the wildcard matcher to each. Then we define reducer functions that saves the full page data under :offer-pages, and sets :offer? to true if we continue through the whole process.

Notice that the first action doesn't have an associated input, it simply happens upon entering the FSM. Notice too that the final input has two associated actions. This is perfectly safe, but the order in which the actions occur is not well-defined, so they should always be indepenent of each other. If the same action is defined twice on a given transition, it will only be performed once.

This is a fairly verbose way to define simple behavior, but it's worth noting that almost everything except for the [:cart :checkout :cart] sequence can be abstracted away. With the proper domain-specific scaffolding, this can be a powerful declarative mechanism.

It's also easy to extend. Let's say that we want to save any :product pages the user visits, to better inform our offer. This is a fairly small modification:

> (view
    (->> [:cart :checkout :cart]
         (map #(vector [% (a/$ :save)]))
         (interpose
           (a/*
             (a/or
               [:product (a/$ :save)]
               a/any)))
         vec))

As our desired behavior gets more complicated, they can still be defined as small, composable pieces of behavior.

Using this FSM would be indeed pretty straightforward :

> (def adv (partial a/advance f))
> (-> nil
      (adv {:page-type :cart})
      (adv {:page-type :product})
      (adv {:page-type :product})
      (adv {:page-type :product})
      (adv :anything)
      (adv {:page-type :checkout})
      (adv {:page-type :product})
      (adv {:page-type :cart}) :value)
{:offer-pages [{:page-type :cart}
               {:page-type :product}
               {:page-type :product}
               {:page-type :product}
               {:page-type :checkout}
               {:page-type :product}
               {:page-type :cart}],
 :offer? true}

searching over streams

find works similarly to regex matching. It takes the stream of inputs, which can be a byte-array, java.nio.Buffer, java.io.InputStream, java.io.Reader, or a normal Clojure sequence. The inputs in the FSM correspond to elements from these streams, which will be consumed until an accept state is reached, or the end of the stream is reached. In either case, find will return a new state.

If a match is found, accepted? will be true, start-index will be the point within the stream where the match begins, and stream-index will be the point where the match ends.

> (a/find f nil [1 2 3])
{:accepted? true, :checkpoint nil, :state-index 3, :start-index 0, :stream-index 3, :value nil}

If accepted? is false, the start-index will describe where the FSM last restarted. This state can be passed into find with a new stream of inputs.

> (a/find f nil [1 2 1 2])
{:accepted? false, :checkpoint nil, :state-index 2, :start-index 2, :stream-index 4, :value nil)}

In either case, state-index will describe where we are in the actual compiled automaton. This can be correlated by calling automat.viz/view on a compiled FSM.

When defining patterns that are subsets of each other, such as (or [1 2] [1 2 3]), find will always return upon matching the shortest of the two. If we want to match on the longest, we can use greedy-find instead. Note that greedy-find will always consume at least one more input than it needs for the match, which means that reduce actions used with it must not have side effects.

When using $ for accumulation tasks with find or greedy-find, it can be tedious to place an accumulation reduce function in between every transition. In these cases, we can simply use interpose-$:

> (def f
    (a/compile
      [(a/$ :clear) (a/interpose-$ :conj (range 5))]
      {:reducers {:clear (constantly [])
                  :conj conj}}))
#'f
> (a/find f nil (range 5))
{:accepted? true, :checkpoint nil, :state-index 5, :start-index 0, :stream-index 5, :value [0 1 2 3 4]}

Using $ can be a powerful tool, but it will have a performance impact - for high throughput use cases prefer using the :start-index and :stream-index values to pull out the input data in bulk.

using in ClojureScript

Automat in ClojureScript works just as it does in Clojure except that there is no ClojureScript version of automat.viz.

Compiling FSMs can be slow if they have many states. While rarely a concern for JVM applications, quick startup time is often crucial for client-side JavaScript applications. If you find that compiling your ClojureScript FSMs is too slow, consider defining and precompiling them in Clojure:

(ns clj.namespace
  (:require
   [automat.core :as a]))

(defmacro get-fsm []
  (->> [:foo :bar :baz]
       (a/interpose-$ :conj)
       a/precompile))
(ns cljs.namespace
  (:require
   [automat.core :as a])
  (:require-macros
   [clj.namespace :refer [get-fsm]]))

(def fsm
  (a/compile (get-fsm) {:reducers {:conj conj}}))

(defn fsm-find [input]
  (a/find fsm nil input))

(fsm-find [:foo :bar :baz])
;=> {:accepted? true, :checkpoint nil, :state-index 3, :start-index 0, :stream-index 3, :value (:baz :bar :foo)}

license

Copyright © 2014 Zachary Tellman

Distributed under the MIT License

automat's People

Contributors

achengs avatar cemerick avatar danielcompton avatar dupuchba avatar dvberkel avatar emlyn avatar huahaiy avatar juskrey avatar lewang avatar masztal avatar michalmarczyk avatar reiddraper avatar ztellman avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

automat's Issues

Inconsistency with Char

How to reproduce:

=> (a/advance (a/compile [\A]) nil \A)

ClassCastException java.lang.Character cannot be cast to java.lang.Number  automat.stream/to-stream/reify--19385 (stream.clj:70)

While this of course works:

=> (a/advance (a/compile [1]) nil 1)

And even this:

=> (a/advance (a/compile [\A]) nil 65)

Is there any fundamental reason for chars are mandatorily converted to ints when compilation happens?

java.lang.RuntimeException: Method code too large!

Compiling a complex FSM throws java.lang.RuntimeException: Method code too large!

(require '[automat.core :as a])

(let [coll (range 20)]
  (->> coll
       (map #(interpose % coll))
       (apply a/or)
       a/compile))
;=> clojure.lang.Compiler$CompilerException: java.lang.RuntimeException: Method code too large!

Automaton with `a/not` triggers handlers unexpectedly

Again, apologies if I'm misunderstanding the docs - from the main docs "The not operator is equivalent to the regex ^ flag for negating character classes". Running viz/view on trivial (one-node) automata this fits my understanding, however the following example of a word-count automaton fails:

(defn word+ [v _] (inc v))

(def word [\a])

(def !word (a/not word))

(def word-count-with-not
  (a/compile 
    (a/*
      (a/or 
        [(a/+ !word)]
        [[(a/+ word) !word] (a/$ :word+)]))
    {:reducers {:word+ word+}}))

(:value (reduce #(a/advance word-count-with-not    %1 %2) 0 "aaa aa aaaaaa.")) ;=> 6?!

If I preprocess the machines with a signal instead, the logic seems to be correct:

(def word-count-with-signal
  (a/compile 
    (a/*
      (a/or 
        [(a/+ false)]
        [[(a/+ true) false] (a/$ :word+)]))
    {:signal (partial = \a)
     :reducers {:word+ word+}}))

(:value (reduce #(a/advance word-count-with-signal %1 %2) 0 "aaa aa aaaaaa.")) ;=> 3

Comparing the state machines with viz.view, it looks like the :word+ event is triggered, unexpectedly, after the 2nd (a/+ word) iteration.

README example code errata

The following example under "Searching over streams" has some errors:

> (def f
    (a/compile
      [($:clear) (a/interpose-$ :conj (range 5))]
      {:clear (constantly [])
       :conj conj}))

I believe that should be (a/$ :clear) on line 3 and a missing {:reducers ...} wrapper on lines 4-5.

Referencing a non-existent reducer should cause an error

If you mis-spell the name of a reducer function, it will be ignored silently, e.g.:

(let [fsm (a/compile [:foo :bar (a/$ :baar)]
                     {:reducers {:bar (fn [state _] (assoc state :bar true))}})]
  (reduce (partial a/advance fsm)
          {}
          [:foo :bar]))

I believe it's because of this line in the eval version, and this one in the base version, throwing away any nils after looking up the reducer in the reducers map. Wouldn't it be better to throw some sort of error if a requested reducer does not exist?

Conflict with midje

Using automat together with midje causes exception on startup.

How to reproduce:
Create a new project, add dependencies:

[automat "0.1.3"]
[midje "1.8.2"]

Launch REPL, load midje:

=> (require '[midje.repl])
Run `(doc midje)` for Midje usage.
Run `(doc midje-repl)` for descriptions of Midje repl functions.

CompilerException java.lang.RuntimeException: Unable to resolve symbol: _wrapping-target_ in this context, compiling:(midje/repl.clj:32:3) 

It can be worked around by adding a third dependency:

[potemkin "0.4.2"]

Seems a minor issue, but it took me a while to figure out, eventually found this: marick/Midje#340
I believe it can be fixed by changing potemkin version for automat to 0.4.2. I could make a pull request, but there is no branch for 0.1.3 hotfixes :)

svg viewing problem

When the result of automat's fsm->dot is run through rhizome's dot->svg, the resulting
svg fragment doesn't display the entire automata. Specifically, the svg's width and height
are larger than the viewBox dimensions:

(-> (automat/fsm->dot [1] {}) rhizome/dot->svg)
;=>  ... <svg width="242pt" height="72pt"  viewBox="0.00 0.00 174.00 52.00" ...

I need to call {:dpi 72} to get a correct rendering. I'm on ubuntu 12.04.

(-> (automat/fsm->dot [1] {:dpi 72}) rhizome/dot->svg)
;=>  ... <svg width="174pt" height="52pt"  viewBox="0.00 0.00 174.00 52.00" ...

I'm using the svg fragments to render the automata in gorilla-repl.

Question, how do I view?

Hi, I'm new to clojure and when I try to do the example I'm unsuccessful.

$ lein repl
2016-12-21 09:00:52,663 [main] DEBUG org.jboss.logging - Logging Provider: org.jboss.logging.Slf4jLoggerProvider
nREPL server started on port 62218 on host 127.0.0.1 - nrepl://127.0.0.1:62218
REPL-y 0.3.7, nREPL 0.2.12
Clojure 1.8.0
Java HotSpot(TM) 64-Bit Server VM 1.8.0_25-b17
    Docs: (doc function-name-here)
          (find-doc "part-of-name-here")
  Source: (source function-name-here)
 Javadoc: (javadoc java-object-or-class-here)
    Exit: Control+D or (exit) or (quit)
 Results: Stored in vars *1, *2, *3, an exception in *e

user=> (require '[automat.viz :refer (view)])
nil
user=> (require '[automat.core :as a])
nil
user=> (view [1 2 3])

IOException error=2, No such file or directory  java.lang.UNIXProcess.forkAndExec (UNIXProcess.java:-2)
user=>

I'm on mac os x. What do I need to do to allow lein access to x11?

`greedy-find` not setting `:checkpoint`

I'm not sure if this is a bug, or just a confusion on my part about greedy-find vs find: The documentation is a little unclear, but it seems to suggest that the :checkpoint value would be set by greedy-find if a greedy match is found which skipped a non-greedy match.

For example with this automaton:

(def foo (a/compile 
          (a/or [:d :d (a/$ :first)]
                [:d :d :g (a/$ :second)])
          {:reducers {:first (fn [s t] :first)
                      :second (fn [s t] :second)}}))

I run e.g.:

boot.user=> (require '[automat.core :as a])
nil
boot.user=> (def foo (a/compile
       #_=>           (a/or [:d :d (a/$ :first)]
       #_=>                 [:d :d :g (a/$ :second)])
       #_=>           {:reducers {:first (fn [s t] :first)
       #_=>                       :second (fn [s t] :second)}}))
#'boot.user/foo
boot.user=> (a/find foo nil [:d :d])
#automat.compiler.core.CompiledAutomatonState{
    :accepted? true, :checkpoint nil, 
    :state-index 2, :start-index 0, 
    :stream-index 2, :value :first}
boot.user=> (a/greedy-find foo nil [:d :d])
#automat.compiler.core.CompiledAutomatonState{
    :accepted? true, :checkpoint nil, 
    :state-index 2, :start-index 0, 
    :stream-index 2, :value :first}
boot.user=> (a/find foo nil [:d :d :g])
#automat.compiler.core.CompiledAutomatonState{
    :accepted? true, :checkpoint nil, 
    :state-index 2, :start-index 0,
    :stream-index 2, :value :first}
boot.user=> (a/greedy-find foo nil [:d :d :g])
#automat.compiler.core.CompiledAutomatonState{
    :accepted? true, :checkpoint -->nil<---, 
    :state-index 3, :start-index 0,
    :stream-index 3, :value :second}

The first 3 calls are as expected. I'm not finding the full sequence [:d :d :g] because either I didn't pass in enough tokens, or because I did a non-greedy find.

In the 4th example, I find the full pattern, and get the expected :value. But should the :checkpoint not have been set to the previous value? (I'm not sure what to expect here - perhaps the :first symbol, or the state-index 2, but certainly something to indicate that the most recent non-greedy find.)

java -jar standalone.jar throws ExceptionInInitializerError - Caused by: j.l.ClassCastException: c.l.Var$Unbound cannot be cast to clojure.lang.MultiFn

I'm a bit lost having this problem only when i tried my app using the uberjar generated using lein uberjar

The error says ...
Caused by: java.lang.ClassCastException: clojure.lang.Var$Unbound cannot be cast to clojure.lang.MultiFn

(perhaps) Could be related with my AOT project.clj config???

:main roc.platform.main
:aot [roc.platform.main
          #"roc\.platform\.*" 
          #"roc\.[a-z]+$"]

the output error ...

$ java -jar target/roc-0.1.0-SNAPSHOT-standalone.jar 
Exception in thread "main" java.lang.ExceptionInInitializerError
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:344)
	at clojure.lang.RT.classForName(RT.java:2168)
	at clojure.lang.RT.classForName(RT.java:2177)
	at clojure.lang.RT.loadClassForName(RT.java:2196)
	at clojure.lang.RT.load(RT.java:443)
	at clojure.lang.RT.load(RT.java:419)
	at clojure.core$load$fn__5677.invoke(core.clj:5893)
	at clojure.core$load.invokeStatic(core.clj:5892)
	at clojure.core$load.doInvoke(core.clj:5876)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$load_one.invokeStatic(core.clj:5697)
	at clojure.core$load_one.invoke(core.clj:5692)
	at clojure.core$load_lib$fn__5626.invoke(core.clj:5737)
	at clojure.core$load_lib.invokeStatic(core.clj:5736)
	at clojure.core$load_lib.doInvoke(core.clj:5717)
	at clojure.lang.RestFn.applyTo(RestFn.java:142)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$load_libs.invokeStatic(core.clj:5774)
	at clojure.core$load_libs.doInvoke(core.clj:5758)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$require.invokeStatic(core.clj:5796)
	at clojure.core$require.doInvoke(core.clj:5796)
	at clojure.lang.RestFn.invoke(RestFn.java:421)
	at automat.compiler.core$loading__5569__auto____14606.invoke(core.cljc:1)
	at automat.compiler.core__init.load(Unknown Source)
	at automat.compiler.core__init.<clinit>(Unknown Source)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:344)
	at clojure.lang.RT.classForName(RT.java:2168)
	at clojure.lang.RT.classForName(RT.java:2177)
	at clojure.lang.RT.loadClassForName(RT.java:2196)
	at clojure.lang.RT.load(RT.java:443)
	at clojure.lang.RT.load(RT.java:419)
	at clojure.core$load$fn__5677.invoke(core.clj:5893)
	at clojure.core$load.invokeStatic(core.clj:5892)
	at clojure.core$load.doInvoke(core.clj:5876)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$load_one.invokeStatic(core.clj:5697)
	at clojure.core$load_one.invoke(core.clj:5692)
	at clojure.core$load_lib$fn__5626.invoke(core.clj:5737)
	at clojure.core$load_lib.invokeStatic(core.clj:5736)
	at clojure.core$load_lib.doInvoke(core.clj:5717)
	at clojure.lang.RestFn.applyTo(RestFn.java:142)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$load_libs.invokeStatic(core.clj:5774)
	at clojure.core$load_libs.doInvoke(core.clj:5758)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$require.invokeStatic(core.clj:5796)
	at clojure.core$require.doInvoke(core.clj:5796)
	at clojure.lang.RestFn.invoke(RestFn.java:482)
	at automat.viz$loading__5569__auto____14604.invoke(viz.clj:1)
	at automat.viz__init.load(Unknown Source)
	at automat.viz__init.<clinit>(Unknown Source)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:344)
	at clojure.lang.RT.classForName(RT.java:2168)
	at clojure.lang.RT.classForName(RT.java:2177)
	at clojure.lang.RT.loadClassForName(RT.java:2196)
	at clojure.lang.RT.load(RT.java:443)
	at clojure.lang.RT.load(RT.java:419)
	at clojure.core$load$fn__5677.invoke(core.clj:5893)
	at clojure.core$load.invokeStatic(core.clj:5892)
	at clojure.core$load.doInvoke(core.clj:5876)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$load_one.invokeStatic(core.clj:5697)
	at clojure.core$load_one.invoke(core.clj:5692)
	at clojure.core$load_lib$fn__5626.invoke(core.clj:5737)
	at clojure.core$load_lib.invokeStatic(core.clj:5736)
	at clojure.core$load_lib.doInvoke(core.clj:5717)
	at clojure.lang.RestFn.applyTo(RestFn.java:142)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$load_libs.invokeStatic(core.clj:5774)
	at clojure.core$load_libs.doInvoke(core.clj:5758)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$require.invokeStatic(core.clj:5796)
	at clojure.core$require.doInvoke(core.clj:5796)
	at clojure.lang.RestFn.invoke(RestFn.java:619)
	at roc.auto$loading__5569__auto____14602.invoke(auto.clj:1)
	at roc.auto__init.load(Unknown Source)
	at roc.auto__init.<clinit>(Unknown Source)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:344)
	at clojure.lang.RT.classForName(RT.java:2168)
	at clojure.lang.RT.classForName(RT.java:2177)
	at clojure.lang.RT.loadClassForName(RT.java:2196)
	at clojure.lang.RT.load(RT.java:443)
	at clojure.lang.RT.load(RT.java:419)
	at clojure.core$load$fn__5677.invoke(core.clj:5893)
	at clojure.core$load.invokeStatic(core.clj:5892)
	at clojure.core$load.doInvoke(core.clj:5876)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$load_one.invokeStatic(core.clj:5697)
	at clojure.core$load_one.invoke(core.clj:5692)
	at clojure.core$load_lib$fn__5626.invoke(core.clj:5737)
	at clojure.core$load_lib.invokeStatic(core.clj:5736)
	at clojure.core$load_lib.doInvoke(core.clj:5717)
	at clojure.lang.RestFn.applyTo(RestFn.java:142)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$load_libs.invokeStatic(core.clj:5774)
	at clojure.core$load_libs.doInvoke(core.clj:5758)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$require.invokeStatic(core.clj:5796)
	at clojure.core$require.doInvoke(core.clj:5796)
	at clojure.lang.RestFn.invoke(RestFn.java:1289)
	at roc.platform.system$loading__5569__auto____24062.invoke(system.clj:1)
	at roc.platform.system__init.load(Unknown Source)
	at roc.platform.system__init.<clinit>(Unknown Source)
	at java.lang.Class.forName0(Native Method)
	at java.lang.Class.forName(Class.java:344)
	at clojure.lang.RT.classForName(RT.java:2168)
	at clojure.lang.RT.classForName(RT.java:2177)
	at clojure.lang.RT.loadClassForName(RT.java:2196)
	at clojure.lang.RT.load(RT.java:443)
	at clojure.lang.RT.load(RT.java:419)
	at clojure.core$load$fn__5677.invoke(core.clj:5893)
	at clojure.core$load.invokeStatic(core.clj:5892)
	at clojure.core$load.doInvoke(core.clj:5876)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$load_one.invokeStatic(core.clj:5697)
	at clojure.core$load_one.invoke(core.clj:5692)
	at clojure.core$load_lib$fn__5626.invoke(core.clj:5737)
	at clojure.core$load_lib.invokeStatic(core.clj:5736)
	at clojure.core$load_lib.doInvoke(core.clj:5717)
	at clojure.lang.RestFn.applyTo(RestFn.java:142)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$load_libs.invokeStatic(core.clj:5774)
	at clojure.core$load_libs.doInvoke(core.clj:5758)
	at clojure.lang.RestFn.applyTo(RestFn.java:137)
	at clojure.core$apply.invokeStatic(core.clj:648)
	at clojure.core$require.invokeStatic(core.clj:5796)
	at clojure.core$require.doInvoke(core.clj:5796)
	at clojure.lang.RestFn.invoke(RestFn.java:408)
	at clojure.core$eval145.invokeStatic(NO_SOURCE_FILE:7)
	at clojure.core$eval145.invoke(NO_SOURCE_FILE:7)
	at clojure.lang.Compiler.eval(Compiler.java:6927)
	at clojure.lang.Compiler.eval(Compiler.java:6916)
	at clojure.lang.Compiler.eval(Compiler.java:6890)
	at clojure.core$eval.invokeStatic(core.clj:3105)
	at clojure.core$eval.invoke(core.clj:3101)
	at roc.platform.main$_main.invokeStatic(main.clj:6)
	at roc.platform.main$_main.doInvoke(main.clj:4)
	at clojure.lang.RestFn.invoke(RestFn.java:397)
	at clojure.lang.AFn.applyToHelper(AFn.java:152)
	at clojure.lang.RestFn.applyTo(RestFn.java:132)
	at roc.platform.main.main(Unknown Source)
Caused by: java.lang.ClassCastException: clojure.lang.Var$Unbound cannot be cast to clojure.lang.MultiFn
	at automat.fsm__init.load(Unknown Source)
	at automat.fsm__init.<clinit>(Unknown Source)
	... 150 more

Thanks in advance for this great lib!

timeout example

While I think I know how I would do that (via reducer functions), I am looking for a recommendation how to achieve timeout transitions.

[feature request] signal function return several values

It is desirable for the signal function to return multiple values (a seq of candidates, for example), and the FSM would try and take the first value that can advance to the next state. This way, the signal become context sensitive, giving different interpretations depending on the state.

Would it be possible to add something like this? I looked at the code, it seems to be easy to add this functionality to the base compiler, but I do not seem to grok the eval compiler to be able to add it myself.

Graphviz Required to run View

Thanks for this amazing library Zach.
You may want to mention in the README.md that automat uses rhizome which in turn leverages Graphviz and that you need to have it installed first. If not for that fact that I suspected you were using other libraries of your own, I was stuck looking at a java error on UnixProcesses and no idea why!

Thanks again

Support character ranges

I'd expect character ranges (e.g. the way regexes allow [A-Z]) to work, but they're not supported:

boot.user=> (view (a/range \A \Z))
java.lang.IllegalArgumentException: Don't know how to create a range from '\A' to '\Z'

Non-numeric input when numeric input is expected causes error, not rejection

> (a/advance (a/compile [(int \f)]) nil \f ::fail)
ClassCastException java.lang.Character cannot be cast to java.lang.Number  automat.stream/to-stream/reify--91907 (stream.cljc:73)

to-stream's prior attempt to cover the narrow case of char input when expecting a number caused other issues (#34), but the char/int mismatch is just the most obvious/convenient/common case. Any FSM that requires a numeric input to advance, but encounters one that can't be coerced to a number will fail with a ClassCast instead of rejecting.

A straightforward way to address this would be to signal rejection in the reified nextNumericInput impl if the return isn't actually a number. That would cover the immediate problem.

One interesting/useful case that this wouldn't cover is where one has an FSM expecting a numeric input, it encounters something non-numeric, but you'd like to account for this via the :signal mechanism. Right now, that's not possible at all, but in such a case, it could "back off" to use nextInput, get the non-numeric result, apply the :signal fn, and then attempt to continue. This might belong in a separate enhancement issue, but I think it would nicely shave off a rough corner and make :signal much more general-purpose. At the moment, I'm "preprocessing" inputs to my FSM because some never get to :signal in the first place.

No such var: automat.core/range

Hi there! Sorry to disturb you.
I try to use your library today, and when i tested it with example from README.md , i get error when i use automat.core/range, i put the screenshot of the error :
screen shot 2015-08-26 at 11 45 22 am

i use this version : [automat "0.1.3"]

Thank you!

Documentation link incomplete

The README says to go to http://ideolalia.com/automat/ for the complete documentation, but that link only contains the docs for the viz namespace.

Compilation error in Clojure 1.8?

[automat "0.1.3"] in project.clj's dependencies. When I lein repl and try to require view, I see:

user=> (require '[automat.viz :refer (view)])

CompilerException java.lang.NoClassDefFoundError: IllegalName: compile__stub.automat.fsm.automat.fsm/State, compiling:(automat/fsm.clj:18:1)

Really sorry if this is a known problem, I tried googling and poking around github, but can't find anything, and am super-excited to try it!

Automat "slow"

We discussed this on Slack briefly. I got this using a plain clojure.main repl. I've generated a few data sets using test.check, I've used 1055 as the length of input.

user=> (require '[automat.core :as a]
                '[criterium.core :as c])
user=> (let [data "7Z0YzI6qmKUR4QNOq81l409AC2Ng3c00tb9J4VOz5kBuGTqCAWmYTx4E7Xckp6662k0jd14ds9G1jt0ldShcf37POcfYqsPcqliwlcM0l6GYVF3850650biK89QL3N68Z19k57Eu3dLSukaWZCM32J3Dz99l4KF53JvkWraBwlX3FpU3TWM3X992a1tU2qL6q6Y0g4r4ay5QMuc1twL9xRJMn3WoeA7g8Oh3FoYMX3VD02FhaB76TwMm5kxf2mxK5HUmDu88gYsO4CS6paOqEf7s17hJ712y3xaCT9a3r71M4E2467jr0i4uGt267HMKQO7dqHywM8CFy72AIUq7OZ4K5Nn2t9JuL5Kqv79Y8ZE3aAS4tNeG8ckh201AfB11okTh5bT9oann21A6wN67vzq3Q31SyF5wg2Xx1885z56X4WLgkRfq5A3kFgmqAJdfmlcTp5Dr4404P2nWGmDBJ62dR100RQtLLjKN25a42Kebk1pWFbu6z56y5jHE0txpN9V87cb66Exp50Cg42Jl37Mbi2MqkW218LjNMTnRU7365RT8Uf3523X1cCZFWq7z2GSkc6uL2ai2d88B7a7i1k0h0d9K63765p3w4obwd32kIlQu5xy4yFQ3R91XP353c8J7H3Ep3Oo45p8F70y3649hqM310Gnv13gBXHdLyXs25od8tmwE5fHB3G06OaG877ymRI5J89KI1XylAx47o18H17CHC6P5fm6KC71n40WjK01P4rt06K06F8TM6OF81eq3wE4W0AO702PxEO042I4r9VUt8g0jp3057Sbju25U15HCP1742NEVTM1niDjb0S89885Nf93X97Cgsw5x3eu13ZU8cGbj246qt5t7lzzW08ozl1yykx73d7a74VNPYdZE4498XTgnzhv3R44p6iulMm9Vauv9sXG2Hb4TH4Kx9G6hK1X2MJEsHFm680ZueWNu98G3oVnA435SfwSA6umdt26g9hBoTGMXZ7K64yIs8kYxgwKi0Jqhvm3wdTJdn86u9HX5TeQh45Vc6TSZeCW2xLxNagu"
      fsm (a/compile [\f \o \o])]
  (c/bench
    (reduce
      #(a/advance fsm %1 %2 false)
      nil
      data))) 
Evaluation count : 339480 in 60 samples of 5658 calls.
             Execution time mean : 178.169999 µs
    Execution time std-deviation : 2.128803 µs
   Execution time lower quantile : 176.386367 µs ( 2.5%)
   Execution time upper quantile : 182.867510 µs (97.5%)
                   Overhead used : 1.727016 ns

Found 4 outliers in 60 samples (6.6667 %)
	low-severe	 1 (1.6667 %)
	low-mild	 3 (5.0000 %)
 Variance from outliers : 1.6389 % Variance is slightly inflated by outliers

If I'm not being daft, which is common enough, (/ 178.16 1055) is 0.169µs = 169ns per advancement.

Numbers have a similar result:

user=> (let [data [-3 25 1 -14 -2 15 -18 -11 30 3 -30 17 24 -20 27 13 -1 -17 -15 -23 -24 -25 22 11 12 -8 -24 10 11 14 18 -10 -10 19 -28 27 -26 -8 1 -22 20 4 -12 -13 -15 13 27 -8 30 -7 21 -5 25 -28 10 4 -18 24 3 -16 -20 -22 22 -28 -8 25 -29 -13 -16 -24 -9 1 1 7 -9 7 -26 -5 5 -2 19 25 29 25 -4 20 1 21 -3 21 -13 -11 -15 2 -3 -17 28 -12 -10 15 -7 -28 14 2 13 18 17 -17 7 1 17 11 23 -16 -8 22 -28 21 28 1 18 7 26 27 5 -14 7 16 -7 -21 -7 10 5 5 20 24 30 30 17 27 16 -17 -18 21 -9 -3 -30 3 -25 -24 7 25 22 -18 -3 -12 -8 14 -20 -4 15 7 -19 23 -29 2 1 29 -12 24 19 18 16 -17 20 27 -26 18 -26 16 -17 13 24 29 -23 21 -7 1 -2 -15 -6 17 19 5 -18 30 -7 5 2 -1 3 6 -24 3 -13 -18 5 4 6 -5 7 10 22 7 30 -9 6 -16 -27 22 26 -15 17 14 -10 24 -22 17 20 30 5 14 17 -26 -6 17 23 18 6 1 -25 -15 -29 -7 -20 -23 3 1 23 -1 22 -15 -11 2 -22 12 16 28 24 4 12 -11 -8 1 23 10 0 20 25 -9 -17 21 -21 10 7 -7 5 11 -8 -11 -15 29 8 -29 -24 -14 17 -19 -17 -18 16 16 3 13 -19 7 27 -29 15 -21 -26 24 2 10 -27 -26 13 17 29 -14 0 -26 -2 10 18 0 25 23 8 21 6 -6 -18 5 16 19 23 26 24 -27 -26 -11 -3 -17 -12 23 21 12 23 22 20 -27 -3 -3 15 -1 -20 5 -13 -21 30 -6 -18 -24 -4 20 -30 -15 -28 23 10 27 3 -21 18 -12 1 23 16 4 3 29 -27 -2 15 -26 2 -9 -14 28 18 -25 9 -17 7 -23 -29 -14 -23 -7 -25 16 3 -1 -27 -14 7 -25 28 -11 -27 -15 -27 -6 19 19 -15 -5 26 -9 -28 -4 7 -20 -26 7 8 28 0 14 -21 -16 6 30 10 -21 0 -8 -2 -25 -18 -21 20 -27 24 8 -22 18 -20 8 -10 -29 6 9 -7 4 -11 -17 -21 -4 -16 15 3 10 -23 8 -26 3 20 28 -3 25 20 -28 -28 5 -11 24 -3 -29 -21 27 -20 -12 2 3 22 25 22 25 29 3 26 27 -16 -10 30 25 -28 26 -13 -24 -16 -2 10 11 11 -2 -24 18 -22 14 -30 -25 -5 -23 -28 18 28 10 -16 6 7 -8 -22 23 -13 11 21 -3 -24 -4 22 -13 -8 27 -13 19 -29 10 -6 -7 -5 9 13 -19 -1 -9 30 -2 2 21 -13 9 -12 -5 -26 12 7 -23 20 -29 2 27 5 -14 14 2 14 2 -9 17 -13 -24 -9 -6 11 -6 -26 -27 -13 -1 28 -23 -19 22 3 -29 2 -13 9 -1 11 30 3 5 -17 -18 28 26 28 -9 -20 15 -4 -19 -14 -18 -8 -17 -22 -29 -24 20 12 12 16 7 -25 -6 14 0 -15 14 20 -25 23 10 -29 8 14 -19 -20 -3 25 -3 -7 -19 -11 0 -29 24 27 -28 -28 22 -17 27 12 -24 -26 -19 14 17 -25 -7 -13 18 28 21 24 22 -25 25 -18 -15 -2 25 26 -17 20 15 -7 30 18 20 -14 -12 -16 10 23 -27 -7 -14 -8 -8 -12 24 6 -4 30 -16 14 4 -15 27 -17 -14 -24 29 29 18 -7 -15 28 -11 7 6 18 30 -25 22 -2 -20 1 -23 -13 -28 -4 26 18 25 22 -18 -5 -25 -7 5 30 -1 -7 -8 -30 24 -13 19 18 26 1 -16 1 -1 25 16 10 9 1 12 -28 5 -27 -16 14 24 18 -1 -12 25 -19 30 24 -21 27 18 -10 -14 -7 -12 -26 -9 13 23 10 -6 1 -7 -16 25 20 -22 11 12 20 4 14 -6 18 19 5 -6 14 -29 -26 21 11 30 19 -24 -12 -6 -27 25 14 -1 25 -12 18 -17 0 -29 -24 -30 5 -20 29 24 -17 28 -11 -10 7 11 7 -22 2 21 14 -15 -29 8 16 16 10 9 -10 16 30 12 -2 -18 9 5 -20 13 10 -29 17 11 22 22 -27 -3 -9 -19 22 11 15 -22 -15 11 -27 15 15 -26 6 -2 2 16 16 -7 -1 -7 -2 -17 20 -12 -13 18 8 7 -3 9 30 13 -19 -4 -13 7 -12 24 -7 8 10 -5 14 6 26 -18 -21 -1 -4 28 4 2 -13 21 15 -4 12 6 -20 -6 23 -21 8 22 22 5 23 -12 -23 -16 -11 7 -12 20 -24 10 -23 16 7 20 3 -1 26 -24 23 -21 -9 27 -3 -23 -30 22 16 12 3 10 -14 -17 -6 16 -16 -17 -2 5 13 -6 23 27 -26 15 -7 21 -14 14 12 -8 -13 23 -5 -27 30 27 -5 26 29 29 28 1 -15 -26 20 30 17 1 2 -1 -3 13 13 -10 -13 13 -7 -15 -14 -26 -11 -3 1 21 -16 11 23 -1 6 17 19 2 2 -14 3 -27 9 2 14 -18 -30 22 3 -2 19 -26 30 29 -24 -16 -2 5 -6 -13 -21 -20 -17 23 15 30 15 -12 -5 -18 8 -2 -9 -13 -24 -20 -26 -27 -9 21 -30 -10 4 -27 22 25 18 8 -28 -16 6 30 -2]
      fsm (a/compile [50 20 10])]
  (c/bench
    (reduce
      #(a/advance fsm %1 %2 false)
      nil
      data)))

Evaluation count : 446280 in 60 samples of 7438 calls.
             Execution time mean : 139.068076 µs
    Execution time std-deviation : 12.810401 µs
   Execution time lower quantile : 134.080157 µs ( 2.5%)
   Execution time upper quantile : 168.040733 µs (97.5%)
                   Overhead used : 1.727016 ns

Found 9 outliers in 60 samples (15.0000 %)
	low-severe	 1 (1.6667 %)
	low-mild	 8 (13.3333 %)
 Variance from outliers : 65.3027 % Variance is severely inflated by outliers
nil

This is with this java:

❯ java -version
openjdk version "1.8.0_152"
OpenJDK Runtime Environment (build 1.8.0_152-b03)
OpenJDK 64-Bit Server VM (build 25.152-b03, mixed mode)

and clojure version 1.8.

I also tried with Oracle JDK:

user=> (let [data [-3 25 1 -14 -2 15 -18 -11 30 3 -30 17 24 -20 27 13 -1 -17 -15 -23 -24 -25 22 11 12 -8 -24 10 11 14 18 -10 -10 19 -28 27 -26 -8 1 -22 20 4 -12 -13 -15 13 27 -8 30 -7 21 -5 25 -28 10 4 -18 24 3 -16 -20 -22 22 -28 -8 25 -29 -13 -16 -24 -9 1 1 7 -9 7 -26 -5 5 -2 19 25 29 25 -4 20 1 21 -3 21 -13 -11 -15 2 -3 -17 28 -12 -10 15 -7 -28 14 2 13 18 17 -17 7 1 17 11 23 -16 -8 22 -28 21 28 1 18 7 26 27 5 -14 7 16 -7 -21 -7 10 5 5 20 24 30 30 17 27 16 -17 -18 21 -9 -3 -30 3 -25 -24 7 25 22 -18 -3 -12 -8 14 -20 -4 15 7 -19 23 -29 2 1 29 -12 24 19 18 16 -17 20 27 -26 18 -26 16 -17 13 24 29 -23 21 -7 1 -2 -15 -6 17 19 5 -18 30 -7 5 2 -1 3 6 -24 3 -13 -18 5 4 6 -5 7 10 22 7 30 -9 6 -16 -27 22 26 -15 17 14 -10 24 -22 17 20 30 5 14 17 -26 -6 17 23 18 6 1 -25 -15 -29 -7 -20 -23 3 1 23 -1 22 -15 -11 2 -22 12 16 28 24 4 12 -11 -8 1 23 10 0 20 25 -9 -17 21 -21 10 7 -7 5 11 -8 -11 -15 29 8 -29 -24 -14 17 -19 -17 -18 16 16 3 13 -19 7 27 -29 15 -21 -26 24 2 10 -27 -26 13 17 29 -14 0 -26 -2 10 18 0 25 23 8 21 6 -6 -18 5 16 19 23 26 24 -27 -26 -11 -3 -17 -12 23 21 12 23 22 20 -27 -3 -3 15 -1 -20 5 -13 -21 30 -6 -18 -24 -4 20 -30 -15 -28 23 10 27 3 -21 18 -12 1 23 16 4 3 29 -27 -2 15 -26 2 -9 -14 28 18 -25 9 -17 7 -23 -29 -14 -23 -7 -25 16 3 -1 -27 -14 7 -25 28 -11 -27 -15 -27 -6 19 19 -15 -5 26 -9 -28 -4 7 -20 -26 7 8 28 0 14 -21 -16 6 30 10 -21 0 -8 -2 -25 -18 -21 20 -27 24 8 -22 18 -20 8 -10 -29 6 9 -7 4 -11 -17 -21 -4 -16 15 3 10 -23 8 -26 3 20 28 -3 25 20 -28 -28 5 -11 24 -3 -29 -21 27 -20 -12 2 3 22 25 22 25 29 3 26 27 -16 -10 30 25 -28 26 -13 -24 -16 -2 10 11 11 -2 -24 18 -22 14 -30 -25 -5 -23 -28 18 28 10 -16 6 7 -8 -22 23 -13 11 21 -3 -24 -4 22 -13 -8 27 -13 19 -29 10 -6 -7 -5 9 13 -19 -1 -9 30 -2 2 21 -13 9 -12 -5 -26 12 7 -23 20 -29 2 27 5 -14 14 2 14 2 -9 17 -13 -24 -9 -6 11 -6 -26 -27 -13 -1 28 -23 -19 22 3 -29 2 -13 9 -1 11 30 3 5 -17 -18 28 26 28 -9 -20 15 -4 -19 -14 -18 -8 -17 -22 -29 -24 20 12 12 16 7 -25 -6 14 0 -15 14 20 -25 23 10 -29 8 14 -19 -20 -3 25 -3 -7 -19 -11 0 -29 24 27 -28 -28 22 -17 27 12 -24 -26 -19 14 17 -25 -7 -13 18 28 21 24 22 -25 25 -18 -15 -2 25 26 -17 20 15 -7 30 18 20 -14 -12 -16 10 23 -27 -7 -14 -8 -8 -12 24 6 -4 30 -16 14 4 -15 27 -17 -14 -24 29 29 18 -7 -15 28 -11 7 6 18 30 -25 22 -2 -20 1 -23 -13 -28 -4 26 18 25 22 -18 -5 -25 -7 5 30 -1 -7 -8 -30 24 -13 19 18 26 1 -16 1 -1 25 16 10 9 1 12 -28 5 -27 -16 14 24 18 -1 -12 25 -19 30 24 -21 27 18 -10 -14 -7 -12 -26 -9 13 23 10 -6 1 -7 -16 25 20 -22 11 12 20 4 14 -6 18 19 5 -6 14 -29 -26 21 11 30 19 -24 -12 -6 -27 25 14 -1 25 -12 18 -17 0 -29 -24 -30 5 -20 29 24 -17 28 -11 -10 7 11 7 -22 2 21 14 -15 -29 8 16 16 10 9 -10 16 30 12 -2 -18 9 5 -20 13 10 -29 17 11 22 22 -27 -3 -9 -19 22 11 15 -22 -15 11 -27 15 15 -26 6 -2 2 16 16 -7 -1 -7 -2 -17 20 -12 -13 18 8 7 -3 9 30 13 -19 -4 -13 7 -12 24 -7 8 10 -5 14 6 26 -18 -21 -1 -4 28 4 2 -13 21 15 -4 12 6 -20 -6 23 -21 8 22 22 5 23 -12 -23 -16 -11 7 -12 20 -24 10 -23 16 7 20 3 -1 26 -24 23 -21 -9 27 -3 -23 -30 22 16 12 3 10 -14 -17 -6 16 -16 -17 -2 5 13 -6 23 27 -26 15 -7 21 -14 14 12 -8 -13 23 -5 -27 30 27 -5 26 29 29 28 1 -15 -26 20 30 17 1 2 -1 -3 13 13 -10 -13 13 -7 -15 -14 -26 -11 -3 1 21 -16 11 23 -1 6 17 19 2 2 -14 3 -27 9 2 14 -18 -30 22 3 -2 19 -26 30 29 -24 -16 -2 5 -6 -13 -21 -20 -17 23 15 30 15 -12 -5 -18 8 -2 -9 -13 -24 -20 -26 -27 -9 21 -30 -10 4 -27 22 25 18 8 -28 -16 6 30 -2]
      fsm (a/compile [50 20 10])]
  (c/bench
    (reduce
      #(a/advance fsm %1 %2 false)
      nil
      data)))

Evaluation count : 428820 in 60 samples of 7147 calls.
             Execution time mean : 140.780361 µs
    Execution time std-deviation : 748.460193 ns
   Execution time lower quantile : 139.914201 µs ( 2.5%)
   Execution time upper quantile : 142.457726 µs (97.5%)
                   Overhead used : 1.727470 ns

Found 6 outliers in 60 samples (10.0000 %)
	low-severe	 6 (10.0000 %)
 Variance from outliers : 1.6389 % Variance is slightly inflated by outliers
nil
❯ java -version
java version "1.8.0_144"
Java(TM) SE Runtime Environment (build 1.8.0_144-b01)
Java HotSpot(TM) 64-Bit Server VM (build 25.144-b01, mixed mode)

Is this enough detail? Anything else I can do?

`permissable?` predicate?

I'm trying to express the concept of valid transitions between states for one of the domain entities in my system.

I have an fsm that does this nicely: [1 (a/* [2 3]) 4].

Requests to move the domain entity from its current state (any of 1, 2, 3, or 4) to another state (say from 1 to 4 or 1 to 2 or 2 to 3 or 3 to 2) come in via http requests and I'd like to return a 409 in the event that the transition would not be allowable given the above fsm.

However, to do that, it appears that I have to construct an entire state map with knowledge of the internal state-indexes used by the fsm once it's been compiled. There doesn't appear to be any way to turn the compiled fsm into, say, a state transition table like {1 #{2 4} 2 #{3} 3 #{2 4}} which could then be used in turn to implement a possible? or permissible? predicate.

So, am I missing something or could something like permissible? be implemented directly in automat? Ideally, it would be something like (a/permissible? fsm (things-current-state-as-named-in-fsm-data thing) proposed-state)

I will say that I may be fundamentally misunderstanding this library and maybe even FSMs in general so forgive me if that's the case and maybe point me in the direction of some reading I could do/a different library I could use.

Thanks in advance!

Something about all-numerics strings causes failure (?)

The automata ["1"] won't accept the input "1":

avi.repl=> (a/advance (a/compile [:1]) nil :1 :error)
#automat.core.CompiledAutomatonState{:accepted? true, :checkpoint nil, :state-index 1, :start-index 0, :stream-index 1, :value nil}
avi.repl=> (a/advance (a/compile ["1"]) nil "1" :error)

ClassCastException java.lang.String cannot be cast to java.lang.Number  automat.stream/to-stream/reify--20992 (stream.clj:70)
avi.repl=> (a/advance (a/compile ["x1"]) nil "x1" :error)
#automat.core.CompiledAutomatonState{:accepted? true, :checkpoint nil, :state-index 1, :start-index 0, :stream-index 1, :value nil}
avi.repl=>

Inconsistency of $

First of all, thanks for the wonderful library! I'm trying to study FSM applications and doing some exercises. Suddenly I bumped into an issue:

This works:

(view  (a/interpose-$ 42 [1]))

While this doesn't:

(view [1 (a/$ 42)])

If I replace 42 with :foo, it works both ways and produces identical FSMs.
My use case is that I want to dispatch reducers with a function, not with a map:

(defn reducers [v]
  (fn [state _] (+ state v)))
...
(a/compile [ ... ]
             {:reducers reducers}))

Therefore I need to pass more than just a keyword, possibly a nested data structure.
Is this a bug or a feature? It would be great to have it working consistently, and also just $ is much shorter than interpose-$.

`valid-transitions` fn?

Related to #23, the other question I'm interested in asking the fsm is, given the current state, what inputs would be valid. This would allow me to return to the user in the 409 the statement: you asked me to do this, which is wrong, but you could've asked me to do these, which would have been ok.

Again, given the tools available in fsm I'm unclear how to accomplish that goal. Any help there?

Wrong fsm/complement

The construction of the complement of an automaton (automat.fsm/complement) is wrong, as it appears by inspecting the svg of the complement of a, or of the complement of a*. Currently the function builds the complement by "switching" final and nonfinal states. This works only for complete DFAs but not for incomplete DFAs or NFAs (see https://en.wikipedia.com/wiki/Deterministic_finite_automaton for definitions). In the general case the construction should be as follows:

1- first, if the automaton is nondeterministic, make it deterministic; call d the obtained deterministic automaton;
2- then, if d is incomplete, make it complete by adding a "refuse all" nonfinal state S, and transitions s -[DEF]->S from all the states s to S; call c the resulting automaton;
3- finally, the final and nonfinal states of c are switched. Note that this way S becomes an "accept all" state.

In the current implementation both step 1 and step 2 are missing.

[Feature request] Generate state transition table

Hello,

As FSM's can be represented by both a state diagram and a state transition table, I think it would be really nice if the lib could generate such tables. As an image or as HTML. HTML could be easy to implement, but I'm not sure since I haven't dig into the code yet.

What do you think ?

infinite loop with "or"

The or function in core may run into infinite loop at the minimization step. In particular, when it happens, the infinite loop will appear in the root function in the merge-pairs function. The occurrence of the infinite loop seems to be random, however, it will always appear if one runs or enough times.

To reproduce, run the following code:

(dotimes [i 100]
   (println (str "No." i))
   (a/or 1 2 3 4))

It may take a couple of runs of the above to generate an infinite loop. The process will have to be killed to stop it.

If we only or two states together, this bug doesn't appear.

WARNING: cljs.core/bit-or

Seeing a warning in cljs:

WARNING: cljs.core/bit-or, all arguments must be numbers, got [clj-nil number] inste
ad. at line 601 /Users/zalan/.boot/cache/tmp/Users/zalan/src/semion/onty/2ao/-bj0ipr
/main.out/automat/fsm.cljc                                                         

Simple test case:

(ns simple.core
  (:require [automat.core :as a]))

(a/compile [1])

Tested with:

  • boot: 2.5.5
  • clojure: 1.8.0
  • cljs: 1.8.40, 1.9.76, 1.9.225

It doesn't seem to have any affect in my simple use cases. Is this a known issue? Is this something to worry about?

Customize viz (edge) labels

I'm generating FSMs that use integers as inputs/signals, but they're actually character codes. (Doing this so range and such are available.) I'd like to get characters on the visualizations' edges, but I don't see an easy way to do so. Looks like I could do unwise things to #'pprint-inputs as a workaround, but maybe I'm missing something more straightforward?

CompilerException when using aleph and automat together

I'm trying to use both aleph and automat in a project together. In :requireing libraries from these projects I end up with the error:

CompilerException java.lang.IllegalAccessError: vector does not exist, compiling:(byte_streams/graph.clj:1:1)

I've tried to track this down myself and see where byte_streams/graph.clj does the following so that it can refer to clj-tuple's vector. Beyond that I'm lost.

(:refer-clojure :exclude [vector type byte-array])

Here are the dependencies from project.clj:
:dependencies [[org.clojure/clojure "1.6.0"] [automat "0.1.3"] [aleph "0.4.0"]])

Here is the ns:
(ns nettink.core (:require [automat.core :as a] [aleph.tcp :as tcp]))

Not sure if there is a different way to :require these two projects together or not. Am hoping that you have combined the two before and can help me out.

reducers for actions on overlapping unions are not called

(require '[automat.core :as a]

(-> (a/or (a/interpose-$ :x [1 2 3])
          (a/interpose-$ :y [1 2 9]))
    (a/compile {:reducers {:x #(update-in %1 [:x] conj %2)
                           :y #(update-in %1 [:y] conj %2)}})
    (a/find {:x [] :y []} [1 2 3])
    :value)
;=>  {:x [3] :y [1 2]}

I would expect the reduction value to be {:x [1 2 3] :y [1 2]}.

It looks like only actions attached to the last union are triggered when the unions overlap.

(-> (a/or (a/interpose-$ :y [1 2 9]) ; :y actions will be replaced by overlapping :x actions
          (a/interpose-$ :x [1 2 3]))
    (a/compile {:reducers {:x #(update-in %1 [:x] conj %2)
                           :y #(update-in %1 [:y] conj %2)}})
    (a/find {:x [] :y []} [1 2 3])
    :value)
;=> {:x [1 2 3], :y []}
; no :y actions trigger

ClassCastException thrown by heterogenous FSMs

(require '[automat.core :as a])

(-> [:a \b]
    a/compile
    (a/find nil [:a :b]))
;=> java.lang.ClassCastException: clojure.lang.Keyword cannot be cast to java.lang.Number

Looks like chars and 1-character strings are coerced to ints: https://github.com/ztellman/automat/blob/8bd3989e0c9d987caafc3b5f0d64e7750d2a3d49/src/automat/core.clj#L19-24

Oddly, this works just fine:

(-> (a/or [:a \b] [:a :b])
    a/compile
    (a/find nil [:a :b]))
;=> #automat.core.CompiledAutomatonState{:accepted? true, :checkpoint nil, :state-index 2, :start-index 0, :stream-index 2, :value nil}

persisting automata

I am looking for a way to dump the state into EDN.

I can achieve the opposite using (a/->automaton-state fsm m) though.

Please advise.

More details on $

Could you add more details on when the reduce actions are being executed to the documentation, and show the actions in the view graph?

In the documentation it reads like the actions might be associated with the transitions, but they actually seem to be collected on each state, i.e. for :x :w input action :F is still executed because it's associated with state 1, not the transition :z.

(defn states
  [arg]
  (let [f (a/compile
            [(a/$ :A)
             (a/or
               [(a/$ :B) :x (a/$ :C)]
               [(a/$ :D) :y (a/$ :E) :z (a/$ :F)])
             (a/$ :G)
             :w
             (a/$ :H)]
            (apply merge
                   (map (fn [t] {t #(conj %1 [t %2])})
                        [:A :B :C :D :E :F :G :H])))])
  (:value (a/greedy-find f (a/start f []) arg)))

(states [:x :w])
=> [[:A :x] [:D :x] [:B :x] [:F :x] [:C :x] [:G :x] [:H :w]]
(states [:y :z :w])
=> [[:A :y] [:D :y] [:B :y] [:E :y] [:F :z] [:C :z] [:G :z] [:H :w]]

screen shot 2014-05-24 at 16 25 58

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.