Подтвердить что ты не робот

Чистая и надежная реализация конечного автомата на статически типизированном языке?

Я реализовал простую машину состояний в 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;
}

Итак, я понял, что это проблема с системой C сломанного типа. Поэтому я использовал язык с системой реального типа (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 ())

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

4b9b3361

Ответ 1

Если вы используете 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

Изменить: Hmm, 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

Ответ 2

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

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, который забыл некоторые состояния.

Ответ 3

В системах типа С-типа функции не являются гражданами первого порядка. Существуют определенные ограничения на их обработку. Это было решение для простоты и скорости выполнения/исполнения, которые застряли. Чтобы функции функционировали как объекты, обычно требуется поддержка замыканий. Тем не менее, они, естественно, не поддерживаются наборами инструкций большинства процессоров. Поскольку 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/С++ и продолжать использовать Python для конечного автомата.

Ответ 4

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

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

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

Ответ 5

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

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
      [email protected](GoalState _) -> do
        putStrLn "Goal state reached:"
        print s'
        putStrLn "Input remaining:"
        print xs
      _ -> traceMachine s' xs

Теперь попробуйте попробовать на простой машине, которая игнорирует ее входные данные. Будьте осторожны: формат, который я выбрал, довольно многословный. Тем не менее, каждая следующая функция может рассматриваться как node в диаграмме конечного автомата, и я думаю, вы найдете, что многословие будет полностью релевантным для описания такого node. Я использовал 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. :)"

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

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

Ответ 6

Невозможно сделать государственные машины в 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)

Удачи.

Ответ 7

Вы можете получить тот же эффект в 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);
    }
}

Ответ 8

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

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

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

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

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

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

type Fn func () Fn

Ответ 9

Ваша проблема была прежде: Рекурсивное объявление указателя функции в 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.

Ответ 10

Пример в 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 вместо правильной лямбда и пропустить эту ошибку до тех пор, пока код не будет выполнен.

Ответ 11

Выведенный вами код Python будет преобразован в рекурсивную функцию, но оптимизация хвоста не будет оптимизирована, потому что у Python нет оптимизации хвостового вызова, поэтому в какой-то момент он будет переполняться потоком. Таким образом, код Python действительно сломан и потребует больше работы, чтобы получить его так же хорошо, как версии Haskell или C.

Вот пример того, что я имею в виду:

so.py:

import threading
stack_size_bytes = 10**5
threading.stack_size(10**5)
machine_word_size = 4

def t1():
    print "start t1"
    n = stack_size_bytes/machine_word_size
    while n:
        n -= 1
    print "done t1"

def t2():
    print "start t2"
    n = stack_size_bytes/machine_word_size+1
    while n:
        n -= 1
    print "done t2"

if __name__ == "__main__":
    t = threading.Thread(target=t1)
    t.start()
    t.join()
    t = threading.Thread(target=t2)
    t.start()
    t.join()

оболочки:

$ python so.py
start t1
done t1
start t2
Exception in thread Thread-2:
Traceback (most recent call last):
  File "/usr/lib/python2.7/threading.py", line 530, in __bootstrap_inner
    self.run()
  File "/usr/lib/python2.7/threading.py", line 483, in run
    self.__target(*self.__args, **self.__kwargs)
  File "so.py", line 18, in t2
    print "done t2"
RuntimeError: maximum recursion depth exceeded