haal
A Haskell library for Active Automata Learning.
https://github.com/steve-anunknown/haal#readme
| Stackage Nightly 2026-10-11: | 0.7.0.0 |
| Latest on Hackage: | 0.7.0.0 |
haal-0.7.0.0@sha256:00ad52b5755ab664f164959517ee997d683480e3f0cfe8f6924d61d750dd4906,5291Module documentation for 0.7.0.0
- Haal
- Haal.Automaton
- Haal.BlackBox
- Haal.Dot
- Haal.EquivalenceOracle
- Haal.Experiment
- Haal.Learning
- Haal.Statistics
_ _ _ _ _
/ /\ / /\ / /\ / /\ _\ \
/ / / / / / / / \ / / \ /\__ \
/ /_/ / / / / / /\ \ / / /\ \ / /_ \_\
/ /\ \__/ / / / / /\ \ \ / / /\ \ \ / / /\/_/
/ /\ \___\/ / / / / \ \ \ / / / \ \ \ / / /
/ / /\/___/ / / / /___/ /\ \ / / /___/ /\ \ / / /
/ / / / / / / / /_____/ /\ \ / / /_____/ /\ \ / / / ____
/ / / / / / / /_________/\ \ \ / /_________/\ \ \ / /_/_/ ___/\
/ / / / / / / / /_ __\ \_\/ / /_ __\ \_\/_______/\__\/
\/_/ \/_/ \_\___\ /____/_/\_\___\ /____/_/\_______\/
A Haskell library for Active Automata Learning
Haal is an Active Automata Learning library aimed at making it easy to construct learning experiments and explore different configurations of learning algorithms and equivalence oracles. The library is still in its early stages, so nothing is set in stone yet. Most probably, the more features are added the more will the structure of the library change. For the time being, a summary of the current architecture can be found below. Of course, the best documentation is the source code itself.
š§ Features (constantly updated)
| Automaton Type | Learning Algorithms | Equivalence Oracles |
|---|---|---|
| Mealy Machines | ā LM* | ā W-method Oracle |
| ā LM+ | ā Wp-method Oracle | |
| ā Random Words Oracle | ||
| ā Random Walk Oracle | ||
| ā Random W-method Oracle | ||
| ā Random Wp-method Oracle |
Table of Contents
Architecture
The library consists of the following main components:
- Systems under learning
- Automata
- Learning algorithms
- Equivalence oracles
- Experiment
Systems under learning
Systems under learning are defined by the SUL typeclass, parameterized by the types of their inputs and outputs. They must implement functions that allow them to be queried by learning algorithms and equivalence oracles. They are also parameterized by a monad which they operate in. For example, a SUL that is an external program performs IO actions, whereas a SUL that is a pure function operates in the Identity monad.
Automata
Automata are a subclass of systems under learning that also expose information about their internal states. In addition to being queryable, they must implement functions that expose useful structural information, particularly for use by equivalence oracles.
Learning algorithms
Learning algorithms are used to construct hypotheses based on a SUL. They must support initialization, hypothesis construction, and refinement via counterexamples. Not all learning algorithms learn the same type of automaton; the type of automaton to be learned is determined by the algorithm itself. At the moment, all learning algorithms are represented by a typeclass and each specific learning algorithm is meant to be its own data type. The values of the data type are the different configurations of the algorithm.
Equivalence oracles
Equivalence oracles are algorithms that generate a test suite of queries, which are sent to both the current hypothesis and the SUL, in order to discover counterexamples. They must implement functions to report the size of the test suite, generate the suite, and test the hypothesis for counterexamples. As in the case of learning algorithm, equivalence oracles as a whole are represented by a typeclass and a specific equivalence oracle algorithm is meant to be represented by a data type, with the possible values of it being the different configurations of the algorithm.
Experiment
An experiment ties all the components above together. In this library, experiments are represented as values and can be built with various configurations. An experiment is a function that takes a learning algorithm and an oracle, and returns an environment that awaits a SUL to execute the experiment. This is implemented using a Reader monad, where the environment provides access to the SUL.
Installing
The project is available on Hackage. You can check the available version with the command:
stack list haal # cabal list haal
Given that the project is still in its early stages, you probably always want to see the latest version listed.
You can clone the repo and build the library yourself with:
stack build haal:lib
Moreover, given that the library makes use of LiquidHaskell annotations from v0.4.0.1 onwards, you can build with verification turned on:
stack build haal:lib --flag haal:liquid
Example
Hereās a quick GHCi session putting it all together, showing how to define a simple Mealy machine, configure an experiment, and run a learning algorithm using haal. This session is also available as a program examples/demo.hs which can be run with stack runghc examples/demo.hs, or stack run demo and can also be loaded interactively with stack ghci haal:exe:demo.
ghci> :set +m
ghci> import qualified Data.Set as Set
-- Define input, output, and state types
ghci> data Input = A | B deriving (Show, Eq, Ord, Enum, Bounded)
ghci> data Output = X | Y deriving (Show, Eq, Ord, Enum, Bounded)
ghci> data State = S0 | S1 | S2 deriving (Show, Eq, Ord, Enum, Bounded)
-- Define the transition function for the system under learning
ghci> let sulTransitions S0 _ = (S1, X)
ghci| sulTransitions S1 _ = (S2, Y)
ghci| sulTransitions S2 A = (S0, X)
ghci| sulTransitions S2 B = (S0, Y)
ghci> learner = mkLMstar Star
ghci> teacher = either error id (mkWMethod (WMethodConfig 2))
-- Set up the experiment.
ghci> myexperiment = experiment learner teacher
-- Define the Mealy system under learning. Remember that automata can act as suls.
ghci> mysul = mkMealyAutomaton2 sulTransitions (Set.fromList [S0, S1, S2]) S0
-- Run the experiment
ghci> learnedmodel = runExperiment myexperiment mysul
-- View the learned model
ghci> learnedmodel
{
Current State: 0,
Initial State: 0,
Transitions: fromList [
((0,A),(1,X)),
((0,B),(1,X)),
((1,A),(2,Y)),
((1,B),(2,Y)),
((2,A),(0,X)),
((2,B),(0,Y))
]
}
This shows how a simple Mealy machine can be learned using the LM* algorithm and a W-method equivalence oracle.
This also showcases some strong points of using a functional programming language like haskell for the task of active automata learning:
1. Type-safe alphabets
In Haal, the input and output alphabets are represented as plain Haskell data types. This means the compiler can catch errors early ā for example, if a symbol not defined in the alphabet accidentally appears in a transition, the type checker will reject the code. This eliminates entire classes of bugs that are easy to make in more loosely typed implementations.
data Input = A | B deriving (Show, Eq, Ord, Enum, Bounded)
data Output = X | Y deriving (Show, Eq, Ord, Enum, Bounded)
2. Automata as functions
Instead of defining transitions via tables or external formats like dot, Haskell allows transitions to be encoded as pure functions:
sulTransitions :: State -> Input -> (State, Output)
sulTransitions S0 _ = (S1, X)
sulTransitions S1 _ = (S2, Y)
sulTransitions S2 A = (S0, X)
sulTransitions S2 B = (S0, Y)
Combined with exhaustive pattern matching and totality checking, this ensures that:
- All input cases are handled for each state
- No states or transitions are forgotten
- The definition is both human-readable and machine-checkable
In essence, Haskell itself is the language for defining automata, without needing external DSLs or formats like DOT.
Changes
Changelog for haal
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to the Haskell Package Versioning Policy.
0.7.0.0 - 2026-10-10
This release replaces the built-in statistics with a general mechanism:
experiments report events, and statistics are folds over them. It also makes
query overridable, adds a way to compare automata, and removes the Moore and
DFA types. See āMigrating from 0.6ā at the end of this entry.
Added
queryis now a method ofHaal.BlackBox.SUL, with the old behaviour (reset, then walk) as its default. A SUL can override it to answer a whole query at once, e.g. by sending the whole word in one message. The documentation ofquerylists what an override must guarantee.Haal.BlackBox.queryChecked:query, checking that the SUL returned exactly one output per input. The learners and oracles query through it, so an override that breaks this fails with a clear error instead of corrupting the observation table.Haal.Statistics, with the events an experiment reports (Event:PhaseChanged,Queried,Hypothesis,Counterexample, andPhase), theStatisticsrecord and thestatisticsfold.statisticsmeasures the membership queries and symbols sent while constructing hypotheses and while validating them, and records every hypothesis and counterexample. A statistic is aControl.Foldl.Foldover events, so statistics combine with<*>and users can define their own.Haal.Experiment.experimentWith: run an experiment, reporting every event to a monadic handler.Haal.Experiment.measuredExperiment: run an experiment and a fold over its events in one pass, returning the model and the foldās result.Haal.BlackBox.difference: a shortest input word on which two Mealy automata produce different outputs, orNothingif they are equivalent.MealyAutomatonis a SUL in any monad, not only inIdentity, so an automaton can be learned inside the monad that holds a userās own state.
Changed (breaking)
Haal.Experiment.experimentreturns only the learned model, instead of the model and aStatisticsrecord.Haal.Experiment.Statisticsis removed; the newHaal.Statistics.Statisticsrecord, measured withmeasuredExperiment statistics, replaces it. Itshypothesesinclude the final one; the number of rounds isrounds stats, and the number of equivalence queries islength (hypotheses stats).queryis a class method now, so a module that importsSUL (..)and defines its own top-levelquerygets an ambiguity error. Rename the local definition (theioexample renames it toaskProgram).- New dependency:
foldl.
Removed
Haal.Automaton.MooreAutomatonandHaal.Automaton.DFA. No learning algorithm in haal produces or uses Moore machines or DFAs, so supporting these types on their own was not useful, and their semantics had a known quirk (a step emitted the output of the state it left, not the one it reached). Support for more kinds of automata will come back properly in a future release, together with learning algorithms for them. To learn a DFA today, encode it as a Mealy machine withBooloutputs, where the output of a transition says whether the state it reaches is accepting.
Migrating from 0.6
-- 0.6
(model, stats) = runExperiment (experiment learner oracle) sul
-- statsRounds stats, statsCexs stats, statsHyps stats
-- 0.7: the model only
model = runExperiment (experiment learner oracle) sul
-- 0.7: the model and statistics
(model, stats) = runExperiment (measuredExperiment statistics learner oracle) sul
-- rounds stats, counterexamples stats, hypotheses stats (incl. the final one),
-- learning stats / testing stats: membership queries and symbols per phase
-- 0.7: your own statistics alongside, e.g. the number of events
import qualified Control.Foldl as L
(model, (stats, n)) = runExperiment (measuredExperiment ((,) <$> statistics <*> L.length) learner oracle) sul
0.6.1.1 - 2026-10-09
Fixed
- The W-method and the Wp-method (and their random variants) did not test a
hypothesis with a single state. Such a hypothesis has an empty characterizing
set, so the W-method and the Wp-method generated no test words at all and
accepted it without testing, at any depth, and the random Wp-method crashed
with
Set.elemAt: index out of range. This happens whenever no single input distinguishes the states of the SUL, so learning stopped after the first hypothesis with a wrong one-state model. Test words now end with the empty word when the characterizing set is empty, which still checks the outputs along the rest of the word. - The second phase of the Wp-method started from the wrong prefixes: the state
cover without the transition cover instead of the transition cover without
the state cover. Depth
ktherefore only accounted for aboutk - 1extra states. Learned models can now be correct at a lower depth: on fourhaal-modelsprotocol models, depth 1 now learns three of them correctly (before: none) and depth 2 all four (before: three).
0.6.1.0 - 2026-10-05
Added
Haal.BlackBox.queryruns a single query: it resets the SUL, then walks it over the given inputs and returns the outputs.
Fixed
- The SUL is now reset before every membership query and every equivalence test case. Before, queries only started from the initial state because the library kept returning to the same SUL value. That works for pure automata, but a SUL that wraps a stateful system (a running process, a socket) was queried from whatever state the previous query left it in, so learning could produce wrong models or never terminate. Learning from an automaton that is not in its initial state is fixed too.
- The documentation of
Haal.BlackBox.FiniteOrdsaid(Ord, Bounded)instead of(Ord, Finite).
Changed
- Simplified the examples.
0.6.0.0 - 2026-10-03
Added
Haal.Automaton.MealyAutomaton.mkMealyAutomatonTablebuilds a Mealy automaton with states0 .. n - 1from a transition table and an output table, each encoded as aString, and validates both tables.Haal.Dot.MealyTableandHaal.Dot.mealyTableencode aParsedMealyin that table form, rejecting automata with missing or conflicting transitions.
Fixed
Haal.Dot.parseDotagain orders states, inputs, and outputs by first appearance, as documented. Since 0.5.0.0 it sorted them by name, which changed the constructor order and state numbering of generated modules.
Changed (breaking)
Haal.Experiment.Learnerdrops its state parameter (Learner l aut); learners now always produce automata withIntstates.
Changed
haal-gen(Haal.Dot.generateModule) emits the transition and output functions as string-literal tables built withmkMealyAutomatonTable, instead of one equation per transition, and lists every transition in a comment. Generated modules have the same types and behaviour, but compile about 2-3 times faster.generateModulenow rejects incomplete or nondeterministic automata, instead of generating functions that fail at runtime.Haal.BlackBox.distinguishreturns[]immediately when both states are equal, instead of exploring the product automaton first. Results are unchanged. Its LiquidHaskell spec now states that equal states yield an empty word.
Removed (breaking)
Haal.BlackBox.StateIDtype alias. Learned automata useIntstates directly, ashaal-genandhaal-modelsalready do; replace any use ofStateIDwithInt.Haal.Experiment.pairwiseWalkandHaal.Experiment.executeare no longer exported; usefindCexinstead.
0.5.0.0 - 2026-04-22
Fixed
Haal.Learning.LMstar.equivalenceClassesnow iterates overSmonly, matching Definition 2 of Shahbaz & Groz, āInferring Mealy Machinesā. Previously it iterated overSm āŖ SmĀ·I, which could pick a representative fromSmĀ·Iwhoserep++[i]was not in the observation table, causingmakeHypothesisto fail on many real protocol models (DTLS, medium MQTT) with"invariant violation ā makeHypothesis failed on closed consistent table".
Changed (breaking)
Haal.Learning.LMstar.otIsConsistenttightens its output constraint fromEq otoOrd oto support Map-based row grouping.
Changed
Haal.Learning.LMstar.otIsConsistentgroups prefixes by row signature before pairwise comparison, reducing the worst-case pair enumeration.Haal.Dot.parseDotusesSet.fromListinstead ofnubfor deduplication (O(n²) ā O(n log n)).
Added
tasty-bench-based benchmark suite inbench/covering BlackBox operations, W-method / Wp-method test-suite generation, DOT serialize/parse/roundtrip, and end-to-end learning experiments. Run withstack bench haal.
0.4.1.0 - 2026-03-21
Added
- Serializer from Mealy Automaton to Dot format.
- haal-gen executable that accepts a .dot file and produces a haskell module that exports a function for the specified Mealy Automaton, with specific input and output types, rather than just using String for both.
- haal-models subpackage that exports learned models of tls, mqtt, tcp and dtls protocols.
0.4.0.2 - 2026-03-17
Changed
- Dependency bounds.
0.4.0.1 - 2026-03-17
Added
- Optional
liquidCabal flag (--flag haal:liquid) to enable LiquidHaskell verification without requiring it as a dependency for normal builds.
Verified
Haal.BlackBox:walkproduces outputs of length equal to the input length.Haal.Learning.LMstar:ObservationTableinvariant that all entries inmappingTmap to non-empty output lists, preserved acrossupdateMap,makeConsistent,makeClosed, andinitializeOT.
0.4.0.0 - 2026-03-16
Changed
- Changed the types of oraclesā constructors from
<Oracle>toEither String <Oracle>whereStringis an error message indicating invalid values to<OracleConfig>. Now, for example, instead oforacle = mkWMethod (WMethodConfig 2), one should either pattern match withcaseor dooracle = either error id (mkWMethod (WMethodConfig 2)).
0.3.0.0 - 2026-03-11
Changed
SULtypeclass no longer takesiandoas class parameters; they are now universally quantified in the method signatures. Instances should dropi ofrom their instance heads:instance SUL MyType IOinstead ofinstance SUL MyType IO Input Output.Automatontypeclass likewise dropsiandofrom its class head. All constraint occurrences(Automaton aut s i o)become(Automaton aut s).
0.2.0.0 - 2026-03-05
Added
stepPure,walkPure,resetPureexported fromHaal.BlackBoxConfigrecord types for all equivalence oracles:WMethodConfig,WpMethodConfig,RandomWalkConfig,RandomWordsConfig,RandomWMethodConfig,RandomWpMethodConfigmkCombinedOraclesmart constructor forCombinedOraclerandomWordsConfigaccessor forRandomWordsmealyDelta,mealyLambdaas explicit named exports fromHaal.Automaton.MealyAutomaton
Changed
- All oracle constructors now take a
Configrecord instead of positional arguments:mkWMethod :: WMethodConfig -> WMethod,mkWpMethod :: WpMethodConfig -> WpMethod, etc. mkRandomWMethodandmkRandomWpMethodnow take aConfigrecord instead of positional arguments (also fixes an argument-order bug in the old interface)
Removed
mealyStepfromHaal.Automaton.MealyAutomaton; usestepPurefromHaal.BlackBoxmooreStepfromHaal.Automaton.MooreAutomaton; usestepPurefromHaal.BlackBox- Raw constructor exports (
MealyAutomaton (..),WMethod (..),WpMethod (..),RandomWalk (..),RandomWords (..),CombinedOracle (..),LMstar (..)); use the correspondingmk-prefixed smart constructors instead
0.1.0.0 - 2025-12-02
- Initial release of
haal.- Support for Mealy Automata and DFAs.
- One learner for Mealy Automata and DFAs with 2 configurations.
- LStar.
- LPlus.
- Basic equivalence oracles.
- Examples that showcase usage of the library.