[HN Gopher] Cyclic dependencies are evil (2013) ___________________________________________________________________ Cyclic dependencies are evil (2013) Author : nojito Score : 94 points Date : 2021-01-30 15:48 UTC (7 hours ago) (HTM) web link (fsharpforfunandprofit.com) (TXT) w3m dump (fsharpforfunandprofit.com) | layer8 wrote: | The generic way to remove a cyclic dependency is by Dependency | Inversion (not to be confused with Dependency Injection), so that | a mutual dependency _A_ <-> _B_ is transformed into the | noncircular set of dependencies { _A_ - > _BIntf_ , _BImpl_ - > | _BIntf_ , _BImpl_ - > _A_ }. That is, at least one node in the | dependency cycle is split into interface and implementation, | breaking the cycle. | | Compilers for languages like C# and Java can deal with | (seemingly) circular dependencies within a set of classes to | compile, because when _A_ uses _B_ they only have to consider the | interface (type signature) of _B_ , not its implementation. | (Cyclic dependencies _between_ modules are still a problem, in | particular for build systems.) | | I'm not really familiar with F# (so someone please correct me if | necessary), but in languages that use type interference to | determine function signatures, one consequence is that the | interface (type signature) of the function then depends on its | implementation, so you can't as easily invert the dependency as | when you have explicitly declared type signatures. | squibbles wrote: | I still cringe when I think back to some C projects from long | ago, where the dependency chains of headers and libraries where | unfathomably byzantine. | | On a related note, lately I have been looking into some | biochemistry topics and their relation to computer science. It | seems that cyclic dependencies are possibly a requirement for | life, which makes faithful simulations of biochemical processes | an interesting challenge. | jimmaswell wrote: | The C language and gcc are a cyclic dependency too. | ReactiveJelly wrote: | Only if you don't consider time, or optional dependencies. | GCC 10 _can_ be built with GCC 10, but it was probably built | with GCC 9 the first time around. | | If you can bootstrap it, and you can, then it's not really a | hard, unbreakable cycle, right? It's just an option that's | quicker than starting from tcc or whatever every time. | nyberg wrote: | But how would you build the first GCC in that chain? | pjmlp wrote: | Using another C compiler, if none exists, create one in | Assembly or other programming language in the target | platform and go from there. | dilawar wrote: | That is an interesting observation. I wonder whether the | cycling in chemistry is more about information flow then like | (negative?) feedback, it helps in making a stable system. | mattgreenrocks wrote: | I'm a compiler nerd at the moment. I have an inherent trust of | things that bootstrap themselves, as if it means they are | conceptually more pure. I'm not sure whether that is 100% well | founded, but it sounds similar to some of these observations. | :) | ReactiveJelly wrote: | I think self-hosting is just a little over-rated for | languages. It's good that Rust and C++ are self-hosting, | you'd expect those to be languages you can write a compiler | in. | | But Lua and JavaScript satisfy their own niche just fine | without having popular runtimes written in themselves. | mattgreenrocks wrote: | Most languages don't aspire to be so conceptually-complete | that they insist on being bootstrapped. Which is fine, I | make my living with them. | | Those that do, however, are immensely beautiful and there | is more to be learned from them. | TheBrokenRail wrote: | There are a few JS interpreters written in JS, don't know | about Lua. | lazulicurio wrote: | In a related vein, is there any sort of theoretical transform | that can be used to convert cyclic dependencies into a tree? | Similar to how lambda lifting eliminates free variables. (Other | than the obvious one of treating the entire cycle as one node) | qudat wrote: | For my FE projects I employ a module based code structure. If you | import a module, you get the whole thing. This lends itself to | many positives but one negative is it makes it easy to run into | circular dependencies. I wrote an article discussing my | architecture and how I solve circular dependencies in js. | | https://erock.io/scaling-js-codebase-multiple-platforms/ | | In the article I argue that circular dependencies are a good | thing because they uncover poor code organization. | Smaug123 wrote: | EDIT: In fact I appear to have paraphrased the next post in the | series, https://fsharpforfunandprofit.com/posts/removing-cyclic- | depe... . Probably go and read that instead. | | One pattern this nudges you towards is having a Domain.fs at the | top of each project. Since everything in your project wants to | talk in terms of the data types you've modelled, it makes sense | to put them all in the same place at the top. Moreover, once | you've done that, you're guided towards having your data types | really being "dumb" - algebraic data types only - because to put | actual behaviour into them is harder when all you have is | definitions of data types (and no associated modules). You | certainly _can_ write OOsagne using the Domain.fs pattern, but it | 's much harder and the code really smells when you do (because | the domain file gets super long). | | The upshot is that your domain model appears explicitly at the | start of the project, which is a big win for anyone who comes | into the project and needs to learn quickly what's going on. By | contrast, nearly all the C# I've ever come across has the domain | spread across many files and all mixed up with implementation | details. | | This is certainly not the only way to write F# - I've written | projects which have the domain spread across multiple files - but | it's one nice way to handle a small-to-medium-sized project. | mattgreenrocks wrote: | This is hexagonal architecture. I've yet to see anything quite | as nice as it once I started using it. | | The only downside to hexagonal architecture is that you start | to see how mediocre most architectures are, and how frameworks | often cap how good your architecture can be by virtue of their | structure. Frameworks tend to resist being put on boxes, and | hexagonal architecture tries to put all external dependencies | in boxes. | bob1029 wrote: | Language constraints aside, the real world is not something that | can be cleanly modeled without the notion of circular | dependencies between things. Not very many real, practical | activities can be truly isolated from other closely-related | activities and wrapped up in some leak-proof contract. | | Consider briefly the domain model of a bank. Customers rely on | Accounts (I.e. have one or many). Accounts rely on Customers | (i.e. have one or many). This is a simple kind of test you can | apply to your language and architecture to see if you have what | it takes to attack the problem domain. Most approaches lauded on | HN would be a messy clusterfuck when attempting to deal with | this. Now, if I can simply call CustomerService from | AccountService and vice versa, there is no frustration anymore. | This is the power of reflection. It certainly has other caveats, | but there are advantages when it is used responsibly. | | If you want to understand why functional-only business | applications are not taking the world by storm, this is the | reason. If it weren't for a few "messy" concepts like reflection, | we would never get anything done. Having 1 rigid graph of | functions and a global ordering of how we have to compile these | things... My co workers would laugh me off the conference call if | I proposed these constraints be imposed upon us today. | monocasa wrote: | You would forget everything about a customer if they closed | their last account? | Smaug123 wrote: | No, of course you would maintain a Set<Customer> as well | (possibly even a Map<Customer, Account Set> for efficiency). | ImprobableTruth wrote: | ... if you model it that way you're not using cyclic | dependencies at all and this is in fact exactly how you | would model it in a functional way. | monocasa wrote: | Their whole premise is that | | > Customers rely on Accounts (I.e. have one or many). | | I'm giving a domain relevant counter example to that | premise. | | And interestingly enough, that leads you to a clean way of | modeling it without the cyclic dependency as you've just | done. | Someone wrote: | "Customers rely on Accounts (I.e. have one or many)" | | Alternative model: the Customer relation couples a Person to an | Account. | | I think that's a better model, as it allows Person P to be a | Customer at bank B but not at Bank C, Person Q to be a Customer | at both B and C, etc. | | It also allows you to model Persons before you model Accounts, | breaking the circularity, allows Companies to become Customers, | etc. | Ma8ee wrote: | In a relational database you would have a CustomerAccount table | keeping track of which accounts that belongs to which | customers. Customer would not know about Account, and vice | versa. | | And what has reflection to do with this? | gorky1 wrote: | Reflection allows you to put the interfaces to Account and | Customer into a shared module and the business logic | implementations into their own mutually independent modules, | thus removing the cyclic compile time dependency. | | You "wire up" the implementations at runtime, using | reflection. | Ma8ee wrote: | I still don't get it. I put the interface into a shared | module and the implementations in independent modules | without reflection. Why exactly would we need reflection to | do that? | Smaug123 wrote: | If I may, that sounds horrifying. Please, for the sake of | my sanity and the IDE, always do things at compile time | rather than runtime if you can! | ldlework wrote: | If the code is implementing against interfaces, why would | late binding mess up your IDE experience at all? | gorky1 wrote: | I've seen people learning about dependency inversion from | a book called "Clean Architecture", then proceeding to | apply it to every bit of code they write, to make it | "clean". It makes code difficult to trace by reading | alone. Indirection may be cheap, but it adds up. | Smaug123 wrote: | My heart sinks whenever I need to spend ten minutes | trying to work out which of the three different | implementors is actually going to be called on a | particular code path. For about a week of December, my | work was solely spelunking to track down which | implementations of an interface in C# were dead code and | so on. | gorky1 wrote: | It's called the "dependency inversion principle". People | don't like to call it "reflection", and heap various | layers like XML or dependency injection annotions on it, | but technically, it comes down to reflection at runtime, | as far as I've seen. | | It certainly has its cost in additional complexity | through indirection, but it's better than creating cyclic | dependencies or giant balls of mud. | | https://en.m.wikipedia.org/wiki/Dependency_inversion_prin | cip... | Ma8ee wrote: | No, you don't need reflection or annotations to use | dependency inversion. Why do you think so? | gorky1 wrote: | The question was "what has reflection got to do with it". | I've used reflection for dependency inversion, so I think | that's what it's got to do with it. | | If you can do dependency inversion without reflection, | more power to you :-) We can't do classpath scanning in | the project I'm working on because of the size of the | classpath, and compile time configuration using direct | imports would introduce cycles, so reflection it is for | us, in one form or another. | brundolf wrote: | I think the OP is overly dogmatic and I agree with you that | sometimes you just need circular dependencies | | That said, I don't see what circular dependencies have to do | with reflection, much less a functional style? For a trivial | example, Rust lacks reflection but the following (functional- | ish) code works just fine: mod ModA { | use crate::ModB::B; pub struct A { | ref_to_b: Option<Box<B>> } | pub fn into_b(a: A) -> Option<Box<B>> { a.ref_to_b } } | mod ModB { use crate::ModA::A; | pub struct B { ref_to_a: Option<Box<A>> | } pub fn into_a(b: B) -> | Option<Box<A>> { b.ref_to_a } } | | Am I missing something? | Smaug123 wrote: | Could you give an example of a domain that naturally requires | circular dependencies? I have been trying lately to practice | seeing outside the functional-programming-tinted lenses, but | I need a bit of help to do so :) | brundolf wrote: | I think the GP's example is pretty reasonable. And like I | said, I still don't really see what this has to do with FP. | Clearly F# has trouble with it, but so does C++ | Smaug123 wrote: | I don't think the GP's example is reasonable, and nor | would an SQL-writer. Could you give something I will find | it harder to wriggle out of? | IggleSniggle wrote: | But functional-only business applications already did take the | world by storm! Relational DBs is the defacto way data is | organized, and a Junction table is the way you model the data | so that a functional query (generally in SQL) can be made | against it. | username90 wrote: | SQL is not functional, it mutates global state. | perl4ever wrote: | SELECT? | dataflow wrote: | I actually don't follow your example. In your example what | exactly would CustomerService be responsible for that prevents | it from functioning unless it has a reference to | AccountService? AccountService is pretty obvious (add/remove | money, link other accounts, close account, etc.) but I'm | struggling to see what CustomerService would do on a per- | customer basis that it would need a reference to AccountService | for. The only thing I can imagine is a get/add/remove accounts | operation, but (a) you need nothing but an account ID for that | (not the actual AccountService class), and (b) you can (and I | think perhaps should?) model this as a database table managed | by the Account class where you're associating customer IDs with | account IDs. | valand wrote: | In the functional paradigm, language constraints aside, to | model this "story" would need these: | | - The definition of Account | | - The definition of Customer | | - A ledger consisting of: List of Account, List of Customer, | List of Account-Customer relations | | A clerk works on the records on the ledger with one hand and | one pen, a metaphor for the service process working on the data | in the database. | | An act, such as money transfer, would be described as a | function. | | A customer creating a new account for himself is written as `fn | createAccount(Customer, NewAccountData)` because from the | perspective of the clerk/bank manager/service the customer, | newAccountData, and the existing data in the ledger as objects | which the clerk/bank manager/service must move around in a | precise way. | | The module which has 'fn createAccount` depends on the types | `Account` and `Customer`. | | In english it roughly sounds like, | | "the success of writing the rule of creating an account for a | customer depends on knowing the definition of Account and | Customer." | | The function is not written as | `customer.accountService.createAccount(newAccountData)`, | because the clerk doesn't schizophrenically pretend to be the | customer and create an account for himself. The clerk just | receive request from the actual customer, writes new account | entry and customer-account-relation entry into the ledger, | that's it. | | There's simply no need to call CustomerService from | AccountService and vice versa. There's no need for reflections | because data types are all available at compile time. | piva00 wrote: | > `customer.accountService.createAccount(newAccountData)` | | I believe that most OO implementations would read | accountService.createAccount(customer, newAccountData) | | Care to elaborate if that was the main point of the clerk's | schizophrenia criticism? Or if I'm misinterpreting just call | me dumb, haha. | valand wrote: | Oh, I meant to emphasis on that "customerService depends on | accountService and vice versa" thingy. While in my writings | "accountService is a member of customer" is a form of | customer depending on accountService. | | But my main point is that it should not be written that way | at all. | | Semantically, if we must include the subject, it should be | | `theService.createAccount(Customer, NewAccountData)` | | Or replace theService with CustomerAccountService or | whatever. | | Writing that way, with functions depending on types, avoid | getting tangled from the so-called account-customer | cyclical dependencies. Because account and customer don't | need to know each other until a function needs to know both | of them. There's no such thing as `customer.getAccounts()` | because in the end the query would roughly look like | `getAccountsWhereCustomerIs(CustomerId)`. | | You're not dumb. It's just me mis-writing due to the fact | that I'm writing this at 4 in the morning lol. Or maybe I'm | misinterpreted parent comment and that confuses you. In | that case, I'm the dumb. | kohlerm wrote: | circular dependencies at runtime are not really a problem and | often necessary. circular dependencies at build time/between | modules are evil. | jonhohle wrote: | Your build time is someone else's runtime. | zaphar wrote: | Sometimes I read conversations about software engineering | concepts by career developers and I get this sense that there | is this entire hidden world of other engineers that I have | somehow managed never to encounter. | | Every engineer I've ever met who has been doing it for more | than a couple years universally decries circular dependencies. | Every one of them has come to that opinion via hard won | experience dealing with real world problems they encountered. | And yet comments like yours reveal there are other engineers | working in places where apparently the opposite is true. | Whenever I see that I wonder how this sort of self sorting | manages to occur. | ironmagma wrote: | Some problem domains have unique characteristics. I think the | closer a problem resembles nature, the more it breaks our | structures we want to impose on it. | dexwiz wrote: | Manifolds? Locally we want everything to be a straight line, | but when you zoom out far enough you see it was a circle all | long. | kayodelycaon wrote: | Possible reason: selection bias. If everyone else is fine | with circular dependencies or at least sees them as a | necessary evil... are they going to extremely vocal about the | current status quo when there they have no need to defend it? | Aeolun wrote: | In my experience, it's generally less experienced engineers | that will happily make a clusterfuck of circular | dependendies. | | If a more experienced engineer does, it's generally | considered a necessary evil, but not a choice taken | lightly. | username90 wrote: | I've worked with very high level engineers at Google that | thought cyclic dependencies are fine. Sometimes they are just | the simplest solution and trying to design abstractions to | avoid it just creates a huge mess. | | If you've working with Java at Google (Guice) makes you learn | to hate hiding circular dependencies behind dependency | injection, since your binary gets injected by some huge tree | of thousands of dependencies all that can create errors or | interfere with each other. And without static checking trying | to reason and fix those issues becomes really hard. | | I strongly prefer using the languages actual type system to | create circular dependencies that you can inspect using well | known tools over that any day, better have a problem you can | see than hide the same problem to satisfy some tools | requirement. | jonhohle wrote: | I think something has been lost along the way with | injection and IoC that makes using a fully functional | programming language for object wiring a compelling | alternative for most engineers. | hrktb wrote: | To me, the parent is describing a functional circular | dependency that we all have to live with, but when we'll | design the system it will be masqueraded in a many to many | association in a third object/table and we'll proudly say "we | have no circular dependencies". | | To me both are technically true, reality is messy, and we | hide the mess under imaginary constructs. | TeMPOraL wrote: | > _Every engineer I 've ever met who has been doing it for | more than a couple years universally decries circular | dependencies. Every one of them has come to that opinion via | hard won experience dealing with real world problems they | encountered._ | | They may not realize they have them - at runtime, in the | object graph, possibly indirectly. There's little difference | in principle between cycles in "static" code vs. runtime | state, but we often can't express them in the former, because | our languages don't specify the concept of dependency at | enough granularity (and some rely on a linear compilation | pass). | [deleted] | titzer wrote: | I don't understand how reflection is involved here? Don't you | just have a data model (schema) that can be interacted with? | Smaug123 wrote: | In F#, you would naturally approach this by defining a Customer | independent of the Account (e.g. just containing a name and | address), and an Account independent of the Customer (e.g. just | containing an ID), and then a Bank which is a mapping of | Account to Set<Customer>. What you see as a cyclic dependency, | I see as a data type that you haven't reified. | piaste wrote: | > and then a Bank which is a mapping of Account to | Set<Customer> | | The problem is that this only lets you get customers from | accounts, and not vice-versa. | | And if you add a Map<Customer, Set<Account>> to the Bank, you | now have to ensure the mappings are synchronized at all | times. Which, to be fair, is very straightforward as long as | Bank is immutable, but still kind of annoying. | | But considering how incredibly common many-to-many | relationships are in business logic and especially in SQL, | I'm constantly surprised that there isn't a de facto | standard, efficient data structure to represent them. That | speaks to how entrenched cyclic relationships are in software | engineering, I suppose. | Smaug123 wrote: | Sure, but you wrap that up if you want. A Bank can contain | both maps, if you want, and you hide the methods that would | update individual maps, only exposing your wrappers that | update everything together. | valand wrote: | In the in-storage form (database), the mappings is | synchronized at all times. | | Many-to-many relationships in the purest form is described | as records of ItemAId/ItemBId pairs. `List<{ account: | Account, customer: Customer}>`. There isn't any cyclical | relationship in this form. | | From `List<{ account: Account, customer: Customer}>` one | can derive `Map<Customer, Set<Account>>` and `Map<Account, | Set<Customer>>`. Bank can have copy of both mappings and | use them as double index, synchronizing both mappings as | operations goes by, while at the same synchronizing both | mappings to the in-storage form. Or not! | | We're talking mostly about data-modelling in a programming | language, which applies to the in-memory form. `Customer { | accounts: Set<Account> }` and A `Account { customers: | Set<Customer> }` is its in-memory form, a layer of | indirection from its actual form, the in-storage `List<{ | account: Account, customer: Customer}>`. | | If informations of Accounts, Customers, and the many-to- | many relationship of both are laid flat in its purest, | source-of-truth form, the in-storage database entries, why | are people suggesting that there is/should be a cyclical | relationship in its representative form, the in-memory | objects? | jonhohle wrote: | Which makes it easy to answer the question "what customers | are on this account" and hard to answer the question "what | accounts does this customer have". As a customer, I usually | want the latter. | im3w1l wrote: | This resembles sql a lot. Btw I'm surprised that few | languages seem to have in their standard library a many-to- | many container which supports efficient lookups in either | direction. | resonantjacket5 wrote: | Actually yeah that is kinda interesting. The couple times | I've had to do it, always ended up hand-rolling a 'class' | with hashmaps in both directions. I wonder why few standard | libraries have a both way look up structure. | iso8859-1 wrote: | Which languages support this at all? Are you talking about | ORM? | im3w1l wrote: | I'm talking about something like this | std::relation<Foo, Bar> foobars; ... | auto& bars = foobars.forward(foo); ... | auto& foos = foobars.backward(bar); | | Edit: Boost apparently has it, see e.g. | https://stackoverflow.com/questions/1128144/c-maps-for- | many-... | gpderetta wrote: | Boost.Bimap is a special case (and built on top) of | Boost.Multiindex that allows indexing a logical table | with an arbitrary subset of fields. | JackFr wrote: | This came up in an application I had modeling college | football. A Team is a member of a Conference and a Conference | is made up of Teams. But during realignment a few years ago | when teams were switching all over the place it became | apparent that Teams and Conferences were truly independent of | each other and that their association itself was a first | class object which also needed to capture the Season/Year. | | Not really rocket science but some times your model is | telling you something if you're willing to listen. | kd5bjo wrote: | This is almost exactly the motivational example that Codd | used when originally describing relational algebra. He | described 5 different data organization schemes for a | single problem and designed relational algebra to work with | all of them. This ability for the same program logic to | work with many different data storage layouts should make | changes like you describe less painful to implement. | | (see page 2) | | E. F. Codd. 1970. A relational model of data for large | shared data banks. Commun. ACM 13, 6 (June 1970), 377-387. | DOI:https://doi.org/10.1145/362384.362685 | | PDF: https://www.seas.upenn.edu/~zives/03f/cis550/codd.pdf | gpderetta wrote: | My dream, when I'll have a few years of free time, is to | design a language were the relational table is the | primary data structure abstraction. | kd5bjo wrote: | I'm working on a Rust library for this as my MS project. | It calculates the queries inside the type system, so | there's minimal runtime cost. | swirepe wrote: | This is a cool idea. What would you need to be able to | try this in 2 weeks, instead of a few years? What's the | lean slice? | kd5bjo wrote: | Relational algebra isn't that hard to implement if you're | willing to sequential-scan all of the time. The massive | complexity comes in the query planner: The point of | adding an index is to change the space-performance | tradeoff of certain operations. The system needs to be | able to take advantage of these data layout changes, or | there's little benefit over storing everything in flat | arrays. | sbelskie wrote: | I'm having a hard time understanding why you think F# is unable | to handle the domain model presented. | mattgreenrocks wrote: | It reads as an extremist take on Chesterton's Fence: "the | fence exists, therefore it is probably the only thing that | could've worked." | | There is nothing in this contrived example that F# or Haskell | (gasp, pure functional) wouldn't handle with ease. It just | would be modeled differently from traditional langs. I'd | argue the alternative modeling forced by FP langs would be | superior. | hutzlibu wrote: | " I'd argue the alternative modeling forced by FP langs | would be superior." | | Then I'd like to see that. The other example of the | functional someone gave more above, was not convincing to | me. Much more complicated then the straight-forward simple, | but evil cycle approach. | danielovichdk wrote: | Customers and Accounts are not cyclic. | | A customer has an account. The customer is the domain root. | Hence there would be no accounts if there were no customers. | | A customer does need to to hve an account. An account must have | a customer. | | It's not cyclic in any way | dgb23 wrote: | I don't quite understand what you're trying to convey with your | example in regards to FP. | | First of all, both data and functions are first class concepts | in the functional paradigm and are not glued together as | classes. So you're not modeling your domain as ,,account | service" and ,,customer service", but rather have data | representation of these concepts and functions that | query/reduce etc on those. In your example that would be a | relational model, described as sets of tuples/maps, and | relational functions do derive new data. There is no need for | circular dependence, because your operations are generic over | relational data. | | I agree with the notion that the purely functional approach | doesn't dominate, for good reason. But more and more languages | are incorporating FP concepts since about a decade or longer, | at least the basic building blocks like closures, immutability | and function composition have gained massive traction and | eliminate the need for many of the complex patterns in | traditional, class based OO. | sbelskie wrote: | I've played around with F# over the years, but have only recently | started getting into seriously. The top down nature of | dependencies (both within a given file and for the files within a | project) is a little odd at first, but once you get used to it | feels quite natural. | | If nothing else, it encourages (somewhat) common ways of | structuring projects and avoids a lot of bike shedding about | separation of concerns and project structure I encounter in C#. | titzer wrote: | Nice article. The first thing that came to mind when seeing the | title was "but circular dependencies just mean that your layers | are wrong and those should all be in the same layer" and funnily | enough that's _exactly_ what 's explained in the article! | | What would be interesting would be to organize modules into named | layers--instead of just explicit 1-by-1 module dependencies (i.e. | edges). I have not seen a language do that yet. | | Another interesting thing that I haven't seen touched on in more | mainstream languages is the idea of bidirectional interfaces. The | somewhat DSL-like NesC language had this, primarily driven by the | need to write device drivers that serviced interrupts (and | events). | klyrs wrote: | > What would be interesting would be to organize modules into | named layers--instead of just explicit 1-by-1 module | dependencies (i.e. edges). I have not seen a language do that | yet. | | Correct me if I'm wrong, but I think you're describing | namespaces here | KingOfCoders wrote: | This was one of the reasons I used lots of Interfaces back in the | day with Java. | username90 wrote: | That doesn't remove circular dependencies though, it just hides | them. | danielovichdk wrote: | Funny to see those layered architectures/designs. | | Tightly coupled dependencies are bad. Cyclic or not. | | And if you do mange to to a cyclic dependency you are not | thinking about your design property. Imo | kstenerud wrote: | This is one of the things that frustrates me about go. They | started with this very good advice and then took it one step | stupider: Disallowing circular imports. | | Why is this bad? It makes it impossible in many situations to | locate your public API at the top level. Let's say you have a | Widget interface and APIs to do things with those Widgets. | Eventually your library gets complex enough that you want to | separate functionality into subdirs. Now you have a problem: You | can't access the definition for Widget from the subdir because | importing the top level would create a circular import! | | The only way to get around this problem is to have no code at the | top level, and put your public API in a subdir. Just be sure to | do this when first starting your library or you'll have to break | things. | caylus wrote: | You can deal with that issue by defining things in a submodule, | then importing and re-exporting them in the top-level (e.g. | `type Foo = subdir.Foo`). That even lets you move things into | the submodule without breaking callers. Other submodules can | then import from the submodule rather than the top-level. | xiphias2 wrote: | This is how libraries are exported inside Google (use /public | subdir for the public interface), and I guess with Go this got | into the language. But I think Go made much worse things than | this :) | codazoda wrote: | I've never tried F# so I'm not sure I'm exactly "on base" here, | but I actually _think_ this way. It 's frustrating to me when | dependencies happen automatically or when they can be used before | they are declared. | | It helps me understand what's included if I can see a nice list | of all the dependencies. It helps me narrow down where the | problem is if I only have to address dependencies I know have | already loaded. | | I learned to program in the 80's however, and it probably is a | bit "old school". ___________________________________________________________________ (page generated 2021-01-30 23:01 UTC)