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
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.