I couldn’t come up with a good title,
but what I really wanted to express is how I’ve recently come to appreciate
how Postfix Completion (or Postfix Templates) improves coding fluency.
(All mentions of “readability”, “thought process”, “coding order”, and “coding fluency” in this post are based purely on personal experience and subjective opinion.)
In recent years, due to work, I’ve been switching between multiple programming languages.
Lately, I’ve been working in the Python ecosystem — it offers rich libraries, a very developer-friendly syntax, and excellent readability.
But no matter the language, I always felt a slight friction in the development process.
Eventually, I realized it might be caused by a mismatch between coding order and thought process. Here’s what I mean:
When we think while coding, our order usually goes like this:
- Identify the variable (possibly the result of a previous step)
- Decide what to do with it (function/operator)
- Get the output
The thought process feels like this:
a + b = c; c - d = e; e * f = g; g / h = answer
It flows from left to right, with input on the left and output on the right, all in the same direction.
But our actual code often looks like this:
c = a + b
e = c - d
g = e * f
answer = g / h
Now our eyes jump first to the right at a + b
, then to the left at c
, then to the right and down to c - d
,
forming a kind of reverse-Z or zigzag scanning pattern.
Because output variables are always aligned at the start of the line, “readability” is improved — but “coding fluency” suffers. Why?
For example, calling an async function like:
c = await add(a, b)
It looks clear, but the “coding order” doesn’t match the “thought process”.
The natural thought process might go:
Think of a
→ use the function add
→ add parameter b
→ decide to await
→ finally assign result to c
.
But the coding order is left to right: you first write the result c
, then =
, then await
, then the function add
, and only at the end, the parameters a
and b
.
If the right-hand side await add(a, b)
needs to be changed later, you’ve already named the output c
— so you have to move your cursor back to rename it.
And if you want to treat add(a, b)
as a task instead of awaiting it, the await
— which you already wrote early on — needs to be deleted, again requiring cursor movement.
(By the way, I used to like Rust’s postfix .await
— it matches thought process and doesn’t interrupt a chained call,
but the most important part, await
, ends up hidden in the middle of the chain, which hurts “readability”.)
This reminds me of C#'s LINQ query syntax. It’s similar to SQL, but you write from
first, then where
, and finally select
.
This order matches the developer’s thought process: input source → filter conditions → output result.
SQL, on the other hand, starts with SELECT
, when the developer doesn’t yet know what tables to access or what data to filter.
Eventually, I discovered that IDEs support postfix completion for await
,and I felt it really helped align coding order with thought process.
Take the async function add
as an example. Here’s a simulated input scenario:
a|
# Cursor is right after `a`
a.add|
# Type `.add` and press Tab
# [1]
add(a, |)
# IDE converts to function `add` automatically, with cursor at the second parameter
add(a, b)|
# Type second parameter `b`, press End to move cursor to the end
add(a, b).await|
# Type `.await` and press Tab
await add(a, b)|
# IDE adds `await` automatically
await add(a, b).=|
# Type `.=` and press Tab
| = await add(a, b)
# IDE moves cursor to the beginning and inserts `=`
c = await add(a, b)
# Type output variable `c` — done
[1] .add
here is a custom snippet for demonstration purposes. Ideally, IDEs should suggest suitable functions for the variable automatically.
Another example: I want to get the number of items in member_list
and assign the result. member_list
looks like this:
member_list = ['Frank', 'Miki', 'Jack']
Simulated input:
member_list|
# Cursor is right after `member_list`
member_list.len|
# Type `.len` and press Tab
len(member_list)|
# Converts to function call, cursor at the end
len(member_list).=|
# Type `.=` and press Tab
| = len(member_list)
# Cursor moves to the beginning and inserts `=`
member_count = len(member_list)
# Type variable `member_count` — done
In this example, len(member_list)
looks like “length of member_list”,
making the key idea — “get the length” — easy to understand.
But personally, I often first think of what I want the length of (i.e., member_list
),
then apply the function len()
, and finally name the result.
If I wanted to change the function from len
to filter
, I could — because I haven’t written the output variable yet.
But in the traditional coding style, I would’ve already written member_count
, and then have to refactor it to something like filtered_members
.
Defining functions has the same issue:
The coding order is async
→ def
→ function name.
Easy to read, but unfriendly for typing.
We usually first think of the function’s purpose (i.e., get_member_list
), then use def
to declare it, and finally decide whether it needs to be async
.
In fact, the async
modifier should ideally be inferred by the IDE.
If a sync function contains an await
inside, the IDE should automatically add the async
modifier,
and propagate this change upward: any function calling it should also add async
/await
— a kind of “automatic function coloring.”
To sum up:
Since I started using Postfix Completion, coding has felt much more fluent. I highly recommend giving it a try.