Marijn Haverbeke, creator of CodeMirror, ProseMirror and author of Eloquent JavaScript, writes about editors and programming tools.
HN commenters link it as the home of ProseMirror and CodeMirror, which power editors like Obsidian's and Nuclino's, and praise his book.
Author here. I think it depends what you're doing! OT is a true distributed systems algorithm and to my knowledge there are no projects that implement true, distributed OT with strong support for modern rich text editor SDKs like ProseMirror. ShareJS, for example, is abandoned, and predates most modern editors.
If you are using a centralized server and ProseMirror, there are several OT and pseudo-OT implementations. Most popularly, there is prosemirror-collab[4], which is basically "OT without the stuff you don't need with an authoritative source for documents." Practically speaking that means "OT without T", but because it does not transform the ops to be order-independent, it has an extra step on conflict where the user has to rebase changes and re-submit. This is can cause minor edit starvation of less-connected clients. prosemirror-collab-commit[5] fixes this by performing the rebasing on the server... so it's still "OT without the T", but also with an authoritative conflict resolution pseudo-T at the end. I personally recommend prosemirror-collab-commit, it's what we use, and it's extremely fast and predictable.
If you just want something pedogocically helpful, the blessed upstream collaborative editing solution for CodeMirror is OT. See author's blog post[1], the @codemirror/collab package[2], and the live demo[3]. In general this implementation is quite good and worth reading if you are interested in this kind of thing. ShareJS and OTTypes are both very readable and…
A lot of the same refactoring is possible in dynamic languages as in static ones. I recommend reading up on Term to see what's possible to do with JavaScript http://marijnhaverbeke.nl/blog/tern.html
I use Cursive https://cursive-ide.com/ for working with Clojure, and it can do safe refactoring for symbols by doing static analysis of the source. It can show all usages of a symbol, rename it, do automatic imports, and so on.
Another piece of tooling that's not available in any statically typed languages at the moment is REPL integration with the editor seen here http://vvvvalvalval.github.io/posts/what-makes-a-good-repl.html
I find that the REPL driven workflow found in Lisps is simply unmatched. When you have tight integration between the editor and the application runtime, you can run any code you write within the context of the application immediately. This means that you never have to keep a lot of context in your head when you're working with the application. You always know what the code is doing because you can always run and inspect it.
Having the runtime available during development gives you feedback much faster than the compile/run cycle. I write a function, and I can run it immediately within the context of my application. I can see exactly what it's doing and why.
The main cost of static typing is that it restricts the ways you can express yourself. You're limited to the set of statements that can be verified by the type checker. This is necessarily a subset…
It would probably depend on the Scheme/Lisp implementation and how it was secured.
If it used a Scheme implementation like in Halo:CE, then people would have justifiably hated it. If it used something more like MIT-Scheme, people might've been okay with it. But I suspect it would get the same hate that JS gets, since there is a lot of low-quality JS code floating around -- even though JS itself isn't remotely that bad -- and you would instead have a lot of low-quality Scheme code floating around. Damned if you do, damned if you don't.
FWIW, it is feasible to put Scheme/Lisp in a browser today, and it might even be fun. Take a browser that lacks JS (e.g. Dillo, w3m); figure out how to add Firefox's or Chromium's approach to isolating each Javascript instance; and embed a Scheme or Common Lisp implementation (e.g. Chez Scheme [1] or ECL [2]).
You will need to decide what Scheme/Lisp functions/macros to include/exclude. It may seem obvious to exclude file operations, but they did that with JS and now we have IndexedDB; a restricted, virtual file system might be fun to play with. Multithreading might seem like a great thing to include, until you start trying to implement secure multithreading. I would start with whatever cl-isolated [3] allows and expand/contract from there.
And before anyone asks, "would this actually be worth implementing in 2025?", the answer is decidely "yes". You could build powerful desktop applications without the full weight of Electron. You could…
I was glad when the author of the article advised against learning Monads by analogy, because it's one of those subjects where analogies serve only to confuse people. Personally, my brain maps badly to Haskell syntax, so let's do this in JavaScript (I know, I know, not so powerful and everything):
In fact if you used jQuery, you already know what a Monad is, probably without realizing it. Consider the following code:
$('.myelements').filter(filterFunction).css('color', '#ff0');
You can follow along what this code does by following the dots. First, all elements matching the ".myelements" class are returned, these are then filtered, and the result of that gets a css modification.
Every time you wrote a method that returns an object, you already did monads in principle. They can be used to chain things together. While Haskell provides semantics that explicitly deal with the mechanics of monads, pretty much every dynamic language has them.
Here's what it looks like in PHP, for example:
$obj
->bla()
->beep()
->borp();
You can use monads to make your code more concise, to simply chain function calls, or to iterate over a list, but they're also often used when sifting through databases since filtering and applying attributes to items. For example, an SQL-like database query might be expressed like this:
table1.select(all).where({ ref : 1 }).orderby('id');
Of course, that's an API which is a bit dishonest since it acts as if the code really goes through…
Hi, the author of Yjs here. Thanks, Nick, for chiming in!
As this article is blowing up now, I want to address a few points.
I, too, feel the need for simplicity over overly complex solutions - and I found it in CRDTs. They beautifully allow me to reason about conflicts - so that my users don't have to. Very few people can design a custom conflict resolution algorithm for an application. Yjs is a general-purpose framework that enables you to make EVERYTHING collaborative. That's the goal.
It's fine if you want to explore different solutions. I don't understand the need to put down one framework in favor of another. It doesn't have to be "OT vs CRDT". Hey, if you found something that works for you - great! But let me tell you that neither solution magically makes everything simpler. There is still a lot to learn.
Different solutions to conflict resolution have different tradeoffs. It's unfortunate that the author of the article attributes all complexity to Yjs. It's just that collaborative editing is a very complex problem and requires a lot of attention to detail. In many regards, Yjs has done very well for the larger ecosystem. In other regards there is room for improvement.
The only thing I acknowledge from the article is the criticism about y-prosemirror "replacing the whole document". Unfortunately, the author extrapolated some false assumptions. This is not a performance issue. y-prosemirror runs at 60 fps even on large documents. We leverage ProseMirror's…
This talk makes a really important point for multiplayer app implementations!
But I don't love how he separates the global order approach from operational transform. Those two approaches go very well together in my experience: - Global ordering eliminates much of the complexity of OT - OT enables optimistic, offline edits and fine-grained merging on top of global ordering
Most SaaS editors are not truly peer-to-peer: edits are sent to a central server before being broadcast back to clients. The service can define the global ordering then - the point of this talk.
But you need the server to be able to reconcile and merge concurrent edits from clients that haven't fully sync'ed yet, and you want clients to be able to locally apply edits before receiving the latest state from the server, so you need some way to undo and replay edits on top of newly discovered edits that happened before (like a Git rebase). This is the basic operational transform function.
The knock on OT has been complexity - the claim that you need n^2 merge functions for n operations. But... most operations in a real app don't collide in ways that require merges so in reality you need far fewer than than n^2. You generally need text and collection merges.
Marijn Haverbeke, the author of ProseMirror and CodeMirror, has a great blog post on a lot of this here: https://marijnhaverbeke.nl/blog/collaborative-editing-cm.html