20-CS-4003-001 Organization of Programming Languages Fall 2018

Lambda calculus, Type theory, Formal semantics, Program analysis

    Prev     Next     All lectures           Code

Topological Sort - Manage State Transitions

{- Compute new state of DFS from current state -}
f::[[Int]] -> [[Int]]
f state = y
        {- no root vertex: empty state returned -}
    y = if (length(head(state)) == 0) then
        {- root depends list empty:
           remove root from vertex stack -}
        else if (lv == []) then 
          [(tail (head(state))) ] ++ 
          (drop 1 state)
        {- root depends list has single vertex:
           add root to solution list,
           make depends list empty, 
           remove root from vertex stack -}
        else if (length(lv) == 1) then
          [(tail (head(state))) ] ++
          (drop 1 (take (length(z)-1) z)) ++
          [ v:last(state) ]
        {- root depends list lv has > 1 vertex:
           head of lv is new root vertex,
           head of lv removed from lv -}
          [(take 1 (head (drop (v+1) state))) ++ 
           (head state) ] ++ 
          (drop 1 z)
            {- root vertex -}
            v = head(head(state))
            {- root's dependency list lv -}
            lv = (head (drop (v+1) state))
            {- remove 1st vertex in lv -}
            r = (drop 1 lv)
            {- put 1st of lv on the vertex stack
               replace lv with r -}
            z = (take (v+1) state) ++ 
                [r] ++
                (drop (v+2) state)

{- Perform a DFS until vertex stack is empty -}
h::[[Int]] -> [[Int]]
h state =
  if (head(state) == []) then state
  else h(f(state))

{- Set initial root vertex -}
setInit::[[Int]] -> Int -> [[Int]]
setInit state i = [[i]] ++ (drop 1 state)

{- Perform the topological sort -}
topo::[[Int]] -> [Int]
topo inp = last(last y)
    state = [[]] ++ inp ++ [[]]
    y = [state] ++ 
        [ h(setInit x i) 
        | x <- y 
        | i <- [0..(length(state)-3)] ]

{- small example -}
ex = [[2,3,4,0],[1],[1,2],[1,5,3],[3,5,4],
 -  This example illustrates how to manage state changes during execution of a program. Imperative languages like C++ manage state changes through assignment statements. Haskell does not have assignment statements. A reasonable solution to the problem of managing changing state is to create a tail recursive function that computes the new state and passes the new state as an argument to the next recursive call of the function where the next state is computed. This is done here in developing a program that computes a topological sort of a partial order. A few definitions are presented and then a description of the code.

A partial order is a relation R on a collection of vertices such that there is no cycle. For example, v1 R v2, v2 R v3, ... vn R v1 is a cycle. If v1 R v2 then v1 is said to directly depend on v2.

A total order is a list [v1,v2,v3...] of vertices such that, for any i < j, vj R vi is false.

A state contains three parts

  1. A stack of vertices, the leftmost of which is called the root vertex. A total order is found for a given partial order, in whole or in part, via a depth-first search beginning at some root vertex.
  2. A list of lists, one for each vertex, in order of vertex index. Each list contains the dependencies of the associated vertex and ends with the vertex number itself.
  3. A solution vector, initially empty. This will contain a total order of all vertices in the given partial order.
See the small example ex to the left for an example of state: the root vertex is 0, vertices are numbers 0-7 and their dependency lists are given by the middle 8 lists in the state, the last list is the solution list which is empty. When the topological sort is finished the state looks like this:

Function f makes one step of the depth-first search and returns the corresponding new state given some previous state. The input to f is a state. The output of f is a state derived from the input state in one of four ways.

  1. If there is no root vertex, the empty state is returned
  2. If there is a root vertex v but the dependency list associated with v is empty, then v is removed from the vertex stack.
  3. If there is a root vertex v and the dependency list associated with v contains one vertex number, then v is placed at the head of the solution list, the dependency list becomes empty, and v is removed from the vertex stack. Execution of this step means that all dependencies of v have been added to the solution list and search has backed up to v so it is safe to add v to the solution.
  4. If there is a root vertex v and the dependency list Lv associated with v contains more than one vertex, then the head of Lv becomes the root vertex and is placed on the vertex stack and that element is removed from Lv.

The following are some sample values to variables in f:

 state = [[0,2],[1,2,3],[4,5,6],...,[10,11]]
 vertex stack = [0,2]
 dependencies = [[1,2,3],[4,5,6],...]
 solution list = [10,11]
 v is 0  (the root vertex)
 lv is [1,2,3]  (dependency list for v)
 r is [2,3]  (remove first vertex of lv from lv)
 z is [[1,0,2],[2,3],[4,5,6],...,[10,11]]  
   (put first of lv on stack, replace lv with r)

Function h makes a depth-first search for dependencies given a particular root vertex. Input to function h is a state. The output of h is a state with the last list giving a topological ordering of all vertices that root vertex v depends on plus v and such that the presence of all vertices in the solution list is removed from the state dependency lists, provided a root vertex v exists. If there is no root vertex, the input state is returned without change.

Function setInit sets an initial root vertex in a state that is presumed to have an empty vertex stack. The input to setInit is a state and a vertex number v. The output is exactly the same state as the input except that the vertex stack consists solely of v.

Function topo performs a topological sort on a partial order. The input to topo is a list of dependency lists (a partial order). A state is creating by preceding and appending the input with [[]], the initial empty vertex stack and empty solution vector. The output is a total order of vertices from right to left. The method used is to apply function h to all root vertices 0,1,...n. The output of each call to h is a state that has a topological sort of vertices up to the current one, plus their dependencies. That state is input to the next call to h along with the next higher root vertex number.