[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)