Конечный автомат в Haskell
каков хороший способ представления конечного автомата в Haskell? Как будет выглядеть тип данных?
в нашем колледже, автоматы были определены как 5-ти
(Q, X, delta, q_0, F)
где Q-множество состояний автомата, X-алфавит (является ли эта часть даже необходимой), delta-функция перехода, принимающая 2-кортеж из (Q, X) и возвращающая состояние/-s (в недетерминированной версии), а F-множество принимающих/конечных состояний.
самое главное, я не конечно, какой тип delta
должно быть...
2 ответов
есть два основных варианта:
- явная функция
delta :: Q -> X -> Q
(или[Q]
по мере необходимости), как предлагает Свен Хагер. - карта
delta :: Map (Q, X) Q
например,Data.Map
, или если ваши состояния / алфавит могут быть проиндексированы по возрастанию чиселData.Array
илиData.Vector
.
обратите внимание, что эти два подхода по существу эквивалентны, можно конвертировать из версии карты в версию функции (это немного отличается из-за дополнительного Maybe
от lookup
call) относительно легко
delta_func q x = Data.Map.lookup (q,x) delta_map
(или соответствующая Карри-версия функции поиска для любого типа отображения, который вы используете.)
если вы создаете автоматы во время компиляции (и поэтому знаете возможные состояния и можете закодировать их как тип данных), то использование версии функции дает вам лучшую безопасность типов, так как компилятор может проверить, что вы охватили все случаи.
если вы строите автоматы во время выполнения (например, из пользовательского ввода), а затем Хранение delta
как карта (и, возможно, выполнение преобразования функций, как указано выше) и наличие соответствующей проверки ввода, которая гарантирует правильность, так что fromJust
безопасно (т. е. всегда есть запись на карте для любого возможного (Q,X)
кортеж и поэтому поиск никогда не терпит неудачу (никогда не возвращается Nothing
)).
недетерминированные автоматы хорошо работают с опцией map, потому что неудачный поиск совпадает с отсутствием состояния перейти к, То есть пустой [Q]
список, и поэтому не должно быть любой специальная обработка Maybe
за вызов join . maybeToList
(join
С Data.Monad
и maybeToList
С Data.Maybe
).
на другой ноте алфавит наиболее определенно необходим: это то, как автомат получает вход.
Проверьте контролем.Стрела.Трансформатор.Модуль автомата в пакете "стрелки". Тип выглядит следующим образом
newtype Automaton a b c = Automaton (a b (c, Automaton a b c))
это немного запутанно, потому что его трансформатор стрелки. В простейшем случае можно написать
type Auto = Automaton (->)
который использует функции в качестве базовой стрелки. Подставляя ( - > ) для "a" в определении автомата и используя нотацию infix, вы можете видеть, что это примерно эквивалентно:
newtype Auto b c = Automaton (b -> (c, Auto b c))
другими словами, автомат-это функцию, которая принимает входные данные и возвращает результат и новый автомат.
вы можете использовать это напрямую, написав функцию для каждого состояния, которое принимает аргумент и возвращает результат и следующую функцию. Например, вот государственная машина для распознавания регулярного выражения "a+b" (то есть серии по крайней мере одного "a", за которым следует "b"). (Примечание: непроверенный код)
state1, state2 :: Auto Char Bool
state1 c = if c == 'a' then (False, state2) else (False, state1)
state2 c = case c of
'a' -> (False, state2)
'b' -> (True, state1)
otherwise -> (False, state1)
С точки зрения вашего исходного вопроса, Q = {state1, state2}, X = Char, delta-это приложение функции, и F-переход состояния, возвращающий True (вместо того, чтобы иметь "принимающее состояние", я использовал выходной переход с принимающим значением).
в качестве альтернативы вы можете использовать обозначение стрелки. Automaton-это экземпляр всех интересных классов стрелок, включая цикл и схему, поэтому вы можете получить доступ к предыдущим значениям с помощью delay. (Примечание: опять же, непроверенный код)
recognise :: Auto Char Bool
recognise = proc c -> do
prev <- delay 'x' -< c -- Doesn't matter what 'x' is, as long as its not 'a'.
returnA -< (prev == 'a' && c == 'b')
стрелка "задержка" означает, что" prev "равно предыдущему значению" c", а не текущее значение. Вы также можете получить доступ к предыдущим выводам с помощью "rec". Например, вот стрелка, которая дает вам распадающуюся сумму с течением времени. (Фактически протестировано в этом случае)
-- | Inputs are accumulated, but decay over time. Input is a (time, value) pair.
-- Output is a pair consisting
-- of the previous output decayed, and the current output.
decay :: (ArrowCircuit a) => NominalDiffTime -> a (UTCTime, Double) (Double, Double)
decay tau = proc (t2,v2) -> do
rec
(t1, v1) <- delay (t0, 0) -< (t2, v)
let
dt = fromRational $ toRational $ diffUTCTime t2 t1
v1a = v1 * exp (negate dt / tau1)
v = v1a + v2
returnA -< (v1a, v)
where
t0 = UTCTime (ModifiedJulianDay 0) (secondsToDiffTime 0)
tau1 = fromRational $ toRational tau
обратите внимание, как вход в " delay "включает" v", значение, полученное из его выхода. Предложение " rec " позволяет это, поэтому мы можем создать цикл обратной связи.