Problem with cyclic dependencies between types and functions from different files in F#

StackOverflow https://stackoverflow.com/questions/5404735

  •  28-10-2019
  •  | 
  •  

Question

My current project uses AST with 40 different types (descriminated unions) and several types from this AST has cyclic dependency. The types are not so big, therefore I put them in one file and applied type ... and ... construction for mutually dependent types.

Now, I'm adding functions to make some calculations under each element in AST. Since, there are a lot of functions with several lines of code in them, to make source code cleaner to read, I've separated these functions in different files.

It's Ok in the case when cyclic dependency is absent, also works when dependent functions are in the same file - in this case I can use let rec function1 ... and function2 ... construction

But it will not work in my case.

Also I incorrectly thought, that signature files can help me with this, but their behaviour differs from C++ - they are used to define functions/types access mode (internal/public), also functions/types comment header can be added here...

The only possible solution I see - is to move all functions to one file and use let rec ... and ... and ... and ... and ... construction

Possible somebody has different ideas?

Thanks in advance.

Was it helpful?

Solution

As mentioned in the comments, there is no way to split functions (or types) with cyclic dependencies between multiple files. Signature files are useful mainly for documentation purposes, so they won't help.

It is hard to give some advice without knowing what exactly the dependencies are. However, it may be possible to refactor some part of the implementation using functions or interfaces. For example, if you have:

let rec process1 (a:T1) = 
  match a with
  | Leaf -> 0
  | T2Thing(b) -> process2 b

and process2 (b:T2) = 
  match b with 
  | T1Thing(a) -> process1 a

You can modify the function process1 to take the second function as argument. This makes it possible to split the implementation between two files because they are no longer mutually recursive:

// File1.fs
let process1 (a:T1) process2 = 
  match a with
  | Leaf -> 0
  | T2Thing(b) -> process2 b

// File2.fs
let rec process2 (b:T2) = 
  match b with 
  | T1Thing(a) -> process1 a process2

If you can find some more clear structure - e.g. two blocks of functions that contain logically related functions and need to access each other, then you can also define an interface. This doesn't make much sense for the example with just two functions, but it would look like this:

type IProcess2 = 
  abstract Process : T2 -> int

let process1 (a:T1) (process2:IProcess2) = 
  match a with
  | Leaf -> 0
  | T2Thing(b) -> process2.Process b

let rec process2 (b:T2) = 
  let process2i = 
    { new IProcess2 with 
        member x.Process(a) = process2 a }
  match b with 
  | T1Thing(a) -> 
    process1 a process2i

Anyway, these are just some general techniques. It is difficult to give a more precise advice without knowing more about the types you're working in. If you could share more details, perhaps we could find a way to avoid some of the recursive references.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top