Hacker News new | ask | show | jobs
by danyx 1963 days ago
In an F# project file (and thus also in all editor support), the referenced F# source files have an order. Files can only see types, values etc defined in files above them (and the same within files, with some caveats). This sounds really weird and annoying but turns out to have some surprising benefits (because it limits the mental/real search space for definitions as well). In practice it's just something that is surprising at first, then totally fine.
2 comments

This guarantees that there are no cycles, but there are more ergonomic ways to do that. And the change would be backwards compatible.

I wish languages didn't insist on preserving quirks like this one.

That is pretty nice for testing, since it removes the need for depency injection, or am i mistaken?
Not really. There are various DI patterns you can use in F# [1], but the combination of no cyclic dependencies + type inference does mean you can just do the simplest thing - pass dependencies manually as specific arguments - and it will stay manageable for much longer than in java/c#.

Evil dangerous code using global variables:

   let mail = // .. create email service
   let db = // .. create database service

   let receiveThing thing = async {
      let query = // .. compose update query
      do! saveToDb db query
      let emailText = // .. compose email
      do! sendMail mail emailText
   }

   while readInput() do receiveThing (getThing())
Beautiful pure code using dependency injection:

   let mail = // .. create email service
   let db = // .. create database service

   let receiveThing db mail thing = async {
      let query = // .. compose update query
      do! saveToDb db query
      let emailText = // .. compose email
      do! sendMail mail emailText
   }

   while readInput() do receiveThing db mail (getThing())

[1] https://fsharpforfunandprofit.com/posts/dependencies/
All DI libraries can be ripped out and replaced with a bog standard main method, and it doesn't even require much skill. Most of what it's doing for you is stuff like "foo = new Foo(); bar = new Bar(foo, 33);" So there's no strict "need" for them, they help with (and enable) complexity.

What F#'s aversion to cyclic dependencies does forces you to break cycles of types referring to types referring to types. The way you do that is by writing a generic function that avoids referring to a concrete type. Because it's generic, such a function is inherently easier to test by means of a simple stub value.