This page is not created by, affiliated with, or supported by Slack Technologies, Inc.
2024-04-25
Channels
- # announcements (4)
- # beginners (26)
- # calva (18)
- # cider (24)
- # clojure (35)
- # clojure-brasil (9)
- # clojure-dev (6)
- # clojure-europe (39)
- # clojure-madison (1)
- # clojure-nl (1)
- # clojure-norway (100)
- # clojure-uk (6)
- # clojurescript (17)
- # data-science (15)
- # datalevin (5)
- # emacs (1)
- # events (2)
- # introduce-yourself (2)
- # javascript (1)
- # malli (28)
- # missionary (7)
- # off-topic (59)
- # polylith (20)
- # reitit (2)
- # releases (1)
- # remote-jobs (2)
- # rewrite-clj (5)
- # shadow-cljs (27)
- # sql (5)
- # squint (63)
- # xtdb (8)
I have a toddler's question: why is that in the documentation, in poly project and in realworld-example-app the workspace level :extra-deps are prefixed with poly/
, and in user-manager-example they are like this:
:extra-deps {poly-usermanager/web {:local/root "bases/web"}
poly-usermanager/app-state {:local/root "components/app-state"}
...
Is it because these are :local/root deps, and it doesn't really matter, and you can have them like foo/web
, bar/app-state
, etc.?Yes, that's the reason (`poly/` is just a convention). As long as you don't share code outside the workspace, you can use any valid prefix.
Regarding https://clojurians.slack.com/archives/C013B7MQHJQ/p1712118156257669?thread_ts=1712106688.405469&cid=C013B7MQHJQ
I took a look into seancorfield/usermanager-example
and noticed schema
component https://github.com/seancorfield/usermanager-example/blob/polylith/components/schema/src/usermanager/schema/core.clj#L2C14-L2C34 usermanager.database.interface
like you said above
ā¢ schema
doesn't specify the dependency on database
in components/schema/deps.edn
like you said
ā¦ imo it's unexpected, but let's assume it's ok
ā¢ there's {:local/root "../../components/database"}
in projects/usermanager/deps.edn
ā¦ but if I comment it out this single line, I'm still able to run clojure -M:dev -m usermanager.web.main
and the app works
ā¦ building the app clojure -T:build uber
fails
ā¢ there's {:local/root "components/database"}
in root deps.edn
ā¦ if I comment it out, app cannot start with clojure -M:dev -m usermanager.web.main
regardless dependency stated in projects/usermanager/deps.edn
TLDR: effectively for repl/dev only toplevel deps.edn
matters, for the build - only projects/usermanager/deps.edn
, do I miss anything @seancorfield?
imo it's a bit error-prone and non-intuitive when you try to think about components as dependencies, the most expected behaviour (again imo) would be
ā¢ either including :local/root
only in components/schema/deps.edn
and hook/patch into tools.deps
or preprocess/generate projects/*/deps.edn
and toplevel deps.edn
ā¢ or force people to a total DI and pass all database
functions as arguments into schema
(looks too enterprisy tbh)
> ā¢ schema
doesn't specify the dependency on database
in components/schema/deps.edn
like you said
> ā¦ imo it's unexpected, but let's assume it's ok
Bricks (components and bases) only know about interfaces (not other bricks). That's why the concrete implementation of an interface (component) is specified by each project, so that the implementation of an interface can vary between projects.
ok, so this is kinda DI on namespace level? in comparison to DI on argument level when we could rely on actual definterface (including system's component DI being passed through)
On namespace level yes, but they are not injected. The bricks become available when we specify them in a project, in the same way we specify libraries (which are also not injected). I tend to see bricks (and libraries) as living side by side. I try to explain this in the Composable section in https://medium.com/@joakimtengstrand/understanding-polylith-through-the-lens-of-hexagonal-architecture-8e7c8757dab1 blog post.
Here we illustrate how bricks are used across projectsthanks for the article, however interestingly I noticed a "development" project after this line. does it mean there should be no toplevel deps.edn at all (but projects/development/deps.edn
instead)?
The development project is configured in ./deps.edn
and not under projects
as other projects.
Bases can depend on other bases, but that's quite unusual.
ok, got it, the projects/development/deps.edn
-> ./deps.edn
was forced by something like projectile
looking for repl's deps.edn and trying to find files in this subfolder only? or any additional more important reason?
and second question - suppose we really want DI on ns level and use different components (`compA` and compB
providing ns.comp.interface
)
should we have identical fn signatures in compA/src/ns/comp/interface.clj
and compB/src/ns/comp/interface.clj
? (in short there's no defprotocol
on ns-level which declares interface once)
Yes, the signature has to match. See https://cljdoc.org/d/polylith/clj-poly/0.2.19/doc/interface#one-interface-in-multiple-components example.
Since you @'d me...
> but if I comment it out this single line, I'm still able to run clojure -M:dev -m usermanager.web.main
and the app works
Because :dev
is how you specify the development
project and it has "all" the dependencies defined in it. You could also run it via cd projects/usermanager && clojure -M -m usermanager.web.main
> imo it's a bit error-prone and non-intuitive when you try to think about components as dependencies
Depends on your mental model. Polylith talks a lot about "bricks" and assembling projects from those "bricks". At work, we have some components
that have multiple implementations and the project decides which implementation is used (at build time). The development
project can be run with any combination of those implementations but, because it is a build-time decision, you use aliases (`:+default`, etc) to tell the Clojure CLI which implementations to use for building the classpath, that is then used to run your project.
Not having the (implementation) dependencies hard-coded in the bases
or components
is the key to making that possible.
For example, we have an i18n
component that has two completely distinct implementations: one uses a database, the other uses a local JSON file. We have one project that selects the JSON-backed version at build time to produce an app that can be run locally by our UI/UX folks so they can build out new HTML templates and provide the text strings in a local JSON file. That same component (interface) means that all the code that interacts with translations can work identically with both implementations, which improves reuse.
We also have two implementations of our http-client
component: one using hato
that depends on JDK 11+, one using clj-http
that can be run on JDK 8 -- because we have a single project that represents a legacy application that cannot run on JDK 9+.
To give a sense of scale, we have 23 bases, 22 projects (plus development
), and 162 component implementations, with 145k lines of Clojure altogether.
thanks for detailed reply and usecases Sean, I got it, brick != dependency and not self-contained, basically you're also talking about namespace-level-DI as well let me generalize the use-cases: ā¢ for i18n making and passing a "translator" interface would be too invasive? ā¢ for http-client we cannot get a usual DI because there's no interface and classpath for projects and repls should be different anyway
The specifics for i18n is to be able to create an app that has no JDBC or database-related dependencies in it at build time so it produces a minimal JAR file.
And for http-client, the hato-based version will not even load on JDK 8 -- but the clj-http version will load on both JDK 21 (11+) and JDK 8 so we can test both implementations on the more recent JDK if we wish -- but we do not generally want clj-http on the classpath because of its assumed dependencies -- so both implementation choices are build time by necessity.
Since you @'d me...
> but if I comment it out this single line, I'm still able to run clojure -M:dev -m usermanager.web.main
and the app works
Because :dev
is how you specify the development
project and it has "all" the dependencies defined in it. You could also run it via cd projects/usermanager && clojure -M -m usermanager.web.main
> imo it's a bit error-prone and non-intuitive when you try to think about components as dependencies
Depends on your mental model. Polylith talks a lot about "bricks" and assembling projects from those "bricks". At work, we have some components
that have multiple implementations and the project decides which implementation is used (at build time). The development
project can be run with any combination of those implementations but, because it is a build-time decision, you use aliases (`:+default`, etc) to tell the Clojure CLI which implementations to use for building the classpath, that is then used to run your project.
Not having the (implementation) dependencies hard-coded in the bases
or components
is the key to making that possible.
For example, we have an i18n
component that has two completely distinct implementations: one uses a database, the other uses a local JSON file. We have one project that selects the JSON-backed version at build time to produce an app that can be run locally by our UI/UX folks so they can build out new HTML templates and provide the text strings in a local JSON file. That same component (interface) means that all the code that interacts with translations can work identically with both implementations, which improves reuse.
We also have two implementations of our http-client
component: one using hato
that depends on JDK 11+, one using clj-http
that can be run on JDK 8 -- because we have a single project that represents a legacy application that cannot run on JDK 9+.
To give a sense of scale, we have 23 bases, 22 projects (plus development
), and 162 component implementations, with 145k lines of Clojure altogether.