Amazing work!
I don't know how I feel about writing CLIs in JS. Just seems a little bit janky to me, and I don't know why. I wonder if there's any push to have something JSXish in Go, even if it requires a pre-compiler to achieve the syntax, just for CLI apps like this. Then again, maybe I'd just rather stick with something like Nim where you can just have a first-class DSL through the macro system.
0: https://github.com/a-h/templ 1: https://github.com/maragudk/gomponents
The primitives are good enough to build anything you want. Right now I'm trying to figure out how to make better libs and tooling for creators (mainly myself) so I can crank out lots of examples apps that showcase the power of TUIs where you don't need a dedicated executable for each one.
I can feel snarkiness in this response. I don't like it.
From Ink's author [0] post:
> The time has come though to move on from Ink. Since russia’s full-scale invasion of Ukraine in 2022, everything in my life revolves around the war. And anything else has lost all meaning to me. I no longer have the mental and physical capacity to maintain Ink and give it the attention it deserves.
This project and others like React Native show that it works outside of the DOM: you just render something else instead.
Thinking that React is only helpful for DOM UI seems like a misunderstanding of what React is and what makes it great.
Arguably it makes even more sense than with React. One of the problems with React is that the underlying DOM is a stateful, imperative system, whereas React wants to behaves much more like an immediate-mode system, where the whole UI can be dropped and re-rendered at any time. But the terminal is not stateful like the DOM, and a more immediate-mode paradigm arguably works better in this context than in the browser.
And this is why React apps end up with bad performance by default. Doesn't crop up in simple tests and light usage, but the bad scaling will catch up with you when you deploy to production.
But you'd be surprised about the "react for the front end of the CLI" part. I used this thing a whole six years ago in a complex interactive CLI and it came off great to use, maintainable and ergonomic. React is just one framework that proscribes to the "UI as declaritive/composable trees" pattern. And that UI doesn't have to be web based. That pattern works for all UI's I've come across.
Its the pattern that is a good reason to do this. And not react/js landscape. That part is probably a bonus for many though.
Since it's Kotlin, it has backends for the JVM, LLVM, and JS/Wasm, though curiously Jake recently removed the JS target because it wasn't seeing much usage.
React allows you to mix declarative and imperative programming concepts together in an effective way. To give a sense of why this is important: Imagine a CLI built with XML. It'd probably be pretty verbose and have a lot of domain-specific configuration. There also wouldn't be a great typechecker. React combines the most robust type-system in the world (Typescript) with declarative style. It has issues, but being "built for DOM/web" is not one of them.
RE: Ink. A simple example of where it is better than most tools is you can simply import a re-usable "ranger-style" file browsing component, and adapt it for things like exploring a database, a spreadsheet, or an API spec. The potential is very high. Hats off to the author and contributors!!
This is very high praise for something that only provides safety at compile time and not runtime.
TS might not be the most robust type system in the world with no qualifiers. And it might not even try to be sound. And it might disappear at compile time. But none of that changes the fact that it took a bunch of concepts that only programming language dorks knew or cared about and turned them into every day utilities that the junior most developers use. And it doesn't change the fact that it is the only mainstream programming language with a fully Turing complete type system.
Give credit where it's due. I say all of this as one of the aforementioned programming language dorks who took a really long time to get on board with TS, as I thought Flow's focus on purity made it a better choice.
If you told me that was the case today with web developers generally, I’d laugh at you even harder than I would have if you made that prediction ten years ago.
Similarly, a function that accepts (explicitly) `A | B | (C & D)` and then dispatches to functions that accept `A | (C&D)` vs `B` is, you guessed it, type algebra and is a common pattern in hot paths through every TS codebase.
Just because the formal nomenclature is unknown does not mean the concepts are unfamiliar.
It's the opposite in my experience. Most web developers I work with (a lot because of consulting), specially the average "React sprint runner" doesn't have a clue about anything slightly above basic types and just google/chatgpt whenever things break so they can move on to the next task in the sprint.
cryptic typescript errors don't help here either.
Because some checks are expected to be runtime only, it lets you specify types like "Odd integers" by writing a `where` clause.
``` subset OddInteger of Int where !(* %% 2) ```
You can use multiple dispatch to separate dispatch from processing: ``` subset Fizz where * %% 3 subset Buzz where * %% 5 subset FizzBuzz where * %% 15
multi sub fizzbuzz(FizzBuzz $) { 'FizzBuzz' }
multi sub fizzbuzz(Buzz $) { 'Buzz' }
multi sub fizzbuzz(Fizz $) { 'Fizz' }
multi sub fizzbuzz(Int $number) { $number }
(1 .. 100)».&fizzbuzz.say;
```Or even use inline anonymous subsets when you declare your functions: ``` multi sub fizzbuzz(Int $ where * %% 15) { 'FizzBuzz' } multi sub fizzbuzz(Int $ where * %% 5) { 'Buzz' } multi sub fizzbuzz(Int $ where * %% 3) { 'Fizz' } multi sub fizzbuzz(Int $number ) { $number } (1 .. 100)».&fizzbuzz.say; ```
Raku's type system is one of its features that will show you new ways of thinking about code. I advocate playing with Raku specifically for mind expansion because it has so many interesting ideas built in.
For runtime safety, there are lots of frameworks that follow TS's standard, one of the best is called "zod" which allows runtime safety and complex types are inferred.
type Thing = string | boolean;
const arr: string[] = [];
function add(item: Thing, dst: Thing[]): void {
dst.push(item);
}
add(false, arr);
Is valid typescript.However, regularly it's not the case -- especially with people moving from nominal systems.
TS can't really be practically nominal when it has to be constrained by its compile target. So I guess it ultimately boils down to an anomaly/criticism born from the legacy of how web standards came about.
But yeah unfortunately not sure if JS is an appropriate target for these type systems. Nim does it, but I'm not sure how safe it is.
type thing =
| String(string)
| Bool(bool);
let arr: list(thing) = [];
let add = (item: thing, dst: list(thing)): list(thing) => {
switch (item) {
| String(s) => dst @ [String(s)]
| Bool(b) => dst @ [Bool(b)]
};
};
let newList = add(String("asd"), arr);
This doesn't work, since the dst array has to be one that can contain both string and bool in the first place.I understand it may be unwanted at first glance, but this is a contrived example for demo purposes. You wouldn't really make an "add to array" function like this so specifically. You would use generics, which would solve the exact issue that is posed.
function add<T>(item: T, dst: T[]): void {
dst.push(item);
}
Now you can work with any array, and it will only add items with the right type.If you really need it to be just <Thing>, you can do this
function add<T extends Thing>(item: T, dst: T[]): void {
dst.push(item);
}
Now it knows that Item and DST are related but need to be Thing.There's not a lot of languages with unions that handle this differently. F# discriminated unions make you specify which type you're using every time. For example, this doesn't even compile.
type Thing =
| A of int
| B of bool
let arr: Thing list = [1] // Not an A or B type!
Anyway, you can get this with Zod https://zod.dev/
React is descriptive. It creates a virtual tree and tells how that tree should be changed.
What that tree renders into is a completely different problem. On the web, it renders into HTML. On your phone (ReactNative), it renders into native GUI. In ink, it uses a render engine to concat and output the text results (it's a little more complex than that as it uses Yoga[0] today).
https://github.com/pomber/didact
At a high level though, you create a tree of nested objects. Each has a handful of special properties (children which contains an array of child objects being the most interesting to users). The rest of the properties are used to interface with the rendering engine by passing it configuration and (usually) event functions it can call when something happens.
That tree of objects gets passed to the rendering engine which turns the tree into some kind of output (DOM, Canvas, WebGL, Ink, etc)[0].
When something happens (an external event, a timer, event from the render engine, etc), your code potentially changes the object tree. React then walks that tree and uses a few shortcuts to quickly detect what has changed. Instead of re-rendering everything, it can give a list of things that have changed and what the new values are. The render engine then decides how to make those changes happen and the cycle repeats.
It's really pretty simple.
- https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-...
I can also recommend Dan Abramov's post "React as a UI Runtime":
- https://overreacted.io/react-as-a-ui-runtime/
and then of course the actual React docs for learning how to _use_ React:
Talk about a late-night-infomercial-level “let’s compare the thing we are talking up with the absolute worst alternative imaginable as if that was the next best choice” argument.
Things like XSLT are really quite good if you can get past "ew xml icky"
You can inline different schemas in modular, bazaar like ways from a variety of sources and pull in different parsers that do different tasks.
For instance you could have, say an object subtype and pretend you, a spreadsheet developer made something compatible with it.
As the tool parsers the XML, it sees the spreadsheet, which it has never encountered however, the schema tells it where to get the parser so it dynamically loads it, passes off the subtree it doesn't recognize and lets your spreadsheet parser do its things.
It's remarkably clean and elegant clockwork for distributed modular applications.
Things like a mobile phones share feature would be a perfect use case: a wide variety of objects from different applications passing through the same schema with a share directive invoked that allows you to share in a variety of ways. XML can handle this quite magically.
I've built systems like this with XML, it just works. I was like "damn. XML... Who would have thought"
People just never learned XMLs killer features because it got a bad reputation as being the thing that out of touch enterprise Java bozos make messes with - which is true.
But that's also why JavaScript was shat on for 15 years. You gotta be adult enough to look past that. Try to separate the merits of a technology from your perceptive competency of its users.
Just a few of the things I didn’t like about it. But when the system worked, it was quite capable.
The negative feelings about XML are not due to the lack of glossy pitch pages and hypemen pushing XML and XML-based technologies; they developed whole XML had those far more than React ever has.
Some people may be too young to remember that, but...
They were the kinds with a bunch of worthless certifications who go everywhere in suits. I expected nothing but expensive clunky broken complicated bullshit from them. And that's exactly what they gave you when they pitched XML so I ignored it for over a decade as some tedious wasteful time-consuming thing that dumb people use to rake in hourly contract dollars.
But then, maybe 15 years ago or so, I started hitting use-cases that it was designed for and everything changed.
Just to emphasize this: Why has XML been losing as the standard config format? Why did JSON win on web APIs? It's not because JSON is better, but because it's simpler and very-javascript-compatible.
If you like XML, there is a strong argument to support JSX because it's a way to keep XML around!
In APIs you're talking to 1 server, calling 1 function, that returns 1 kind of thing at 1ce.
XML starts becoming useful when you increment those numbers - any of them.
People (including me) have been trained not to think in those ways. The web still isn't a web - it's merely a collection of computers with individual relationships.
The 90s internet had fleeting attempts at other paradigms. We should probably put serious effort into a second bite at that apple since by every measure our technology is at least 1000x better in the same way we've revisited neural networks
The short answer, as you answered it, is:
> there are 1,000 page books on XML alone.
The long answer: it was over-engineered for the intended use-case.
It's easier to fix under-engineered stuff (like JSON) than to fix over-engineered stuff (like XML).
> If you like XML, there is a strong argument to support JSX because it's a way to keep XML around!
Well, this is basically pivoting XML to a different use-case. It'll still be over-engineered unless you stick to JSX only, in which case it's not XML anymore, anyway.
As a way to ultimately render a view, it has never been elegant.
I’ve done it every possible way:
- Templating libraries
- XHTML with embedded XML data
- XML and XLST
- XML embedded in JavaScript (EMCAScript), aka React before React existed
- React, which is similar EMCAScript
I used the first public releases of React. I was immediately sold. No one had to sell anything to me. I knew it was going to win.
I can’t imagine anyone with strong experience in all these ways possibly think that these other ways are more enjoyable.
p.s. I’m not talking about Redux. I was never sold on Redux/Flux.
yup - two of my favorite React packages are Remotion and React Email.
The rare combo of Blub Paradox + Poe's Law
The TS type system is neither especially powerful nor especially robust
By what measure?
Your comment reads like you have something to sell me.
If I'm not mistaken, JSX is a type of XML blended with JS code.
Are you serious?
I think I’d much rather have more nice TUIs than not, regardless of the language they’re written in.
Basically, you're logically re-rendering _everything_ on every render. The React engine then diffs it with the last state, and then applies necessary actions to reconcile them. As a consequence, your rendering code is invoked by React, and you have to follow some rules to make sure that React doesn't have to _physically_ re-render everything.
Another consequence is that physical component creation is taken out of your hands.
This works great, if you're doing something with tons of simple components.
It works less great if the actual rendering is a complicated task. E.g. you're making a map widget, or a 3D editor. You might be better off not using the reactive approach, and fall back onto the classic MVC.
I could take React as a kind of FP design pattern.
Probably, other Ui frameworks could also map, seeing as the declarative/composable tree pattern is ubiquitous now. So it's important to note it's that pattern which enables this, and not a specific framework.
But React is one of the ones that is also more removed from the web target than others.