Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to declare an immutable graph with circular references?

I want to declare a graph of all states where the edges represent contiguous states. I think what I am trying to do might be called "tying the knot" (not sure about that though). It's not working like I expected, and I have a couple of questions.

First, I want a State type that has a string name and a list of contiguous states. But this declaration gives compiler error "...immediate cyclic reference...":

type State = string * (State list)

This way works:

type State(name:string, contigs: (State list)) = 
  let name = name
  let contigs = contigs

But it's really not a requirement to name the members. A tuple is fine. How can I make that terse syntax work?

Second, the following code attempts to declare what should be three graphs of contiguous states (HI and AK are graphs consisting of a single node, all the remaining states constitute the last graph), followed by a list of all nodes. (For brevity I've only actually declared a handful of states here):

let rec hi = State("hi", []) 
and mo = State("mo", [il ia]) 
and il = State("il", [mo]) 
and ia = State("ia", [mo])
and states = [hi,mo,il,ia]

This gives a variety of errors though including "mo will eventually be evaluated as part of it's own definition" and "expression was expected to have type 'a->'b but here has type State". I thought the 'rec' and 'and' keywords would allow this to work. Can I define this self referencing graph? If so, how?

like image 894
Josh Buedel Avatar asked Feb 03 '23 09:02

Josh Buedel


2 Answers

The problem is your data structure and using invalid list element delimiters (should be semicolon). This works: (see edit)

type State =
  | State of string * State list

let rec hi = State("hi", []) 
and mo = State("mo", [il; ia]) 
and il = State("il", [mo]) 
and ia = State("ia", [mo])
let states = [hi; mo; il; ia]

Recursive references will be materialized as thunks (lazy). So you could, with a bit more typing do the same thing yourself with mutable lazys--just FYI--what you have is idiomatic.

EDIT

Intellisense didn't have a problem with it, but the compiler says

Recursive values cannot appear directly as a construction of the type 'List`1' within a recursive binding. This feature has been removed from the F# language. Consider using a record instead.

You can fix this by using seq instead of list.

type State =
  | State of string * State seq

let rec hi = State("hi", []) 
and mo = State("mo", seq { yield il; yield ia }) 
and il = State("il", seq { yield mo }) 
and ia = State("ia", seq { yield mo })
let states = [hi; mo; il; ia]
like image 125
Daniel Avatar answered Feb 04 '23 23:02

Daniel


Although what Daniel says is correct I would contest the assertion that it is "idiomatic" because that does not produce a very useful data structure for representing graphs in the general case. Specifically, it only permits the addition of new vertices and edges from them but not adding or removing edges between existing vertices. In particular, this basically means your graph must be statically defined as a constant in your source code so you cannot load such a graph from disk easily.

The idiomatic purely functional representation of a graph is to replace dereferences with dictionary lookups. For example, represent the graph as a Map from vertices to Sets of vertices to which there are edges:

> let g =
    Map["hi", set[]; "mo", set["il"; "ia"]; "il", set["mo"]; "ia", set["mo"]];;
val g : Map<string,Set<string>> =
  map
    [("hi", set []); ("ia", set ["mo"]); ("il", set ["mo"]);
     ("mo", set ["ia"; "il"])]

For example, you can lookup the vertices directly reachable via edges from mo like this:

> g.["mo"];;
val it : Set<string> = set ["ia"; "il"]

This is easier to debug than the mutable representation but it has significant disadvantages:

  1. Lookup in a purely functional dictionary like Map is at least 200× slower than dereferencing a pointer for traversing graphs (according to a quick test here).

  2. The garbage collector no longer reclaims unreachable subgraphs for you. The imperative solution is to use a weak dictionary but there are no known purely functional weak dictionaries.

So this is only feasible if performance and leaks will not be a problem. This is most commonly the case when your graphs are small or static.

like image 44
J D Avatar answered Feb 04 '23 23:02

J D