As one of the original authors of Cargo, I agree. lockfiles are for apps and CLIs are apps. QED.
HN user
wycats
Certainly!
A little more context: I've been working on the design of this reactivity system in some form since roughly 2018 as part of the Ember framework.
The original design of the reactivity system made its way into Ember Octane as the "auto-tracking" system, and was fairly exhaustively documented (as originally designed) by @pzuraq in his excellent series on reactivity[1]. He also gave a talk summarizing the ideas at EmberConf 2020[2] after Octane landed.
Unfortunately, there's no good way to use the auto-tracking system without the Ember templating engine and all of the baggage that implies. But there's nothing about the reactivity system that is fundamentally tethered to Ember or its templating system in any way!
For various practical reasons, Tom, Chirag and I had a need to build reusable chunks of reactive code[3] that work across many frameworks. We liked the foundation of the auto-tracking system enough to extract its ideas into a new library, decoupling the auto-tracking reactivity system from Ember.
PS. In case you're wondering, I expect Ember to ultimately migrate to Starbeam, once it's in solid production shape and the dust is shaken off.
[1] https://www.pzuraq.com/blog/what-is-reactivity
[2] https://www.youtube.com/watch?v=HDBSU2HCLbU
[3]: I would have called them "components", but that would make it seem like they have something to do with creating reactive output DOM, which is not what I mean.
Hey! Owner of the original repo here :)
Like @tomdale, I'm surprised to see this on the front page of Hacker News!
I have been working on more detailed documentation about the overall model. It's also still in flux (and not ready for release ), but you can check it out at https://wycats.github.io/starbeam-docs/.
You're absolutely right. We took a lot of inspiration from the MobX API design.
One thing worth calling out: MobX requires you to mark getters as `@computed`:
class OrderLine {
@observable price = 0
@observable amount = 1
@computed get total() {
return this.price * this.amount
}
}
The equivalent Octane class: class OrderLine {
@tracked price = 0
@tracked amount = 1
get total() {
return this.price * this.amount
}
}
This is more important than it looks. The fact that you don't need to decorate the getter also means that you can break up a computation that uses tracked properties into smaller functions without needing to think about how the smaller pieces should be written. You just use functions.Here's what happens when you try to add methods with arguments to MobX:
import { observable } from "mobx"
import { computedFn } from "mobx-utils"
class Todos {
@observable todos = []
getAllTodosByUser = computedFn(function getAllTodosByUser(userId) {
return this.todos.filter(todo => todo.user === userId))
})
}
And Octane: import { tracked } from "@glimmer/tracking";
class Todos {
@tracked todos = [];
getAllTodosByUser(userId) {
return this.todos.filter(todo => todo.user === userId))
}
}
The rule in Octane is: "mark any reactive property as @tracked, and use normal JavaScript for derived properties". That's pretty cool!And on top of that, the fact that we use a simple reactivity primitive under the hood[1][2] means that we have been able to transition from an API designed for 2012-era two-way bindings to a unidirectional data-flow model with minimal disruption, and with free interoperability between code written with the two APIs (even in the same object).
This also means that we can design new functionality (like Octane's modifiers, and other upcoming reactive APIs) without worrying about how the parts of the system will work together.
[1] https://github.com/glimmerjs/glimmer-vm/blob/master/guides/0...
[2] https://github.com/glimmerjs/glimmer-vm/blob/master/guides/0...
I think that's probably a good change to the default. Today, `ember build` is analogous to `ember s`, and most people deploy to production with `ember-cli-deploy`.
But that's not a good enough reason :)
Thanks for surfacing this!
You got that right.
We got a lot of inspiration from the React ecosystem, both in terms of component design (the root element is just another element) and composition.
Those changes played out differently in the context of Ember, because of the way our APIs work, but the core ideas are sound, and I'm happy that the React community paved the way.
You're not alone! The Ember core team broadly agrees with this goal.
With Octane, we focused on landing broad ergonomic improvements in a compatible release of Ember.
At the same time, we've been working on updating the way that Ember builds JavaScript so that it can make better use of tree shaking and code splitting tools in modern bundlers. That project is called Embroider[1] and it currently builds substantial Ember codebases.
Wrapping up Embroider and shipping it by default is a substantial part of the work we have planned for 2020[2].
Also, now that Octane idioms fully replace the need for Ember's original object model (designed in 2012!), I would expect it to become an optional feature, meant to be used primarily as a transition path. When combined with tree shaking, that should substantially change the default byte size of Ember.
It's too early to say exactly how that will shake out (no pun intended), but it's a big priority for the Ember community next year.
[1] https://github.com/embroider-build/embroider
[2] https://github.com/emberjs/rfcs/blob/2018-2019-roadmap/text/...
Ultimately, the value prop of a tool like Graphiti or GraphQL is independent of the exact details of the wire format.
Graphiti uses JSON:API[1] under the hood as its protocol, which is a language-independent protocol with a wide variety of clients[2], and is now built-in to Drupal[3].
I designed JSON:API in ~ 2013 with a narrow purpose: to describe a protocol for incrementally communicating with server-side graphs over HTTP.
By incremental, I mean the possibility of fetching exactly the data you need at first, but then slowly filling in more data over time as a user interacts with an application.
URLs are a great "internet pointer", so when I was looking for a way to link fetched data with other not-yet-fetched data, URLs were the natural answer. On the other hand, a lot of REST APIs at the time didn't have a good way to eagerly send a whole bunch of graphy data down at once.
JSON:API's concept of linkage supports both models: a type/id pair for data the client already knows about and a URL for data it doesn't. You can even combine the two, to enable incremental refreshing of a piece of data that you fetched as part of the first request (simply by hitting the URL).
JSON:API was designed to work well with many kinds of client-side approaches, including ORMs. This meant using a composite key of type/id to identify records and not just a single ID. Over time, this has become a best practice for other tools in the space[4][5]
OData exists in a similar space as JSON:API, but JSON:API is much more tightly focused on the problem of incrementally fetching a graph of data from the server into the client. Focus is good :)
[1] https://jsonapi.org/ [2]: https://jsonapi.org/implementations/ [3]: https://www.drupal.org/project/jsonapi [4]: https://graphql.org/learn/queries/ [5]: https://levelup.gitconnected.com/basics-of-caching-data-in-g...
It doesn't appear to solve some or the 2 major headaches graphQL is great at solving: * clients choosing how much data they need. On a desktop on broadband client, I want details of a list of entities, while on a mobile app with limited screen space and less bandwidth, I just need the skinny details like title and summary.
JSON:API solves this problem through sparse fieldsets[1]. Graphiti clarifies the spec by allowing sparse fieldsets at any level of nesting. As the original author and a current editor of the spec, I think we should adopt this extension into the spec proper.
* pooling requests to multiple domains into a single request
JSON:API operates against a logical "graph in the sky", just like GraphQL. Graphiti makes it possible to mix and match database queries with other services and even plain Ruby objects[2].
One nice thing about this approach is that you still get to make database queries that take advantage of indexes and other database optimizations organically and by default. GraphQL's field-based approach can often result in a lot of extraneous database queries in the service of a homogenous server API that is based on fields.
(In my experience working with GraphQL APIs, I've seen a lot of performance issues that could only be debugged as "why is this field slow" that could have benefited a lot from making a big chunk of the query against a SQL database, directly using decades of optimizations around query planning).
[1] https://jsonapi.org/format/#fetching-sparse-fieldsets [2]: https://www.graphiti.dev/cookbooks/without-activerecord
I split my time between Ruby, JavaScript (mostly with TypeScript) and Rust, with occasional Java and devops work. Of those, JavaScript (with TypeScript) is my predominant language at the moment, but the mix changes pretty often.
Skylight's stack is Rust and Ruby for the agent, Rails for the backend, Java for our data processing pipeline (essentially a custom data store) and Ember for virtually the entire front end. The graphs in Skylight are Ember components written in d3.
I still think that Rails is a great choice for most web apps, since (to this day) it provides an extremely productive baseline for building account management and working with third-party integrations, which turn out to be a surprising percentage of the total code (and an even higher percentage of backend code changes) in even an ambitious project like Skylight.
I also think it's reasonable to use something like Java or Rust for any heavy data-crunching your app might do, but I think people over-estimate which aspects of their application are truly performance and efficiency critical.
The short answer is no.
We work differently than other similar products, in that we rely heavily on aggregation, both for presenting useful data in the UI and also to keep our backend scalable. We don't keep around particular aspects of individual requests. Individual requests are essentially only used as "data points" to build statistical models about your app/endpoints. For example, any SQL queries are parsed and sanitized on your server before they are sent to us[1].
That probably sounds more involved than it actually is in practice – you can see it for yourself on the dashboards for The Odin Project[2] and the Homebrew formula browser[3]. The bottom line is that there is no way to get from the aggregated data back to an individual request.
[1] https://www.skylight.io/support/faqs#security
[2] https://oss.skylight.io/app/applications/g0gJSNnzYAws/recent...
[3] https://oss.skylight.io/app/applications/jut3BrkJo722/recent...
For clarity, we also support almost all Rack-based frameworks in Ruby (including Sinatra):
https://www.skylight.io/support/advanced-setup
We should probably update our copy to clarify this point in the prominent places where it isn't already clear.
For slightly more context, I wrote the first lines of Rust code for Skylight as a spike at the end of 2013, and joined the Rust core team more than a year later. :)
I was largely inspired by a blog post in June 2013 by Patrick Walton: Removing Garbage Collection From the Rust Language[1]
[1] http://pcwalton.github.io/blog/2013/06/02/removing-garbage-c...
Preferring implicit code is optimization for the first person writing the code while making it harder to understand, debug, and update.
I feel that this argument has been repeated so often that it's almost understood axiomatically: nobody thinks about what it means anymore, it just "seems true".
I posted Aaron Turon's (of the Rust core team) article that touches on these topics elsewhere in the thread (https://news.ycombinator.com/item?id=14280908), but this topic desperately needs a better definition of "implicitness".
Virtually nobody thinks that C programming is "too implicit" because it's "optimizing for the writer over the reader," and similarly, garbage collection is an extremely implicit mechanism that is widely accepted, including by some of the strongest EIBTI proponents.
On the other hand, as a strong proponent of abstraction and implicitness in many contexts, I was extremely supportive of (and helped champion) Rust's explicit error handling through the Result type.
I like Aaron's "reasoning footprint" rubric because it gives us a way to debate this topic without "implicit" defined in the eye of the beholder. Importantly, it allows us, as a community, to broadly accept changes like garbage collection and control-flow constructs without repeatedly rehashing the same old bumper-sticker debates that mar every generation of programming.
Since this thread is somewhat rehashing the old arguments ("you write once, but read many times, therefore EIBTI"), it might be worth reading a more modern take on the topic by Aaron Turon of the Rust core team in his essay about the Rust language ergonomics initiative[1].
But this, in my opinion, is a misdiagnosis of the problem, one that throws out the baby with the bathwater. The root issue is instead: how much information do you need to confidently understand what a particular line of code is doing, and how hard is that information to find? Let’s call this the reasoning footprint for a piece of code. The pitfalls above come from the reasoning footprint getting out of hand, rather than implicitness per se.
Does readability then demand that we minimize the reasoning footprint? I don’t think so: make it too small, and code becomes hopelessly verbose, making it difficult to read by forcing too much information into view at all times. What we want is a sweet spot, where routine or easy to find details can be left out, but relevant or surprising information is kept front and center.
[1] https://blog.rust-lang.org/2017/03/02/lang-ergonomics.html
Also, this particular format is the "wire format", which is the compact representation that we compile templates into to send to the client.
The client then compiles that representation into flat opcodes, in part by specializing the template based on runtime information (like the exact identity of the components in question).
The runtime opcodes are binary (128-bits apiece at the moment) and optimized for reasonably fast iteration. The wire format is, as chadhietala1 said, not as flat or compact as it could be, but still much more compact than our earlier representations (or the representations of competing rendering engines).
We plan to improve the wire format representation in the near future.
I grew up as an Orthodox Jew and virtually all of the stories of the bible are heavily illustrated in many forms, both recently and in ancient times.
The fact that the Megilla doesn't contain the name of God is an interesting phenomenon and the source of a lot of commentary, but not for this reason.
I don't agree that all orthodox jews subscribe to it, although I do agree that it's a commonly held belief.
The universal values held by Orthodox Jews are the articles of faith of Maimonides ("Rambam")[1], which are included in regular prayers. Maimonides is, as a result, extremely well regarded among Orthodox Jews as a source of values.
Maimonides took the position that Genesis (Bereishis) was to be considered allegorical when its plain reading conflicted with science[2]. This was a source of debate among his contemporaries, and it's uncontroversially the case that he strongly believed this.
As a former Orthodox Jew, I don't think that Orthodox Jews are doing themselves any favors by teaching their children that if they believe that the Torah is not in conflict with science, they are violating basic, universal Orthodox values when Maimonides disagrees with that point of view.
[1] https://en.wikipedia.org/wiki/Maimonides#The_13_principles_o...
[2] https://en.wikipedia.org/wiki/Allegorical_interpretations_of...
Further, Maimonides, who enumerated the principles of faith accepted by Orthodox Jews, also held that Genesis could/should be interpreted allegorically[1].
Some medieval philosophical rationalists, such as Maimonides (Mosheh ben Maimon, the "Rambam") held that it was not required to read Genesis literally. In this view, one was obligated to understand Torah in a way that was compatible with the findings of science. Indeed, Maimonides, one of the great rabbis of the Middle Ages, wrote that if science and Torah were misaligned, it was either because science was not understood or the Torah was misinterpreted. Maimonides argued that if science proved a point, then the finding should be accepted and scripture should be interpreted accordingly.
[1] https://en.wikipedia.org/wiki/Allegorical_interpretations_of...
"favorite" wart at the moment: '% x ' parses to the literal string "x" - "%" when not preceeded by an operand that makes it the infix operator "%" starts a quote-sequence where the following character indicates what the quote character should be - with the exception of a few special character, most characters will set the quote character to its identity. So in '% x ', the quote character is space.
How is this different, in principle, from any other unary/binary operator like plus? In most languages, when `+` is preceded by an expression, it's a binary operator, otherwise it's a unary operator.
The same seems true here: when `%` is preceded by an expression it's binary `%`, otherwise it's a unary operator with the semantics you describe.
My takeaway from this is that first-class cancellation tokens are the right approach, but languages need some kind of syntactic sugar to eliminate, or at least reduce, the verbiage for the most common case of propagating it around.
That is also my perspective, but I think the syntactic sugar cannot have much more overhead than `async function`.
I also read that as Google will refuse to implement features if it doesn't get it's way in how they are designed.
There's no reason for Google to do this, in general, since there are enough Googlers in enough standards groups (all of which operate by consensus) that they can simply object to changes they don't like (like other members of the committee).
I think it's very likely that we could avoid turning this into a zero-sum debate with one total-winner and one total-loser.
That said, I think there's no positive sum because some folks believe that (1) Promises should be the primary async story in JS, and (2) Promises must not allow communication between two parties who hold a reference to the Promise. This means that `async function`s must return a Promise, and the Promise but not have a `cancel()` method on it (because it would allow communication between two parties holding references to the Promise).
Others (I'll speak for myself here) believe that the return value of `async function` should be competitive (ergonomically) with generators used as tasks (I showed examples in the parent comment). Since generators-as-tasks can be cancelled (via `.return()` and `.throw()`), the desire to make `async function x` as ergonomic as `function* x` conflicts with the goal of disallowing the return value of async functions from being cancellable.
In Ember's case, since generators already exist, it's hard for us to justify asking application developers to participate in an error-prone (and verbose) protocol that we could avoid by using generators-as-tasks instead. And that is likely the conclusion that we will ship (and the conclusion that ember-concurrency has already shipped).
For me, the bottom line is that we have very little budget to introduce new restrictions on `async functions`, because people can always choose to reject async functions and use generators instead (with more capabilities and less typing!). I think the cancellation token proposal is well over-budget from that perspective.
TC39 member here.
cwmma gets a lot right here: Promises have always been contentious in JS (and TC39) and Domenic has indeed had the patience of a saint attempting to wrangle the various points of view into a coherent proposal.
TC39 as a group is generally very motivated to find positive-sum outcomes and find solutions that address everyone's constraints in a satisfactory way. That doesn't usually mean design-by-committee: champions work on a coherent design that they feel hangs together, and the committee provides feedback on constraints, not solutions.
As a member of TC39, I'm usually representing ergonomic concerns and the small-company JavaScript developer perspective. I've had a lot of luck, over the years, in giving champions my perspective and letting them come back with an improved proposal.
The staging process (which I started sketching out on my blog[1]) has made the back-and-forth easier, which each stage representing further consensus that the constraints of individual members have been incorporated.
Unfortunately, I fear that promise cancellation may be a rare design problem with some zero-sum questions.
It's worth noting that there has been no objection, on the committee, to adding cancellation to the spec in some form.
The key questions have been:
First. Is cancellation a normal rejection (a regular exception, like from `throw`) or a new kind of abrupt completion (which `finally` would see but not `catch`). The current status quo on the committee, I believe, is that multiple people would have liked to see "third-state" (as Domenic called it) work, but the compatibility issues with it appear fatal.
Second. Should promises themselves be cancelled (`promise.cancel()`) or should there be some kind of capability token threaded through promises.
What that would look like:
let [sendCancel, recvCancel] = CancelToken.pair();
fetchPerson(person.id, recvCancel);
async function fetchPerson(id, token) {
// assume fetch is retrofitted with cancel token support
let person = await fetch(`/users/${id}`, { token });
}
// when the cancel button is clicked, cancel the fetch
cancelButton.onclick = sendCancel;
This approach had many supporters in the committee, largely because a number of committee members have rejected the idea of `promise.cancel()` (in part because of ideological reasons about giving promise consumers the power to affect other promise consumers, in part because of a problem[2] Domenic raised early about the timing of cancellation, and in part because C# uses cancel tokens[3]).In practice, this would mean that intermediate async functions would need to thread through cancel tokens, which is something that bothered me a lot.
For example, it would have this affect on Ember, if we wanted to adopt cancel tokens:
// routes/person.js
export default class extends Route {
async model(id, token) {
return fetch(`/person/id`, { token });
}
}
In other words, any async hook (or callback) would need to manually thread tokens through. In Ember, we'd like to be able to cancel async tasks that were initiated for a previous screen or for a part of the screen that the user has navigated away from.In this case, if the user forgot to take the cancel token (which would likely happen all the time in practice), we would simply have no way to cancel the ongoing async.
We noticed this problem when designing ember-concurrency[4] (by the venerable Alex Matchneer), and chose to use generators instead, which are more flexible than async functions, and can be cancelled from the outside.
At last week's Ember Face to Face, we discussed this problem, and decided that the ergonomic problems with using cancel tokens in hooks were sufficiently bad that we are unlikely to use async functions for Ember hooks if cancellation requires manually propagating cancel tokens. Instead, we'd do this:
// routes/person.js
export default class extends Route {
*model(id) {
return fetch(`/person/id`);
}
}
The `*` is a little more cryptic, but it's actually shorter than `async`, and doesn't require people to thread cancel token through APIs.Also notable: because JavaScript doesn't have overloading (unlike C#), it is difficult to establish a convention for where to put the cancel token ("last parameter", vs. "the name `token` in the last parameter as an options bag" vs. "first parameter"). Because cancellation needs to be retrofitted onto a number of existing promise-producing APIs, no one solution works. This makes creating general purpose libraries that work with "promise-producing functions that can be cancelled" almost impossible.
The last bit (since I started talking about Ember) is my personal opinion on cancel tokens. On the flip side, a number of people on the committee have a very strongly held belief that cancel tokens are the only way to avoid leaking powerful capabilities to promise consumers.
A third option, making a new Task subclass of Promise that would have added cancellation capabilities, was rejected early on the grounds that it would bifurcate the ecosystem and just mean that everyone had to use Task instead of Promise. I personally think we rejected that option too early. It may be the case that Task is the right general-purpose answer, but people with concerns about leaking capabilities to multiple consumers should cast their Tasks to Promises before passing them around.
As I said, I think this may be a (very, very) rare case where a positive-sum outcome is impossible, and where we need, as a committee, to discuss what options are available that would minimize the costs of making a particular decision. Unfortunately, we're not there yet.
Domenic has done a great job herding the perspective cats here, and I found his presentations on this topic always enlightening. I hope the committee can be honest enough about the competing goals in the problem of cancellation so that Domenic will feel comfortable participating again on this topic.
[1] https://thefeedbackloop.xyz/tc39-a-process-sketch-stages-0-a...
[2] https://github.com/tc39/proposal-cancelable-promises/issues/...
[3] https://msdn.microsoft.com/en-us/library/dd997289(v=vs.110)....
Shipping without understand is no virtue, and as far as I can tell, there's no hard conflict between the two.
The entire official story of "lean" is "build, measure, learn", which is fundamentally about developing a process for shipping while deepening an understanding.
The hermeneutic circle (https://en.m.wikipedia.org/wiki/Hermeneutic_circle) cited by Dave Herman of Mozilla Research and Brendan Eich (about JavaScript) is a process of iteration that prizes a deepening understanding interleaved with lessons learned from shipping.
I didn't interpret the OP to be saying that she wanted to gain a perfect understanding before shipping, but rather that she preferred a style that focused more than the tech community average on deepening understanding.
And I don't think she needs to go to a different branch of technology to get that. Maybe app development needs more of it.
Steve Jobs famously talked about the interplay between understanding and building: https://wycats.svbtle.com/theres-a-tremendous-amount-of-craf....
I think we could do worse than to think about this topic more.
We should also consider making startups a place where understanding is more common.
For what it's worth, I spent half the holiday weekend arguing on Twitter that I think apple has lost its way, and talking about my switch to Windows for my home machine, so I found both the vehemence of the flagged comment and the flagging to be pretty strange.
But receiving scorn is no evidence of speaking the truth. A death spiral looms from this confusion.