LOC is useful here not because it's a metric for output but because it's a metric for _understandability_. Reviewing 200 lines is a very different workload than reviewing 2000.
HN user
ilikebits
sccache caches at the per-rustc invocation level, whereas we cache at the package level. In practice, this means we get more cache hits and our cache hits take less overhead to load. You can see the benchmarks in our CI!
We haven't worked with Dioxus in particular, but the CLI is a project that we benchmark against: https://github.com/attunehq/benchmarks
Monads are a generalization of Promises. Each type in Monad defines their own `.then` in a different way. For promises, `.then` is defined as "run this function once you have this deferred value from the last promise". For optionals (`Maybe`), `.then` is defined as "run this function if the last optional had an actual value". For Either, `.then` is defined as "run this function if the last Either returned Right, otherwise early-return with the value from Left" (this is functional early-return, basically).
How do you compare against Morph (https://morphllm.com/)?
Sorry I missed this - let us know if there's anything we can do to help! Feel free to email me at eliza@attunehq.com.
Yup, this was similar to our experience as well. If publishing is still a source of toil for you, feel free to reach out at eliza@attunehq.com - we'd love to see if we can do any dev work or hosting pro bono for your open source projects.
I'm not familiar with mise and ubi, but these look like they're tools that the end user runs to install binaries. In contrast, we're building something that sets up a server so that end users can use their existing `apt-get install`.
Thanks, appreciate it!
Yeah, solving this locally for one repository definitely isn't that hard at all. Most of the features we're building become useful when you're trying to build CI integrations for a larger team while also complying with enterprise security requirements (e.g. audit logging, HSM key protections, etc.).
That's true! The Debian and Ubuntu folks are also experts at this. In our experience, the sharp edges generally affect teams that don't have a lot of in-house expertise in this, and where release engineering is not a core engineering competency that they want to invest in.
We don't plan to do that right now. The CLI isn't that complicated, and the choice for that was driven more by Go's excellent cross-compilation story than anything else.
It would be a fun thing to do if we had the resources to get equally good cross-compilation in Rust, but we're focused on building functionality right now.
Yeah, multi-distro and multi-arch is something on our roadmap! We're especially interested in automating the more annoying OS-specific parts of releasing, like MacOS notarization.
Our use case is customers who (1) want to use a managed cloud hosting service that we provide, but (2) are not willing to give us their signing keys. Our design allows them to keep all of their signing keys local to their environment.
To our knowledge, we have not found another provider who supports both of these requirements. It's not some amazing technical innovation, but it is one of those annoying paper cuts that builds up with all the others.
Thanks for trying, and sorry in advance for the rough edges :)
We're also working on a hosted service! If you'd like a sneak peek, send us a message at founders@attunehq.com (or email me directly at eliza@attunehq.com). I'm happy to talk about your specific needs and see if we can build something for them.
(And yes, it is Rust. I keep trying to find projects where I get to stretch my Haskell wings again, but unfortunately I keep working on things with silly requirements such as "easily understandable performance" and "a cross-compilation story" and "not making my collaborators sit through another monad tutorial".)
You are correct. The reason we built this tool is because all of the scripts we found had sharp edges when we tried to use them in production at scale (I talk about some of them here: https://news.ycombinator.com/item?id=43730136).
Most of these scripts were designed for a world where there was A Blessed Deployment Machine that acted as its own de facto centralized control plane. We're designed for a newer world where publishing is just another piece of your CI, so you need more features to handle concurrency control, distributed signing, incremental index rebuilds, etc.
One of the tools we tried was Aptly! A couple issues we had with Aptly:
1. Aptly needs to rebuild the entire repository before it can do any changes. One of our customers builds packages for every Linux OS, and every architecture, and each release contains 10-something separately packaged tools, and they do regular releases. This means their repository of packages is _huge_. When they try to do small maintenance actions like removing a package that has a bug, they need to rebuild their entire repository to do this, which takes upwards of 30 minutes. On Attune, since we have a centralized control plane, it takes about 10 seconds.
2. Aptly doesn't have concurrency controls. This makes it annoying to run in CI. Imagine you're setting up a CI pipeline that runs on every commit and publishes a package. You need to make sure you don't have two different instances of Aptly running at the same time, because their updates will clobber each other if they publish at the same time. Can you ensure that your CI only ever runs one publish at a time? This is easy in some setups, but surprisingly hard in GitHub Actions - concurrency keys don't quite work the way you'd expect them to.
3. Aptly uses its storage as its source of truth for the current state of packages. This is a great design choice for simplicity! However, if your CI job dies halfway through during an Aptly publish, this can leave your repository in a corrupted (in this case, half-updated) state. To recover from this, you need to rebuild the repository state... which you can't, because the ground truth is contained in the storage which was just corrupted! We mitigate this issue by tracking metadata in a separate transactional database so CI crashes don't cause corruption.
These are some examples of little cuts that build up over time with all of the tools we've found so far. Lots of the open source tooling is great at low scale, but the cuts start to build up once you are pushing a meaningful amount of packages to production.
Totally agree here. The current target is people who are familiar with the process, and we plan to write more documentation as we start thinking more about accessibility to a broader audience. We actually ran into this issue ourselves while building the tool - the Debian documentation is all there, but not the easiest to approach. These docs are one of the rough edges (sorry!) since we wanted to get this out earlier for feedback.
To answer your direct question: on self-hosted repositories, the URI right now is just a string identifier. We are working right now on a cloud hosted service, and there the URI will help us figure out which repository to actually serve.
Congratulations on the launch! I think you might have a typo in your screenshots, where "liabilites" should be spelled "liabilities".
This is great! I loved playing Sub Command as a kid, and seeing the very familiar waterfall display was a real blast from the past.
I've been following Unison since almost the beginning (back in the structure editor days!). It's a very cool project, https://www.unison.cloud/our-approach/ is a great read, and the Unison language (especially their formulation of effect handlers as "abilities") is very cool.
There are two specific things here that make me reluctant to use Unison Cloud in my own work:
1. It doesn't look like there's any FFI or way to shell out to other tools within Unison Cloud. I understand that this is necessary to provide the desired static guarantees, but the lack of an escape hatch for using pre-existing code makes this a really hard sell.
2. Typed storage is excellent! What are its performance characteristics? In my experience, gaining expressiveness in storage systems often requires trading away performance because being able to store more kinds of values means we have fewer invariants to enable performance optimizations. How do migrations work? I've always found online migrations to be a major pain point, especially because data rapidly becomes very heavy. (At a glance, it looks like storage is key-value with some DIY indexing primitives, and I couldn't find anything about migration.)
The approach article asks "Why is it so complicated, anyway?". My guess would be that:
1. For small projects where you can toss together NextJS and SQLite and throw it onto Hetzner, it really _isn't_ that complicated.
2. For large projects where with large amounts of data, high availability requirements, and very large scale, performance and operability matters a lot, and none of these all-in-one systems have yet to demonstrate good performance and operability at scale.
3. There really is not that much demand for projects between these two sizes.
Re: positive and negative - refer to my answer in https://news.ycombinator.com/item?id=36744384 and especially the linked Serokell interview, where we dived into this question specifically.
Re: design workflow - this is not very different from any other code. You have module interfaces, logical subsystems, separately deployable service entrypoints, etc. Our Haskell code tends to take special care to avoid statefulness and globals, but this is not any more of a design burden than writing idiomatic code in any other language (e.g. planning out your classes in Java).
Re: special tricks - we used fused-effects to model our effects, which has been pretty useful (it lets us delay teaching transformers), but this is by no means a secret that we've discovered.
I think the biggest trick is to write simple code. I think when you give a junior engineer a sufficiently powerful hammer (like Haskell and its effect tracking), they will attempt to nail absolutely everything to the wall (e.g. build a taxonomy of every possible effect and every possible exception and write your whole program in this framework for Maximum Compile Time Safety).
Avoid this. It's just not a productive use of time, and the gains to safety often are not worth the effort and velocity and overhead you paid to build the underlying framework. Focus on the high-ROI piece of the program. Write the effects that you would use for testing, do the rest in IO for now, and come back later if you find that you need more granularity in your effects. The important thing is to ship the product.
(I go into much more detail about this in the Serokell interview linked in the other HN comment.)
Excellent question, and this was a serious concern for us. And we have experience with this! We had one deep Haskell expert on the team who helped build a bunch of core primitives who has since left to another company.
One of the motivations for adoption for us was strong interest on the team. Looking back on our team...
1. We started with me (enthusiast), our expert,and a Go developer with no Haskell experience.
2. Over time, at its peak, the team grew to us 3, Python developer who had dabbled, two Ruby developers who were interested but had no experience, a frontend expert who had dabbled, and another engineer who I didn't work with closely but I think had a background in Java?
3. Our expert left the team before or around the time that the last person (maybe-Java person) joined, I think.
4. On the second Haskell team I spun up, it's me, one person from the old team, a Ruby/JS developer, and a JS developer.
We were able to get all of these people productive in Haskell. It got easier once we had more experience teaching folks. There are a handful of _key differences_ between Haskell and ALGOL-family languages (mostly around evaluation model and effect tracking), and once you nail those down, the rest is pretty smooth sailing, and your SWE experience and intuition begins kicking in again.
Although we miss our expert very much every day (they were a very cool person! They drove a motorcycle!), their departure has not had an outsized impact on our velocity.
A sibling comment recommended a bus factor of 2-3. I think this is roughly correct, although I would think of this as not merely your "don't get hit by a bus" group, but also your core teaching group.
Here's my take on a monad tutorial: https://lobste.rs/s/7cllte/monads_part_six_really_what_is_mo...
TL;DR: Monads are Promises and async/await, but generalized to different implementations of `.then`.
You are right to be suspicious; it is _not_ that hard! I think people just get confused and overwhelmed because (1) there are many separate different concepts that are all being introduced at once, and (2) you need to learn a new debugging methodology at the same time. I think the vast majority of this problem is the huge deluge of bad teaching materials (especially monad tutorials written by people who do not use Haskell in production) and the shortage of good teaching materials.
Re: 1 - some separate, orthogonal concepts that I have seen people mix up while trying to teach Haskell:
1. Effect tracking (which is a thing that we _do_ using monads, but is not intrinsically tied to monads - this would be better served by being taught as "dependency injection but stricter")
2. Non-eager evaluation (which is a language runtime evaluation choice that Haskell has made, and which motivates the usage of monads where most languages have function bodies, but is again not intrinsically tied to monads)
3. "Purity" (a more confusing way to explain 1)
4. "Mutable state" (another thing we represent with monads, but again is not intrinsic to monads)
5. Monads the math / category theory concept (don't bother with teaching this - it will not be helpful until you have intuition for writing and operating the programs themselves first, which takes at least a few months to build, and even then it is mostly useful as a source of advanced techniques for library writers rather than application writers)
At work, we've developed our own set of educational materials for teaching folks the language. I'm working on externalizing them in my spare time.
We have, and it's Got The Spirit, but it's still not the same. It's just so much more verbose, so much noisier, and has so much less compiler safety. You don't get things like non-eager evaluation or effect tracking, your stack traces become much messier, and your open source dependencies are still in plain JS.
Overall, we found that fighting against the language this way was the worst of both worlds - you paid a non-trivial cost and did not gain enough benefit. But our original codebase also predated TypeScript (remember TLDRLegal? That's us!), and much of our TS was bolted on after-the-fact on a pretty significant existing codebase that was written in very idiomatic JS. A team with more resources to do technical refactoring (we were quite small and very focused on building product) or starting from scratch may have a different experience.
Re: applications and language comparison, see my answer over at https://news.ycombinator.com/item?id=36744384
Re: development process - it's very similar to development in other languages. Write, compile, yell at compiler, push, complain about how slow CI is, deploy. You know, the usual.
I think the most interesting difference is the _onboarding_ curve. Haskell's curve is pretty brutal, although I think most of this is because of bad pedagogy (many monad tutorials are bad, and beginners can't tell) rather than because of intrinsically difficult concepts. Some observations:
1. Empirically, zero to code review is roughly six weeks for a professional industry software engineer. It's not that much longer than other languages we've had to teach. But it _feels_ very brutal because zero to side project is roughly three or four weeks. Contrast this against Go, where zero to side project is about five minutes.
2. Having an experienced Haskell engineer on your team to start with makes a WORLD of a difference. You've gotten a type error - why? Is it because GHC is doing weird backwards type inference stuff again? Or is it because you've misunderstood this fundamental concept? Or is it because you've done a typo, and GHC has inferred a downstream site to be a type error? This sort of thing is very difficult to explain in words and in general, and much easier to pick up through experience and mentorship. If you do not have an experienced Haskeller at your disposal, I would strongly recommend starting with side projects first, and using the Functional Programming Slack (fpslack.com), who are some of the friendliest and most patient folks I've had the pleasure of talking to.
Developer tools!
I think Haskell and OCaml are popular in finance because they're high-level and familiar to math/quant types (who are broadly--and this is a sweeping generalization--more familiar with expressing algorithms in recursion than in procedural sequences).
I don't have much experience with OCaml, but I can also say that Haskell in particular has incredible support for building very high-level libraries that are both expressive and have strong compile-time guarantees. I've found it's relatively easier for a programming expert to build such a library and a domain expert to consume the library in Haskell than in many other languages I've seen (perhaps Python comes the closest, but that feels to me more like an ecosystem thing than a language thing).
In the broad scheme of things, not really.
I think we waited too long to ship our initial prototype to customers, but a big part of that was that we were pretty sloppy in our product management at that stage of the company (I think we had closed the A about a year ago, and did not yet have our first dedicated PM), and our technical rewrite was also partially a feature and UX rewrite. It's been several years since, and we have clawed our way out of that hole, and I think our delivery process is actually in a really good place now.
If I were to give advice to people looking at adopting new languages, I would say to get it into production ASAP. I believe the keyword to search for is the "tracer bullet pattern". One of my favorite blog posts on this is https://blog.thepete.net/blog/2019/10/04/hello-production/
In functionality, roughly. By LoC, I think the majority of our codebase is still JavaScript/TypeScript. Many of the subsystems still in TS are being rewritten in Haskell (not because we have a "rewrite in Haskell" mandate, but because they are due for a rewrite anyway due to scale and requirement changes, and the team that owns that domain now primarily writes in Haskell).
For us, there were a couple advantages. For context, I work at FOSSA (https://fossa.com/). Our core product solves software supply chain needs in enterprises (around licensing and security), and our core technology is around compiler, build, and source code analysis.
Off the top of my head, 3 advantages stood out:
1. First, if you're not going that far off the beaten low-level path, Haskell has incredible productivity benefits. Effect tracking has enormous benefits for testability and understandability. If you've ever been down a debugging rabbit hole shaped like "there's no way this logging call is sending that API request", then you might be pleasantly surprised to discover that you can statically guarantee that this doesn't occur in Haskell programs! Pattern matching, algebraic data types (sum types!), and typeclass derivation make it much easier to make it impossible to construct invalid representations of data. Other languages are finally picking this up, but their versions of pattern matching often have caveats for backwards-idiom-compatibility. And monads are a very powerful abstraction. It's like being able to write your own semantics for async-await (I've talked more about this before at https://lobste.rs/s/7cllte/monads_part_six_really_what_is_mo...).
2. Haskell was a good domain fit for us. One thing we build is the FOSSA CLI (https://github.com/fossas/fossa-cli/), which runs in customer CI pipelines to analyze their builds. It's a very compiler-shaped problem: shell out to some tools, do a lot of parsing, think very hard, and then spit out a JSON blob to send back to the API. Our first version of this was written in Go. At the time of development, writing correct, testable parsers in Go was like pulling teeth. We have a relatively small headcount-to-product-surface-area ratio, and our team was running up against the overhead of rewriting traverse in Go over and over again (that's a Haskell-flavored joke, but if you've ever been annoyed at writing yet another for-loop in Go, you get it). We decided to hack out a prototype in Haskell, and it turned out to be a good fit.
3. Lastly, the kind of people who wind up working at FOSSA and are interested in the code analysis bits tend to be the same kind of nerds who love Haskell. We had lots of people on our team who were chomping at the bit to try it, so we decided to try it out. I really can't understate how big of a productivity difference it makes when people are working with tools that they actually enjoy rather than are merely forcing themselves to use. It is night and day.
If you want to learn more, we also did an interview with Serokell on this topic (https://serokell.io/blog/haskell-in-production-fossa), and discussed it on an episode of our engineering podcast (https://fossa.com/blog/fossa-podcast-adopting-haskell/).