Think of zk proofs as an extension of asymmetric cryptography to arbitrary logic.
A zkVM makes encoding the arbitrary logic as easy as writing a normal Rust program.
A proof is a probabilistic statement that a specific program run with some public inputs and maybe some private inputs was executed correctly.
For crypto zk proofs are mostly for their succinctness property, not for privacy.
Outside of crypto privacy is the more important property, let's say a government issues a signed document, I could prove any arbitrary statement about that document without revealing the document. We could use zk proofs for anything we use canonical documents for today and we would gain privacy. Will we do this, probably not, but it would be an improvement.
This is such bad advice that I honestly couldn’t tell if it was a parody or not until I read the comment section—it’s not.
Attempting these design patterns is a common part of getting over OOP when new to Rust.
The result: over-abstracted, verbose, unmaintainable C++/Java written as Rust.
Every layer of indirection ossifies the underlying concrete implementations.
The abstractions inevitably leak, and project velocity declines.
I have seen the same types and logic literally copied into three different repositories in the name of separation of concerns.
Luckily people usually get over this phase of their Rust career after a couple of failures.
If you’d like to skip that part, here are a few rules:
1. Always start with concrete types.
Don’t abstract until you have at least two, preferably three, concrete implementations.
> This is such bad advice that I honestly couldn’t tell if it was a parody or not until I read the comment section—it’s not.
There is a large body of content on why concepts discussed in the article are championed. And also a large body of content on how they are misused (and I agree that can be - even to a huge degree).
So while, I think it is fine to judge that ascribed benefits are not worth the cost (or are not even realizable in a typical development team). Or argue why the benefits of an architecture like this doesn't work for Rust in particular - which may be the case since many of these patterns are oriented towards design in the enterprise applications space. But ascribing the approach as being a "parody" is not at all constructive.
The patterns of hexagonal architecture isn't in any way coupled to OOP even if some of the terminology is highly aligned with languages in the OOP space. In fact the well regarded (at least by me) Mark Seeman has an article how Ports and Adapters, another name for Hexagonal Architecture, is inherently functional [1]. And this resonates with my experience.
I have seen the pattern implemented well across: Python, Typescript, Javascript, .Net, Scala, and Go. And while the systems languages such as: C, Rust, and beyond are quite distinct from the previously discussed languages. There is certainly space for debate on the viability of the application of these patterns.
Keep things as simple as possible, but no simpler. The point of this isn't to introduce complexity for no reason, it's to free up the domain model of an application from low level details like persistence, i/o, network protocols etc. What's the Rust way to do that?
What the author calls bad code is one way of writing idiomatic Rust. There are more complex techniques.
It's recommended to not split the low level details from. your business logic, in fact it's not just recommended the compiler slowly forces your hand.
If you write overly abstract code like the author recommends you will leave a large amount of performance on the table. Code like that doesn't play nicely with lifetimes, by trying to separate memory management from business logic you're left with only the least restrictive scheme owned heap allocated data.
The Rust type system teaches you not separate concerns, without giving up the ability to reason about your code.
I'm sorry, but depending on abstract classes does not "free the domain model of an application of low level details". The details are always there, but they could be tucked away inside other classes (structs/types) that higher ones depend on. These low level classes need not to be abstract, it will just make discovering code harder and provides nothing to improve separation of concerns.
I didn't say it did. Using abstract classes is not the goal. The goal is to free the domain model from the low level details. This can be done and these architecture patterns are supposed to achieve that. In other languages you use a lot of abstraction to achieve this. If this isn't how it's done in Rust then it would be good to know how it is done. I want the low level details to depend on the business logic, that's all. That means the business logic is clear and testable independent of any low level details.
I don't fully understand the quote from Djikstra where he first talked about this but I'm sure he didn't mean it as it's interpreted today: "draw invisible boundaries in random places because best practices."
Yeah. The hallmark is they reference some document by Martin Fowler. That's a red flag for me.
I would add - one of the symptoms of this type of over-abstraction is what you could call the "where tf" problem. Any time you need to do anything it's really hard to figure out where
1) ...the thing you're trying to fix actually happens so you can fix it
2) ...the new feature you're trying to add should actually be added
3) ...it's going wrong when it's going slow/not scaling/stalling somehow
...because in reality the answer to the question "where" is always "all over the place". And that means you typically need to make several small changes in various places to do anything. So you've papered over the intrinsic complexity of the system and you have a really nice looking whiteboard but the complexity is now distributed in a bunch of places so it's conceptually complex and much harder for a dev to actually get their arms around the whole system and understand it fully. And you now have a false sense of security because you have great test coverage but the kind of problem you now face isn't caught by tests. Because typically the type of problem you hit is you should have made 6 changes in different places to implement your feature but you've forgotten one and only implemented 5. So the system is now semantically broken in some way even though all the tests pass.
1) Record of functions. Probably the most common, and pretty analogous to calling a constructor in OO. If you've heard "objects are a poor man's closures, and vice-versa", that's what this is referring to. You build a closure with as many dependencies as you can pass in at startup time (fields in OO), then later you can just call one of the functions inside it, with the remaining parameters (method call in OO).
2) final tagless. After you're comfortable with an Optional<Value> that might return a Value, and Future<Value> that will return a Value later, you can venture further down that path and work with a Writer<Value> which will return a value and do some logging, or a Reader<Env, Value> which will return a value while having read-access to some global config. But what if you want many of these at once? You end up with a ReaderT Env (Writer Value) or some nonsense. So instead you can write your code in terms of m<Value>, and then constrain m appropriately. A function which can access Git, do logging, and run commands on the CLI might have type:
(Git m, Logging m, Cli m) => m Value.
But that function does not know what m is (so therefore does not depend on it) You might like to have different m's for unit tests, integration tests, a live test environment and a live prod environment, for instance.
3) Free Monad & interpreter pattern. A nifty way to build a DSL describing the computations you'd like to do, and then you can write different interpreters to execute those computations in different ways. For instance, you could write a network/consensus algorithm, and have one interpreter execute the whole thing in memory with pure functions, for rapid development, debugging and iterating, and another interpreter be the real networked code. It's fun, but someone wrote an article about how Free Monads are just a worse version of final tagless, so I haven't used this in a while.
Yes, and for interested parties it's probably worth elaborating that Git might be something like
class Git m where
clone :: Url -> m GitRepo
currentHead :: GitRepo -> m CommitHash
and Logging might be something like
class Logging m where
log :: String -> m ()
which is very similar to defining
data GitDict m = MkGitDict {
clone :: Url -> m GitRepo,
currentHead :: GitRepo -> m CommitHash
}
data LoggingDict m = MkLoggingDict {
log :: String -> m ()
}
class Git m where gitDict :: GitDict m
class Logging m where loggingDict :: LoggingDict m
So the "record of functions" style and the "final tagless" style are equivalent, except that the former passes operations manually and the latter passes operations implicitly. The former can be considered more cumbersome, but is more flexible.
With (3) it’s hard to have different instruction sets for different parts of your program. Ideally, a function that logs only uses the log instruction set and this is reflected in the type. Once you start down this road, you end up at final tagless anyway.
Wow when I read this comment I did a double take and had to go to Wikipedia… then I realized dependency inversion is not the same thing as inversion of control and things made much more sense.
I guess part of the confusion came from how dependency injection is a form of inversion of control… the words are all very similar to dependency inversion.
I think your identification of that distinction is entirely too generous. Typically the derision of dependency inversion extends to inversion of control since they are cut from the same cloth. One just focuses on what is being inverted and the other the process of inversion.
I haven't internalised what inversion of control means, but I'm very strong on the distinction between dependency inversion and dependency injection frameworks.
With DI, you stop your business logic from knowing about your Postgres database.
With DInjF, not only does business logic still know about Postres, but now it knows about Spring too! (The upside is that it takes fewer lines of code to get from 0 to spaghetti)
Dependency Inversion is a recipe for how to invert the dependency between two components (by introducing a third component on which both depend and which can be grouped with either side, hence allowing to invert the dependency at will). It’s not inherently tied to OOP.
Incidentally, one thing it glosses over is the creation of the components, which may prevent completely inverting the dependency.
This sort of response on second thought seems like a knee-jerk, but in the off chance that you might be open to seeing the perspective that values a hexagonal architecture.
You always have two concrete implementations: the production application and the testing application. Otherwise you yolo things into prod, or run only manual/integration tests. That can work for a while, but many people find it unsavory.
It is pretty easy and sometimes useful to make three implementations: http server, CLI, test. Maybe you want to use files in CLI and a db for a server.
It has always been a good idea to isolate persistence and transport concerns from the business logic and that doesn't change in Rust. Don't dependency drill SQLite up and down every call stack. If your application is small enough and will stay so, then separating it is more a question of habit than anything.
But you shouldn't abstract everything nor try to separate everything! It was a persistence layer before, in the hexagonal architecture it becomes adapter implementations of a port. Transport layer is similar. Separation of concerns in this case means that you have a concrete dependency (an http server, a db etc) that isn't part of your logic.
The best way to conceptualize hexagonal is as a kind of crutch to accomodate the inability of unit tests to effectively fake stuff like the db and their tendency to tightly couple to everything.
It's not intrinsically good design but it does improve unit testability (which sometimes has value and sometimes has zero value).
I am partial to property testing logic and integration testing servers.
This frequently requires some level of separation, the key is to do it only at the right points.
Don't start by saying how can I unit test this tiny bit of logic against several mocks, start with a simple integration test of your real routes.
As you add abstractions you are trading maintainable straightforward code for more granular testing. It's a hard trade off not a set of principles for good code.
What I mentioned was an adapter and port. If you can only run against a postgres database, your integration test is going to require setup and be slow. If you can easily swap the postgres adapter out for sqlite or in memory, the same test will be practically instantaneous and self contained. The same test can be occasionally run against a postgres database (like once a night), to ensure there are no postgres specific idiosyncracies.
Thus I started by saying how you can integration test quickly without mocks (sometimes it would be called a fake, but using a different db is something else) on real routes.
Not the parent, but what's really missing in the article is a complete code listing of what the initial code has been turned into after the refactoring to really hammer home the absurdity of the advice (fwiw I was also scratching my head for a while whether this is satire, because the end result would look a lot like 'Java Hello World Enterprise Edition: https://gist.github.com/lolzballs/2152bc0f31ee0286b722).
The original code fits on one page, is readable from top to bottom, and doesn't contain any pointless abstractions that worry about 'future problems' that never actually come to pass in the real world anyway.
If the code no longer fits the requirements, no big deal, just throw away those 40 lines and rewrite them from scratch to fit the new requirements. That will most likely take much less time than understanding and modifying the refactored 'clean code', because there's a pretty good chance that the new requirements don't fit the abstractions in the refactored version either (and IME that's the typical scenario, requirement changes are pretty much always unpredictable and don't fit into the original design, no matter how well thought out and 'flexible' the original design was).
Yeah, this article is the poster-child for "engineering for an unlikely future at the expense of the certain present".
> If you ever change your HTTP serve
Yeah, that ain't happening. You also won't replace your database engine or queue either. If you do for some reason, it'll be a partial rewrite of you app no matter what. You can't abstract over these things in a useful way, the common denominator isn't rich enough for useful applications.
> You'd have the same problem if this code lived in a setup module
Moving code from file A to file B does nothing to it. This is the fallacy of assuming that the name of the thing changes the thing, and is in the same vein as having a "secure" network where it's secure because it is called that in a spreadsheet of subnets.
> our HTTP handler is orchestrating database transactions
Transactions are deeply linked to requests. If you try to abstract this away, you won't be able to read your code any more because you won't be able to see the control flow in... the controller.
> You cannot call this handler without a real, concrete instance of an sqlx Sqlite connection pool.
Faking a database is a fool's errand. It's a lot of work at best, and a subtle source of false negatives or positives in your tests at worst. Database engines have very complex behaviours such as concurrency, transactions, locking modes, type conversions, collation, and so on. Why try to emulate this!? Just use a local database file for testing!
A bigger concern with the repository pattern is that without eternal vigilance, it'll block the use of high-performance code.
For example, with the Author repository, retrieving authors is all-or-nothing. The blog author used sleight of hand to hide this by having a single "name" field along with a primary key. Okay, what if there are 287 fields, and a bunch of foreign keys? Now what? Do we read in the 1 name field along with 286 unrelated fields just to throw all that work away? That's 0.35% useful work performed per call!
Similarly, he returns single authors, one at a time. How do you returns collections in response to queries? As Iter? A Vec<Author>? What if it's an async streaming response!? If you try this, you'll quickly discover that there is no general portable pattern across different DB providers and every approach has some downside. That downside can be "OOM panic" or similar.
I've been doing a lot of work recently to clean up legacy ASP.NET apps and my #1 trick to directly invoke Entity Framework directly in the controllers (HTTP handlers). I select just the required columns, run the queries as async, and where possible/useful I stream back the results instead of trying to hold them in memory at once.
I've seen 5-10x speed ups compared to SOLID pattern code with everything broken out across dozens of interfaces, abstractions, and layers scattered across a bunch of projects. All this with a 30x (no joke) reduction in lines of code, dramatically faster builds, faster deployments, and readable code that can actually be maintained by one person. I reduced one project that had several thousand lines of code to one page, the same kind of thing as the "bad" everything-in-main example in this blog post.
As someone who has worked on haskell professionally, it's probably the easiest language to interview in/for. It's really easy to see how deep a dev has gone down the haskell rabbit hole. Forget a certificate, just talk for 5 minutes.
Any dev who has fully gone down the haskell rabbit hole can definitely grok any other paradigm. The challenge isn't technical skills with devs like this.
> in my experience aerospace is way better in most ways.
As an Amethyst user, I'm approaching this comparison from the other direction: is there one compelling reason to switch?
The improvement I'd most like to see in Amethyst is more stable window placement when I remove a monitor then add it again (I do that a lot with my laptop).
It would be fantastic to have integration between Amethyst-managed spaces, Firefox windows, and Proton Pass vaults. As in Space 1 knows that new windows should use the Google account in my Work vault, and Space 2 knows that new windows should log out from Google because there is no Google account in my Personal vault. I doubt that's an imminent prospect, though.
In general, I prefer the Amethyst approach of extending the builtin OSX window management to the Aerospace approach of replacing it. Clearly the Amethyst developers weren't convinced that it's impossible to move windows between spaces with hotkeys, because they went ahead and implemented that.
Overall, I'm really happy that these window managers are being written. I use a 42 inch monitor, which would be awkward without them.
> understand when the effect occurs requires examining the type signature of every function that is called. Since this is meaningful control flow, it seems very valuable to be able to identify points at which an error occurs without examining the signatures of each function call.
This is currently the case in rust. IO and other effects are frequently implicit. You don’t have to use ? or await they are *sugar. I have frequently seen reinventions of exceptions, unwind nonsense, adhoc interpreted tagged effects, etc..
Explicit syntax for effectfull calls should not be a goal. We don’t actually have that today.
I think it’s incredibly useful today to have both of these annotations.
I frequently scan for all instances of `?` or `.await` in functions (though, unfortunately, for various reasons this won’t show you everywhere these effects are produced).
I would rather not have to rely on an IDE to get that functionality.
Depends might be a strong word but this would affect things like screenshots submitted in bug reports as well, which would be less than ideal at the very least.
They could probably even just parse all comments and rewrite them. Then remove the redirects. If this feature is being used as intended these files should all be just linked to from comments and they parse the markdown anyways.
While they are at it maybe they can expire some old files that aren't referenced...
This is really cool.
I looked at the kalosm-sound, and noticed it's using candle, how is the perf compared to openai-whisper, whisper-cpp, faster-whisper, ext..?
A zkVM makes encoding the arbitrary logic as easy as writing a normal Rust program. A proof is a probabilistic statement that a specific program run with some public inputs and maybe some private inputs was executed correctly.
For crypto zk proofs are mostly for their succinctness property, not for privacy.
Outside of crypto privacy is the more important property, let's say a government issues a signed document, I could prove any arbitrary statement about that document without revealing the document. We could use zk proofs for anything we use canonical documents for today and we would gain privacy. Will we do this, probably not, but it would be an improvement.