Чистая и безопасная для типов реализация конечного автомата на статически типизированном языке? - PullRequest
25 голосов
/ 09 октября 2011

Я реализовал простой конечный автомат в Python:

import time

def a():
    print "a()"
    return b

def b():
    print "b()"
    return c

def c():
    print "c()"
    return a


if __name__ == "__main__":
    state = a
    while True:
        state = state()
        time.sleep(1)

Я хотел перенести его на C, потому что он был недостаточно быстрым. Но C не позволяет мне сделать функцию, которая возвращает функцию того же типа. Я попытался сделать функцию такого типа: typedef *fn(fn)(), но она не работает, поэтому мне пришлось вместо этого использовать структуру. Теперь код очень уродливый!

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

typedef struct fn {
    struct fn (*f)(void);
} fn_t;

fn_t a(void);
fn_t b(void);
fn_t c(void);

fn_t a(void)
{
    fn_t f = {b};

    (void)printf("a()\n");

    return f;
}

fn_t b(void)
{
    fn_t f = {c};

    (void)printf("b()\n");

    return f;
}

fn_t c(void)
{
    fn_t f = {a};

    (void)printf("c()\n");

    return f;
}

int main(void)
{
    fn_t state = {a};

    for(;; (void)sleep(1)) state = state.f();

    return EXIT_SUCCESS;
}

Итак, я решил, что это проблема с системой сломанных типов Си. Поэтому я использовал язык с реальной системой типов (Haskell), но возникает та же проблема. Я не могу просто сделать что-то вроде:

type Fn = IO Fn
a :: Fn
a = print "a()" >> return b
b :: Fn
b = print "b()" >> return c
c :: Fn
c = print "c()" >> return a

Я получаю ошибку, Cycle in type synonym declarations.

Так что мне нужно сделать какую-то обертку так же, как я делал для кода C, вот так:

import Control.Monad
import System.Posix

data Fn = Fn (IO Fn)

a :: IO Fn
a = print "a()" >> return (Fn b)

b :: IO Fn
b = print "b()" >> return (Fn c)

c :: IO Fn
c = print "c()" >> return (Fn a)

run = foldM (\(Fn f) () -> sleep 1 >> f) (Fn a) (repeat ())

Почему так сложно создать конечный автомат на статически типизированном языке? Я должен сделать ненужные накладные расходы также на статически типизированных языках. Динамически типизированные языки не имеют этой проблемы. Есть ли более простой способ сделать это на статически типизированном языке?

Ответы [ 11 ]

22 голосов
/ 09 октября 2011

В Хаскеле, идиома для этого - просто пойти дальше и выполнить следующее состояние:

type StateMachine = IO ()
a, b, c :: StateMachine
a = print "a()" >> b
b = print "b()" >> c
c = print "c()" >> a

Вам не нужно беспокоиться, что это переполнит стек или что-то в этом роде. Если вы настаиваете на наличии состояний, то вам следует сделать тип данных более явным:

data PossibleStates = A | B | C
type StateMachine = PossibleStates -> IO PossibleStates
machine A = print "a()" >> return B
machine B = print "b()" >> return C
machine C = print "c()" >> return A

Затем вы можете получать предупреждения компилятора о любых StateMachine, которые забыли некоторые состояния.

15 голосов
/ 09 октября 2011

Если вы используете newtype вместо data, никаких накладных расходов не будет.Кроме того, вы можете обернуть функцию каждого состояния в точке определения, поэтому использующие их выражения не должны:

import Control.Monad

newtype State = State { runState :: IO State }

a :: State
a = State $ print "a()" >> return b

b :: State
b = State $ print "b()" >> return c

c :: State
c = State $ print "c()" >> return a

runMachine :: State -> IO ()
runMachine s = runMachine =<< runState s

main = runMachine a

Редактировать : меня поразило, что runMachineимеет более общую форму;монадическая версия iterate:

iterateM :: Monad m => (a -> m a) -> a -> m [a]
iterateM f a = do { b <- f a
                  ; as <- iterateM f b
                  ; return (a:as)
                  }

main = iterateM runState a

Edit : Хм, iterateM вызывает утечку пространства.Может быть, iterateM_ будет лучше.

iterateM_ :: Monad m => (a -> m a) -> a -> m ()
iterateM_ f a = f a >>= iterateM_ f

main = iterateM_ runState a

Редактировать : Если вы хотите провести какое-то состояние через конечный автомат, вы можете использовать то же определение для State, но изменитьфункция состояния:

a :: Int -> State
a i = State $ do{ print $ "a(" ++ show i ++ ")"
                ; return $ b (i+1)
                }

b :: Int -> State
b i = State $ do{ print $ "b(" ++ show i ++ ")"
                ; return $ c (i+1)
                }

c :: Int -> State
c i = State $ do{ print $ "c(" ++ show i ++ ")"
                ; return $ a (i+1)
                }

main = iterateM_ runState $ a 1
11 голосов
/ 09 октября 2011

Проблема с вашим кодом на Haskell заключается в том, что type вводит только синоним, который очень похож на то, что делает typedef в Си. Одним из важных ограничений является то, что расширение типа должно быть конечным, вы не можете дать конечное расширение вашего конечного автомата. Решение использует newtype: newtype - это обертка, которая существует только для проверки типов; накладные расходы абсолютно нулевые (исключены вещи, возникающие из-за обобщения, которое не может быть удалено). Вот ваша подпись; это проверки типов:

newtype FN = FN { unFM :: (IO FN) }

Обратите внимание, что всякий раз, когда вы хотите использовать FN, вы должны сначала распаковать его, используя unFN. Всякий раз, когда вы возвращаете новую функцию, используйте FN, чтобы упаковать ее.

11 голосов
/ 09 октября 2011

В системах типа C-типа функции не являются гражданами первого порядка. Есть определенные ограничения на их обработку. Это было решение для простоты и скорости реализации / исполнения, которые застряли. Чтобы функции вели себя как объекты, обычно требуется поддержка замыканий. Однако они не поддерживаются наборами команд процессоров mosts. Поскольку C был спроектирован так, чтобы быть близко к металлу, для них не было никакой поддержки.

При объявлении рекурсивных структур в C тип должен быть полностью расширяемым. Следствием этого является то, что вы можете иметь только указатели в качестве собственных ссылок в объявлениях структуры:

struct rec;
struct rec {
    struct rec *next;
};

Также каждый идентификатор, который мы используем, должен быть объявлен. Одним из ограничений типов функций является то, что нельзя объявить их вперед.

Конечный автомат в C обычно работает путем преобразования целых чисел в функции, либо в операторе switch, либо в таблице переходов:

typedef int (*func_t)();

void run() {
    func_t table[] = {a, b, c};

    int state = 0;

    while(True) {
        state = table[state]();
    }
}

В качестве альтернативы вы можете профилировать свой код Python и попытаться выяснить, почему ваш код работает медленно. Вы можете портировать критические части на C / C ++ и продолжать использовать Python для конечного автомата.

6 голосов
/ 09 октября 2011

Как обычно, несмотря на прекрасные ответы, которые уже присутствовали, я не мог удержаться, чтобы попробовать это для себя.Одна вещь, которая беспокоила меня о том, что представлено, это то, что он игнорирует ввод.Конечные автоматы - те, с которыми я знаком, - выбирают между различными возможными переходами на основе ввода.

data State vocab = State { stateId :: String
                         , possibleInputs :: [vocab]
                         , _runTrans :: (vocab -> State vocab)
                         }
                      | GoalState { stateId :: String }

instance Show (State a) where
  show = stateId

runTransition :: Eq vocab => State vocab -> vocab -> Maybe (State vocab)
runTransition (GoalState id) _                   = Nothing
runTransition s x | x `notElem` possibleInputs s = Nothing
                  | otherwise                    = Just (_runTrans s x)

Здесь я определяю тип State, который параметризуется типом словаря vocab,Теперь давайте определим способ, которым мы можем отслеживать выполнение конечного автомата путем подачи его входных данных.

traceMachine :: (Show vocab, Eq vocab) => State vocab -> [vocab] -> IO ()
traceMachine _ [] = putStrLn "End of input"
traceMachine s (x:xs) = do
  putStrLn "Current state: "
  print s
  putStrLn "Current input: "
  print x
  putStrLn "-----------------------"
  case runTransition s x of
    Nothing -> putStrLn "Invalid transition"
    Just s' -> case s' of
      goal@(GoalState _) -> do
        putStrLn "Goal state reached:"
        print s'
        putStrLn "Input remaining:"
        print xs
      _ -> traceMachine s' xs

Теперь давайте попробуем это на простой машине, которая игнорирует его входные данные.Будьте осторожны: формат, который я выбрал, довольно многословен.Тем не менее, каждая последующая функция может рассматриваться как узел на диаграмме конечного автомата, и я думаю, вы найдете, что многословие полностью соответствует описанию такого узла.Я использовал stateId для кодирования в строковом формате некоторой визуальной информации о том, как ведет себя это состояние.

data SimpleVocab = A | B | C deriving (Eq, Ord, Show, Enum)

simpleMachine :: State SimpleVocab
simpleMachine = stateA

stateA :: State SimpleVocab
stateA = State { stateId = "A state. * -> B"
               , possibleInputs = [A,B,C]
               , _runTrans = \_ -> stateB
               }

stateB :: State SimpleVocab
stateB = State { stateId = "B state. * -> C"
               , possibleInputs = [A,B,C]
               , _runTrans = \_ -> stateC
               }

stateC :: State SimpleVocab
stateC = State { stateId = "C state. * -> A"
               , possibleInputs = [A,B,C]
               , _runTrans = \_ -> stateA
               }

Поскольку входные данные не имеют значения для этого конечного автомата, вы можете передать ему что угодно.

ghci> traceMachine simpleMachine [A,A,A,A]

Я не буду включать вывод, который также очень многословен, но вы можете видеть, что он явно перемещается от stateA к stateB до stateC и обратно к stateA снова.Теперь давайте сделаем немного более сложную машину:

lessSimpleMachine :: State SimpleVocab
lessSimpleMachine = startNode

startNode :: State SimpleVocab
startNode = State { stateId = "Start node. A -> 1, C -> 2"
                  , possibleInputs = [A,C]
                  , _runTrans = startNodeTrans
                  }
  where startNodeTrans C = node2
        startNodeTrans A = node1

node1 :: State SimpleVocab
node1 = State { stateId = "node1. B -> start, A -> goal"
              , possibleInputs = [B, A]
              , _runTrans = node1trans
              }
  where node1trans B = startNode
        node1trans A = goalNode

node2 :: State SimpleVocab
node2 = State { stateId = "node2. C -> goal, A -> 1, B -> 2"
              , possibleInputs = [A,B,C]
              , _runTrans = node2trans
              }
  where node2trans A = node1
        node2trans B = node2
        node2trans C = goalNode

goalNode :: State SimpleVocab
goalNode = GoalState "Goal. :)"

Возможные входы и переходы для каждого узла не должны требовать дополнительных пояснений, так как они подробно описаны в коде.Я позволю тебе поиграть с traceMachine lessSipmleMachine inputs для себя.Посмотрите, что происходит, когда inputs недопустимо (не соответствует ограничениям «возможных входов»), или когда вы достигаете целевого узла до конца ввода.

Полагаю, многословность моего решения вродене соответствует тому, что вы в основном просили, что должно было сократить расходы.Но я думаю, что это также иллюстрирует, каким может быть описательный код на Haskell.Несмотря на то, что он очень многословен, он также очень прост в том, как он представляет узлы диаграммы конечного автомата.

4 голосов
/ 12 октября 2011

Нетрудно создать конечные автоматы в Haskell, как только вы поймете, что они , а не монады!Конечный автомат, подобный тому, который вам нужен, это стрелка, а точнее автоматная стрелка:

newtype State a b = State (a -> (b, State a b))

Это функция, которая принимает входное значение и выдает выходное значение вместе с новой версией самого себя.,Это не монада, потому что вы не можете написать join или (>>=) для нее.Эквивалентно, превратив это в стрелку, вы поймете, что невозможно написать для нее ArrowApply экземпляр.

Вот примеры:

import Control.Arrow
import Control.Category
import Prelude hiding ((.), id)

instance Category State where
    id = State $ \x -> (x, id)

    State f . State g =
        State $ \x ->
            let (y, s2) = g x
                (z, s1) = f y
            in (z, s1 . s2)

instance Arrow State where
    arr f = let s = State $ \x -> (f x, s) in s
    first (State f) =
        State $ \(x1, x2) ->
            let (y1, s) = f x1
            in ((y1, x2), first s)

Веселитесь.

3 голосов
/ 09 октября 2011

То, что вы хотите, является рекурсивным типом.Разные языки имеют разные способы сделать это.

Например, в OCaml (язык со статической типизацией) есть необязательный флаг компилятора / интерпретатора -rectypes, который включает поддержку рекурсивных типов, позволяя вам определитьтакие вещи:

let rec a () = print_endline "a()"; b
and b () = print_endline "b()"; c
and c () = print_endline "c()"; a
;;

Хотя это и не "некрасиво", как вы жаловались в своем примере на C, то, что происходит под , остается тем же.Компилятор просто беспокоится об этом за вас, вместо того, чтобы заставлять вас писать это.

Как уже отмечали другие, в Haskell вы можете использовать newtype и не будет никаких "накладных расходов".Но вы жалуетесь на необходимость явно оборачивать и разворачивать рекурсивный тип, который «уродлив».(Точно так же с вашим примером на C; нет никаких «накладных расходов», поскольку на уровне машины структура с 1 членом идентична своему члену, но она «некрасива».)

Другой пример, который я хочу упомянутьGo (другой статически типизированный язык).В Go конструкция type определяет новый тип.Это не простой псевдоним (например, typedef в C или type в Haskell), но он создает полноценный новый тип (например, newtype в Haskell), потому что такой тип имеет независимый «набор методов» методовчто вы можете определить по нему.Из-за этого определение типа может быть рекурсивным:

type Fn func () Fn
3 голосов
/ 09 октября 2011

Вы можете получить тот же эффект в C, что и в коде Python, - просто объявите, что функции возвращают (void*):

#include "stdio.h"

typedef void* (*myFunc)(void);

void* a(void);
void* b(void);
void* c(void);

void* a(void) {
    printf("a()\n");
    return b;
}

void* b(void) {
    printf("b()\n");
    return c;
}

void* c(void) {
    printf("c()\n");
    return a;
}

void main() {
    void* state = a;
    while (1) {
        state = ((myFunc)state)();
        sleep(1);
    }
}
2 голосов
/ 09 октября 2011

Ваша проблема была раньше: Рекурсивное объявление указателя функции в C

Перегрузка оператора C ++ может быть использована, чтобы скрыть механику того, что по существу совпадает с вашим C иРешения на Haskell, как описывает Херб Саттер в GotW # 57: Рекурсивные декларации .

struct FuncPtr_;
typedef FuncPtr_ (*FuncPtr)();

struct FuncPtr_
{
  FuncPtr_( FuncPtr pp ) : p( pp ) { }
  operator FuncPtr() { return p; }
  FuncPtr p;
};

FuncPtr_ f() { return f; } // natural return syntax

int main()
{
  FuncPtr p = f();  // natural usage syntax
  p();
}

Но этот бизнес с функциями, по всей вероятности, будет работать хуже, чем эквивалент с числовыми состояниями.Вы должны использовать оператор switch или таблицу состояний, потому что в этой ситуации вам действительно нужен структурированный семантический эквивалент goto.

1 голос
/ 11 октября 2011

Пример на F #:

type Cont = Cont of (unit -> Cont)

let rec a() =
    printfn "a()"
    Cont (fun () -> b 42)

and b n =
    printfn "b(%d)" n
    Cont c

and c() =
    printfn "c()"
    Cont a

let rec run (Cont f) =
    let f = f()
    run f

run (Cont a)

Относительно вопроса «почему так сложно реализовать конечные автоматы с использованием функций на статически типизированных языках?»: Это потому, что тип a и друзейнемного странно: функция, которая возвращает функцию, которая возвращает функцию, которая возвращает функцию ...

Если я удаляю Cont из моего примера, компилятор F # жалуется и говорит:

Expecting 'a but given unit -> 'a. The resulting type would be infinite when unifying 'a and unit -> 'a.

Другой ответ показывает решение в OCaml, вывод типа которого достаточно силен, чтобы устранить необходимость объявления Cont, что показывает, что статическая типизация не виновата, скорее, отсутствие мощного вывода типов во многих статически типизированных языках.

Я не знаю, почему F # не делает этого, я думаю, может быть, это сделает алгоритм вывода типов более сложным, медленным или «слишком мощным» (он мог бы вывести тип неправильно набранных выражений, потерпев неудачупозже, сообщая об ошибках, которые трудно понять).

Обратите внимание, что PythonПример, который вы привели, не совсем безопасен.В моем примере b представляет семейство состояний, параметризованное целым числом.На нетипизированном языке легко сделать ошибку и вернуть b или b 42 вместо правильной лямбды и пропустить эту ошибку, пока код не будет выполнен.

...