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

1

3

My current project uses AST with 40 different types (discriminated 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 behavior 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?

Blacklist answered 23/3, 2011 at 11:46 Comment(2)
According to this answer it is not possible to split your dependend functions across files: #5396965Indented
Thanks for the link - yes I read that this is impossible, but thought somebody knows a smart workaround for thisBlacklist
S
6

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.

Stamin answered 23/3, 2011 at 14:8 Comment(1)
Thank you, this is exact what I was looking for - As an example I have to parse and calculate following Expression: "if (c > 0) then 1 + (a[(b > 0) ? b * 2 - 1 : 0] + 1) * b else 2". In this example there are several cyclic dependencies: c can be an Expression, array a can have several predicates, with index also calculated as Expression, parentheses in arithmetic calculations are also use recursion, etc. Btw, nice book - it helps me a lot in understanding F#Blacklist

© 2022 - 2024 — McMap. All rights reserved.