Search code examples
javascriptalgorithmparsingstate-machineautomata

How to convert nested function call tree to state machine?


Say you have this sort of system:

const input = [0, [1, 2, [3, 4, [8, 9], 6, 7], 4], [5, 6, 7], 3, 4, [8, 9], 6, 7, [1], 9]
const output = parse(input)
console.log(output.join('-'))

function parse(input) {
  const output = []
  iterate(input, value => {
    check(() => isArray(value),
      () => { // yes array
        const children = parse(value)
        output.push(...children)
      },
      () => { // not array
        check(() => isEven(value), () => {
          output.push(value)
        })
      })
  })
  return output
}

function isArray(value) {
  return Array.isArray(value)
}

function isEven(value) {
  return value % 2 == 0
}

function check(condition, success, failure) {
  if (condition()) success()
  else if (failure) failure()
}

function iterate(array, block) {
  array.forEach(block)
}

Essentially this is like a tree grammar:

parse =
  iterate input, value =>
    check isArray(value)
      call parse(value)
    check isNumber(value)
      check isEven(value)
        push(value)

You can imagine this as JSON:

{
  type: 'function',
  name: 'parse',
  children: [
    {
      type: 'iterate',
      children: [
        {
          type: 'check',
          checker: 'isArray',
          children: [
            {
              type: 'call',
              function: 'parse'
            }
          ]
        },
        {
          type: 'check',
          checker: 'isNumber',
          children: [
            {
              type: 'check',
              checker: 'isEven',
              children: [
                {
                  type: 'push',
                }
              ]
            }
          ]
        }
      ]
    }
  ]
}

Basically my real problem is I have something like this JSON in a custom programming language, and I want to convert it into a state machine rather than the nested/recursive function calls like we started with in this post.

So my question is, how can you rewrite the recursive function system from the beginning of this post, such that it uses a state machine instead?

You don't have to go all the way and make the state machine from the JSON (which in this example is incomplete, as I don't have a good example to simplify from).

Instead, just if you could show how to rewrite this recursive or nested function system from the beginning of this post as a state machine, that would be all.

What I am imagining is something like this:

let transitions = [
  function1,
  function2,
  ...
]

let transition = transitions[0] // the start function in theory
let currentInput = input
let someStack = []
while (transition) {
  [currentInput, transition] = transition(someStack, currentInput)
}

But I quickly get lost on how I would keep track of the position we are at in the input, as well as how to handle things like iteration (as in the iterator), or "if statements" (as in the check).

function iterate(input) {
  // you can't do the recursive call here,
  // as the original while loop should handle this.
  // but, we should return some reference to the current input,
  // as well as the next transition.
}

If a language must be chosen then doing this in JavaScript would be simplest. But it can even be done in pseudocode to show generally how it's done.


Solution

  • As it was said, you can not use a state machine, but if I understand your example, you do not need one. You want a way to construct predicates from data; those predicates will work on a complex irregular data structure.

    • Some of those predicates will have side effects.
    • Some of those predicates will propagate the operation on sub parts of the data.
    • I'm assuming you have a finite way to express your predicates, so you can compose a finite set of predicates to obtain your desired ones.

    If Java streams had a 'recursive' flatMap, that would be enough.

    I'm a Java expert so I will write an attempt in Java. The main idea is to have an Op functional interface that maps an Object into a list of objects.

    Then you just compute a fix-point.

    interface Op{ List<Object> of(Object t); }
    class A{
      Op push(List<Object> res){
        return o->{res.add(o);return List.of();};
        }  
      Op isArray(){
        return t->{
          if (!(t instanceof Object[] os)){ return List.of(); }
          return List.of(os);
          };
        }
      Op isNumber(Op more){
        return t->{
          if (!(t instanceof Integer i)){ return List.of(); }
          return more.of(i);
          };
        }
      Op isEven(Op more){
        return t->{
          if (!(t instanceof Integer i) || i%2==1){ return List.of(); }
          return more.of(i);
          };
        }
      void engine1(List<Op> query,Object input){//not recursive
        List<Object>edge=new ArrayList<>();
        List<Object>nextEdge=new ArrayList<>();
        edge.add(input);
        while(!edge.isEmpty()){
          for(var e:edge){
            for(var f:query){
              nextEdge.addAll(f.of(e));
              }
            }
          edge=nextEdge;
          nextEdge=new ArrayList<>();
          }
        }
      void example1() {
        List<Object> res=new ArrayList<>();
        List<Op> query=List.of(isArray(),isEven(push(res)));
        Object input= new Object[]{1,2,new Object[]{3,4},5,6};
        engine1(query,input);
        System.out.println(res);
        }
      public static void main(String[]arg) {
        new A().example1();//this would print 2,6,4
        }
      }
    

    It is unclear from your question if this order is acceptable for you. That is, the visit side effects are Breadth First If not, I think I can get the Deapth First order instead, but it will have to be in 12 hours or so, since it is now getting late in my timezone.