Javascript RORO pattern
tinyblog.dev
tinyblog.dev
Still, I do wince sometimes at the Everest sized heaps created in JS to do even the most trivial of tasks. 10s of thousands or hundreds of thousands of objects being generated in a smallish React app.
It's fast enough, the VM is obscenely optimized, and it's readable. But what is all that stuff really _doing_?
It reminds me I recently had a company ship me a laptop with Win on it. I turned it on and logged in and the CPU was just a madhouse with Edge and 2 tabs open. What is the CPU even _doing_??? Does the company mine crypto on company laptops for cash flow... or, what?
Even in a VM, Windows is constantly doing things in the background, whether it's touching the disk, burning CPU cycles, or making network connections. The background services are both numerous and opaque, so who knows what it's actually doing.
But man, it only loads services I know about and it's sooooo fast. And best of all it shuts up when I stop working :)
For example, Google Closure Compiler can take
function hello({name}) {
alert('Hello, ' + name);
}
hello({name: 'World'});
and compile it into alert("Hello, World");Clojurescript made that choice, though in retrospect it kind of seems like they backed the wrong horse.
... for the developer with the latest, highest-specced machine and only an IDE their app open.
thats what i assume
https://code.visualstudio.com/updates/v1_60#_inlay-hints-for...
First, mixed feelings:
- Every parameter as an object key is both overkill and can make function signatures less intuitive. If your function follows a common convention eg process(collection, argumentToProcessCollection), that’s going to be easier to understand.
- Bare, mixed-type, ambiguous primitive values are harder to understand in an argument position than clear well-defined types with named semantics.
- Booleans, opaque enums are particular red flags here.
- These ideas things can be combined.
I prefer:
1. Use a functional style, including function parameter position (and always make impure functions call out their impurity).
2. Only accept bare primitive types in positions where their meaning is unambiguous.
3. If you want to accept ambiguous primitives or even optional values with a clear type, consider whether you’re actually writing more than one function.
4. If you’re actually writing one function, everything else goes in a clearly defined options object.
5. If your options parameter starts growing, GOTO 3.
Binary relations / operators are a good example of 2. Things that could be infix operators but can’t because language. It’d be a nightmare if those functions required an object as an interface.
As with everything design, there is no hard and fast rule. Keyword arguments / passing an object make wonderfully clear interfaces in a number of situations and it’s helpful to have them. But applying the pattern everywhere would be a complete disaster.
I disagree. By this logic, any fetch-like interface should be decomposed back to fetch(createRequest(new RequestHeaders({…}), …, …), …, …). Big interfaces are there for a reason, and the reason is that we, as developers, are users as well, and we deserve simplicity in our daily work, because there is no deficit in complexity. Structure is only useful if you can remember it and if you use it entirely, naturally (so that you will remember it). If you can’t or you don’t, it’s useless. Why it’s okay to have ls/find/curl/etc in a shell, but having it in the source code is bad, and we have to write pages of readdirs, Regex-compiles and readyState handlers, increasing a mistake probability?
I know what your rationale is, and I agree with it: god objects and functions are hard to debug. But nothing stops you from splitting the complexity into simple, testable chunks under the hood of a god interface, which knows how to combine them by following a simple set of options. Whether you’re writing more than one function or not then becomes an implementation detail, which doesn’t land on shoulders of a user of that interface.
This is a weird example because fetch
1. Is constrained by HTTP semantics and has limited control over how it deals with optional values.
2. Has still somehow conflated at least two functions (idempotent reads like GET/HEAD/OPTIONS vs writes)
That’s really all of the point I was trying to make with 3. The optionality of Request.body is a signal that fetch is actually expressing multiple functions.
And I chose the word “consider” deliberately. If fetch were being written in a vacuum without consideration for HTTP or historical complexities it was meant to address, I’d probably argue for breaking it up. But I would consider those other factors as well.
> Big interfaces are there for a reason, and the reason is that we, as developers, are users as well, and we deserve simplicity in our daily work, because there is no deficit in complexity.
Big interfaces may or may not provide simplicity. Big coherent interfaces which express the whole of one thing are totally different from interfaces of any size which conflate different things.
> Why it’s okay to have ls/find/curl/etc in a shell, but having it in the source code is bad, and we have to write pages of readdirs, Regex-compiles and readyState handlers, increasing a mistake probability?
I’m not arguing for consuming small individual functions with a single responsibility as if they’re the final interface. What those small functions allow is composability, and at least a chance to make invalid state unrepresentable.
> I know what your rationale is, and I agree with it: god objects and functions are hard to debug. But nothing stops you from splitting the complexity into simple, testable chunks under the hood
But those interfaces are hard to debug for users, and
> knows how to combine them by following a simple set of options
If you know how to do that, there! You’ve defined another function! Now your user can call that higher level function and ignore your lower level units.
> Whether you’re writing more than one function or not then becomes an implementation detail, which doesn’t land on shoulders of a user of that interface.
Of course. That doesn’t necessarily mean your end user interface is one function with every argument that could possibly be used to influence the behavior of the underlying implementation. That’s just leaking your implementation!
Again, I would not object to fetch if I encountered it in a design document or code review. That implicitly means I would not object to headers as represented in its signature (it’s just Record<string, string> after all… a very common type in plenty of well-defined interfaces).
An example that would better illustrate when to “consider whether you’re writing more than one function” would be a generalized data access function that may use the following data sources, directly accepting whatever details needed/appropriate for the specific usage:
- HTTP
- SSH
- Postgres
- Redis
- Local file system
- In-memory POJO or equivalent
Sure, you can define such a function. But can you clearly define its usage? And sure, it might defer to smaller units as an implementation detail, but is that single function actually providing any benefit to users? Won’t they (1) find it easier to reason about the different interfaces directly and (2) be able to consume or produce a more usable general function which defers to a single, consistent interface abstraction?
I mean, yeah sure now you need to reference both that function and the implementation satisfying its signature. That’s twice as many imports, surely it’s twice as bad right? Or… maybe it’s better not to have to remember (or even try to document) all the different variations of parameters you must/may pass depending on which data source you’re accessing.
Sure, you can define such a function. But can you clearly define its usage?
Yes! In your example I see many db drivers for a single db/orm-like sort of api (not exactly orm, but very similar). It’s much easier to learn one common interface, and then only learn/discover differences in connection strings and subtle functions that do or do not work for a specific access method. While db/orm is not a single function, it is usually a collection of functions that target multiple very different storages (the same convergence which you find wrong with GET vs POST etc). If I were to use these native APIs, that would be mental overload x 6 for me. Libraries like sequelize and sqlalchemy are very usable because you may use them in both serious postgre projects and your pocket sqlite tools, by changing only the connection string.
Of course we discussed functions, not APIs, but if I were to design dropbox-like thing for developers, e.g. store/access your foo config anywhere, it could be:
f(data, options)
options: {
conn_string: “…”,
// driver-specific opts…
}
Not because my users are too dumb or too lazy, but because in the scope of this function all of these accessors minutiae details are irrelevant to them. They just want f(data, {conn:postgreconn, table:”FooConfigs”}) or f(data, {conn:”webdav://…”, method:”put”}), or just f(data, config.foo_options), the entire options imported from devops-aware config. All of them would be described in one github readme.md page and usable in 15 minutes instead of hours of reading on manuals.This all only works because I reduced the scope of that function to a single operation compatible with all access methods, but that’s exactly my counterpoint for 3, 5. It’s almost like cat data > /dir/file, where <dir> is a mountpoint of hdd, cdrom, nfs, webdav, mongodb, aws kms, mailbox (file is mail folder), sshfs, postgrefs (file is table name) and so on. But for developers, not shell users.
For example:
createCarousel(dom_element, { delay: 10, circular: true});
var spinner = createSpinner(dom_element, { spokes: 5 });
spinner.remove();
And, if you were okay with all the defaults, the object was completely optional: createSpinner(dom_element);
And a jquery example of the pattern: jquery.post(url, { foo: 1, bar: 2 });Pass descriptively named variables instead imo.
Descriptive names are not a solution when the data types are the same. It’s just as easy to mix up the parameters of the same type.
Destructuring where two funcs in the same scope return the same named object key will make your code harder to read for example.
Function options split across multiple lines also make things harder to read and make it less able to fit logic on a single screen.
I prefer less verbose code if I can have my function calls as single lines <80 char wide with self-documenting names that read like recipe instructions as to what is going on step by step.
Prettier’s line splitting makes things less readable a lot of the time. I use prettier-ignore quite a bit.
I've been writing React this way for about 2 years now. It's very nice...for small functions. But over time, it starts to feel extremely heavy, especially with larger functions that need to take in lots of parameters. I know, I know, I could break the function down. But in some cases you're just working with complex data that has a lot of inputs.
When you're working with more than 10 or so parameters, adding and removing them feels like a very heavy chore.
{a, b}: {a: string, b: string}It seems like destructuring came out, and suddenly people destructure everything, even if it hurts readability.
Stuff like this, I see constantly:
const {
id
name,
age,
email,
} = await getUser()
I write it this way: const user = await getUser()
// later
user.name
"user" here is typed, so you can hover over it/trigger autocomplete to see the props anyways.Same thing with functions, I see a lot of:
function createUser({
id
name,
age
}: {
id: number
name: string
age: number
}) {
// code
}
Which, beyond just being ugly (though that's an opinion I guess) takes up an ABSURD amount of lines.I do this:
interface CreateUserParams {
id: number
name: string
age: number
}
function createUser(params: CreateUserParams) {
// code
}- limit the number of parameters for most functions to at most 2.
- Use TypeScript (optional) and editor Intellisense (inline prompts) to display parameter type when writing function calls. VS Code has this by default.
- Use objects only when unavoidable (options, forms)
Parameter count denotes logical branches like an if statement.
If you're passing in more than two to three Params a parameter object quickly becomes desireable.
Forcing parameter objects for one or two parameter functions seems.. stupid to say the least.
Boolean parameters are extremely rare if you're code is clean. They're essentially a code smell that indicate someone has dropped a hack in the middle of your function and handed in a parameter to toggle the behavior on in their one case against all others.
You're much better off writing a new function that triggers the new behavior and wraps a call to the old one.
Similar story for object returns, if you need to return an object that's fine but to be honest destructuring an object straight away makes code pretty hard to read in a lot of cases (not all).
For returning two parameters I'm pretty confused about the code, it's likely breaking single responsibility and signalling sideeffects.
> now it is harder to argue that point in typescript
IMO, TypeScript does nothing to change the necessity of the object parameter pattern.
An good IDE like WebStorm will show the parameter variable names, even inline w/out needing to hover, providing the exact same info as RORO.
On top of that use JSDoc to provide type info and documentation that shows as a tooltip. Or go whole hog and use Typescript.
function foo({a: string, b: string, c: string})
Give the params proper names, and the IDE will reveal them (inline even!) without RORO or its costs. Sure, for a return value like that, use an object with proper names. But that’s been a best practice without “RORO”.Would be cool though if VSCode and other editors had a way to make variable names somehow persistently visible on function invocations - that would be pretty interesting IMO!
Looks like you missed what I wrote (twice). They do. Others have pointed out that VSCode supports it too. Search the comments for "inlay hints".
You also are shown the parameter names and types when writing the function call.
const anything = (a: ActualTypeA, b: ActualTypeB, someOptionalFlagIveInspectedTenThousandTimesAndStillForget?: boolean) => IrrelevantReturnType
What does the parameter mean? Is it in the doc block? (Almost never.) Does it default to false? (Now you’re reading the whole potential call stack.) Are additional behaviors associated with its absence or trueness that aren’t inferred at the call site? (Who knows but it’s a fair roll of the dice!)Those won’t show up in code review. They won’t show up in diffs at all if you rename the parameter and change its semantics.
Side note: REST APIs should use custom headers so they can send back JSON arrays where appropriate. GitHub does this.
You need a way to get to the next page. Some APIs might return it in the JSON response. GitHub returns it in the Link header (not really a custom header). Details here: https://docs.github.com/en/rest/guides/traversing-with-pagin...
I’m sure there are ways to prevent that from creeping in, but it’s something I’ve noticed.
https://blog.logrocket.com/const-assertions-are-the-killer-n...
It isn't just about protecting the caller of a function from having to deal with the function unexpectedly modifying the object. It's also about an async function not having to make a copy of it to prevent the app calling it from changing it before it's returned.
https://gist.github.com/lucasnad27/e93b8f316259be3299e5#clar...
Please don't try to use fancy terms to impress. That's not what destructuring is.
This appears to have been hand selected and placed into the second-chance pool. My question is why? This is such a poorly written article in so many other regards, which wouldn't be bad if it contributed something worthwhile, but it's all to document what is not a novel or noteworthy pattern.
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Why so negative, I’ve had this thought before and found it reassuring that I’m not the only one.
(Edited for clarity)
[0]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...