Skip to content

Blog

My blog about programming & other stuff

If you find a typo or would like to improve my posts feel free to submit a pull request to https://github.com/inwenis/blog.

Check out my other projects on my github like https://github.com/inwenis/collider where I tried to simulate Brownian motions.

My collection of various kata's I've done: https://github.com/inwenis/kata

I used to teach programming, out of sentiment I kept these two repos: - https://github.com/inwenis/sda.javawwa13.prog1.day5.complexity - https://github.com/inwenis/sda.javawwa13.prog1.day3.array_vs_hashtable

Enjoy!

You can reach me at inwenis at gmail.com

Me gusta

I don't have a better place for this yet so here's a list of links I like:

coding agents are bad at being curious

tl;dr

Coding agents are amazing — no doubt. (em dash not accidental)

They can give you impostor syndrome when they nail a bug within a few minutes after a single prompt.

But they are too polite with the codebase.

They preserve too much. They assume too much. They verify too little. They turn accidental behavior into supported behavior.

What they are bad at:

  • They do not investigate like experienced developers.
  • They dry-run things in their head.
  • They believe weird accidental behavior is intentional.
  • They handle too many theoretical cases.
  • They often do not build a real understanding of the flow.

So when I work with them, I try to push them away from:

write the fix

and toward:

prove what is happening

Do not just read the code.

Abuse it. Rip it apart. Run strange little experiments. Create throwaway tools. Break the weird accidental feature if nobody should depend on it.

The agent can write code very fast.

But speed is not the same as understanding.

And unless you help them, they will not abuse and rip apart a codebase just to run separate pieces, grow understanding, and validate claims.

fast mode

Lately I have been using Claude with fast mode enabled.

Works great. Costs quite a lot.

Not necessarily because the model is smarter. Maybe it is not. But because the feedback loop is shorter.

If the agent is slow, I multitask. If I multitask, I lose context. If I lose context, I stop steering the agent properly.

Then the agent goes somewhere weird and I have to reconstruct what happened.

That is exhausting.

hallucinated conclusions

One useful trick is to make the agent list the claims it made.

For example:

  • what did you assume?
  • what did you verify?
  • what is only a guess?
  • what file proves this?
  • what command can I run to check it?

This helps a lot.

It does not remove hallucinations, but it makes them easier to catch.

code is not the truth

One thing I see often is that agents treat existing code like Moses treated his stone commandments.

If the code implies that this ridiculous combination of features and configs is possible, it must be intentional and thou shalt not break it!

This is understandable. Human developers do this too, especially in unfamiliar systems.

It takes time for a human to realize:

I have seen this oddity before, but I have never seen any proof that it is used intentionally.

But agents do it very strongly.

If some accidental behavior exists, the agent may preserve it. It may even build more code around it.

They might harden the ridiculous functionality with tests.

We would not want to break functionality when refactoring, do we now?

The agent sees that and thinks:

this must be supported

But sometimes the correct answer is:

no, this is nonsense, break it

I often have to explicitly say this.

You can change this behavior. Nobody depends on this. Nobody should depend on this. This weird dysfunctionality is not a feature.

Without that permission, the agent may keep carrying the weirdness forward.

defensive coding

Agents also overengineer.

They handle too many cases.

They add defensive code for theoretical problems.

They might add abstractions too early.

The less I review the code they write, the more I can feel the bloat accumulating somewhere.

They add fallback paths.

What if that tool is not available on the user's PC? Let me code a hand-rolled simplified version. LOL.

Often during the review phase I tell it to KISS.

Or:

do not code defensively against imaginary issues

This is not because defensive coding is always bad.

It is because code has a cost.

Every extra branch is something to read. Every extra case is something to test. Every extra abstraction is another thing to understand.

Every extra supported case is something the agent will resist breaking next time it touches the code.

shallow understanding

When I work with agents, I also notice something about myself.

I do not build understanding the same way I used to.

Before, I had to follow the flow myself. I had to open the files. I had to trace the calls. I had to suffer a little.

That suffering was useful.

Now the agent can grab code very quickly.

This is good, but also dangerous.

The agent may jump around the project and collect fragments. Then it builds an explanation from those fragments.

But some of that code may be unreachable. Some of it may be old. Some of it may be a weird edge path. Some of it may not matter.

So the agent can create an understanding that looks broad, but is actually shallow.

It knows many pieces. It does not necessarily know the flow.

debugging by imagination

A common agent failure mode is debugging by imagination.

It reads code. It explains what should happen. It proposes a fix.

But it does not always check what actually happens.

Experienced developers do ugly things.

They add temporary logs. They print variables. They comment things out. They write tiny scripts. They create throwaway harnesses. They call private functions directly. They fake inputs. They break stuff on purpose. They run the program in stupid ways just to see what happens.

Create and destroy worlds.

Create mutants to your liking.

Here the goal justifies almost any means.

Mold the system, the code, the component to your needs.

Run it. Test it. Learn what is actually true.

throwaway tools

Agents often need help creating throwaway tools.

Not production tools. Not nice abstractions. Not reusable libraries.

Just ugly little things used to learn something.

A script that calls one function. A log line in the middle of a branch. A temporary endpoint. A fake input file. A tiny harness around one parser. A command that proves whether some path is reachable.

This is how you turn guessing into knowing.

But agents do not naturally do enough of it.

They tend to dry-run the code mentally.

Sometimes that is enough.

Often it is not.

novelty

There is another thing I suspect.

Coding agents are trained on code where people do things in established ways.

So when you do something unusual, you may be fighting the model a little.

It tries to pull the code back toward the common path.

Usually that is good.

Common patterns are common for a reason.

But if you are deliberately doing something different, something new, or something specific to your system, the agent may keep “normalizing” it.

It may remove the interesting part. It may replace your idea with a standard pattern. It may treat novelty as a mistake.

This can make some kinds of programming harder.

Not impossible.

Just harder than expected.

match feels better than if until PowerShell

tl;dr

match in F# is usually case -> value.

In PowerShell it is often case -> effect.

That is why plain if / elseif / else or switch often reads better in PowerShell than trying to force a fake match style.

In F# match is a value

let color =
    match status with
    | Ok      -> "green"
    | Warning -> "yellow"
    | Error   -> "red"

This whole thing evaluates to one value.

No mutable variable. No repeated assignment.

That is why match feels so clean.

In PowerShell the same thing often becomes control flow

if ($status -eq "Ok") {
    $color = "green"
}
elseif ($status -eq "Warning") {
    $color = "yellow"
}
else {
    $color = "red"
}

This works fine.

But it is not really case -> value. It is branching plus assignment.

The real issue is side effects

PowerShell code often wants to:

  • Write-Output
  • Push-Location
  • Remove-Item
  • Start-Process
  • run git fetch

At that point the question is no longer "what value do I return?".

It is "what do I do in this case?".

if ($exe.Length -eq 0) {
    Write-Output "No .exe files found..."
}
elseif ($exe.Length -eq 1) {
    Push-Location $exe[0].DirectoryName
}
else {
    $selected = ...
    if ($selected) {
        Push-Location $selected.DirectoryName
    }
}

This is not elegant in the F# sense.

But it is honest. Each branch shows the effect directly.

The trap

When trying to make PowerShell feel more like F#, it is easy to add helpers like this:

function Write-NoExeFound { Write-Output "No .exe files found..." }
function Push-ExeDirectory($exe) { Push-Location $exe.DirectoryName }

if     ($exe.Length -eq 0) { Write-NoExeFound }
elseif ($exe.Length -eq 1) { Push-ExeDirectory $exe[0] }

Sometimes this is fine.

Sometimes it is just:

  • more names
  • more jumping around
  • more ceremony

PowerShell also makes this riskier than it first looks because functions write to the pipeline by default.

function Write-NoExeFound { Write-Output "No .exe files found..." }

$goto =
    if ($exe.Length -eq 0) { Write-NoExeFound }
    elseif ($exe.Length -eq 1) { $exe[0].DirectoryName }

if ($null -ne $goto) {
    Push-Location $goto
}

In the zero-results case $goto becomes No .exe files found....

Then Push-Location tries to go there.

switch is the native compromise

If you want something more match-like in PowerShell, switch is usually the right tool.

switch ($exe.Length) {
    0 {
        Write-Output "No .exe files found in the current directory or its subdirectories."
    }
    1 {
        Push-Location $exe[0].DirectoryName
    }
    default {
        $selected =
            $exe |
            Select-Object Mode, LastWriteTime, Length, DirectoryName, Name |
            Sort-Object LastWriteTime -Descending |
            Out-ConsoleGridView -Title "Where are we going?" -OutputMode Single

        if ($selected) {
            Push-Location $selected.DirectoryName
        }
    }
}

It keeps the case-based shape without pretending PowerShell is F#.

small naming side note

AI does not change naming rules.

Use the shortest name that is unambiguous in local context.

Good:

  • $toRemove
  • $localToRemove
  • $remoteToRemove

Bad:

  • names that restate obvious context without adding clarity

bottom line

  • when code is case -> value, match is hard to beat
  • when code is case -> effect, plain branching is often clearer
  • in PowerShell, switch is usually a better match-like tool than helper-function gymnastics

Google/Google Photos

Google products seem to slowly become worse, examples:

  • Gmail no longer recognizes flight itineraries.
  • Google Photos:
    • Removed the map view.
    • Doesn’t let you easily delete all photos.
    • Doesn’t offer a simple way to download everything.
    • Takeout exports require dealing with messy JSON metadata.
  • Google Search:
    • Cluttered with ads.
    • Results feel worse than they used to be.
  • Android removed native phone call recording.
  • YouTube is overloaded with ads.

Just moved away from Google Photos. For geeks, https://immich.app/ is a great alternative — they're amazing.

To fix the chaos from Google Takeout, I used this https://github.com/TheLastGimbus/GooglePhotosTakeoutHelper.

Would definitely consider paying for Immich if they offered a hosted version.

PowerShell quirk 2

tl;dr

Let's say you have a File[1].txt file and you would like to read it.

Get-Content "File[1].txt"
^ this returns nothing

Why?

PowerShell interprets [] as special characters. A range in this case. PowerShell is actually looking for a file named File1.txt.

What to do?

Get-Content "File``[1``].txt"

longer reads

git goodies

git branch -d `git branch | grep feature`                             # delete all branches with feature in its name
git branch | grep feature | xargs git branch -d                       # same as ^
git push origin --delete branchXYZ                                    # git push origin :branchXYZ
git push origin --delete `git branch -r | grep feature | cut -c10-`   # delete all feature branches from remote
git show branch:file > write_here                                     # https://stackoverflow.com/questions/7856416/view-a-file-in-a-different-git-branch-without-changing-branches
git checkout branch -- path/to/file                                   # similar to previous but checkout the file
git checkout --ours foo/bar.java                                      # useful for rebases or merges, works with --theirs too
git log -p -- src/data_capture_tools                                  # changes only made to a specific directory
git log --all --full-history -- "**/thefile.*"                        # https://stackoverflow.com/questions/7203515/how-to-find-a-deleted-file-in-the-project-commit-history
git log --left-right --graph --cherry-pick --oneline feature...branch # https://til.hashrocket.com/posts/18139f4f20-list-different-commits-between-two-branches
git diff main...                                                      # the PR diff - changes since I branched out from main

.. and ... notation

main: 🔴──🟠──🟢
            \
             🔵 B <- HEAD
git log main B
# 🟢 commit-main-3   (main)
# 🔵 commit-b-1      (HEAD -> B)
# 🟠 commit-main-2
# 🔴 initial commit1
git diff main..B
    diff main  B
    diff main    # (when you're on B)
# +🔵 commit-b-1
# -🟢 commit-main-3

git diff main...B
    diff main...
    diff $(git merge-base main B) B
# +🔵 commit-b-1
git log main..B
git log main..
git log ^main B
# 🔵 commit-b-1 (HEAD -> B)
# think of it as set difference: B \ main

git log main...B
git log main...
# 🟢 commit-main-3 (main)
# 🔵 commit-b-1 (HEAD -> B)
# think of it as the symmetric set difference: main Δ B

escape!

Why is escaping a character called escaping a character?

When a C compiler (or any other compiler) encounters a " it thinks to it self "huh, this is the begninning or end of a string!".

Say you need " in your string?

You have to tell the compiler "do not process this character like you always do, I want you to ESCAPE the default processing". You do this with "dear compiler this \" will become a double quotation mark in my string".

Likewise if you want a new line you need to tell the compile "do not process this n as a "n", escape the default processing and process it as a new line \n".

npm wsl EAI_AGAIN

Running npm ci on WSL (Windows Subsystem for Linux) failed with:

npm ERR! syscall getaddrinfo
npm ERR! errno EAI_AGAIN
npm ERR! request to http://registry.npmjs.org/nodemon failed, reason: getaddrinfo EAI_AGAIN registry.npmjs.org

dig registry.npmjs.org
^ confirmed that my Ubuntu WSL can't reach registry.npmjs.org

Solution

In WSL:

rm /etc/resolv.conf
bash -c 'echo "nameserver 8.8.8.8" > /etc/resolv.conf'
bash -c 'echo "[network]" > /etc/wsl.conf'
bash -c 'echo "generateResolvConf = false" >> /etc/wsl.conf'
chattr +i /etc/resolv.conf
^ use 8.8.8.8 (Google's DNS). Stop recreating resolv.conf on startup

In Windows:

Set-Content -Path "C:\Users\{user}\.wslconfig" -Value "[wsl2]`nnetworkingMode=mirrored"

Works now, also on my company's VPN.

kudos https://github.com/microsoft/WSL/issues/5420#issuecomment-646479747

more info https://gist.github.com/machuu/7663aa653828d81efbc2aaad6e3b1431

update 2025-03-18

Today the above solution didn't work - I could not reach 8.8.8.8 - I tested it with dig registry.npmjs.org. This fixed it today :confused:

WSL:

chattr -i /etc/resolv.conf
rm /etc/resolv.conf
rm /etc/wsl.conf

Windows:

rm "C:\Users\{user}\.wslconfig"

Post25

1024 * 1024 - this many bytes is an mibibyte (MiB).

A megabyte like a megameter is 10^6 bytes.

We all frequently say megabyte meaning a mibibyte. Like wise a kilobyte != kibibyte

Unit Abbreviation Size in Bytes
Kibibyte KiB 1,024
Mebibyte MiB 1,048,576
Gibibyte GiB 1,073,741,824
Kilobyte KB 1,000
Megabyte MB 1,000,000
Gigabyte GB 1,000,000,000

Network speeds are measured in Mbps - that is mega bits per second - that is 1,000,000 bits per second.

MB - Megabyte (SI) MiB - Mibibyte (IEC) Mb - Megabit (SI)

https://www.iec.ch/prefixes-binary-multiples

Some neat fsx F#

My company had a hackathon focused on data scraping/processing.

Each team had to scrape 3 endpoints. I came up with something similar to this:

open System
open System.Net.Http
open System.Text

let c = new HttpClient()
c.Timeout <- TimeSpan.FromSeconds(5.0)

let lockObject = new obj()
let printSync text =
    let now = DateTimeOffset.Now.ToString("O")
    lock lockObject (fun _ -> printfn "[%s] %s" now text)

let s = new HttpClient()
s.Timeout <- TimeSpan.FromSeconds(5.0)
s.DefaultRequestHeaders.Add("X-Sender", "this is me, Mario!")
let sendToDestination stream response = async {
    let template = """{
    "CreatedAt": "xxXCreatedAtXxx",
    "Stream": "xxXStreamXxx",
    "Data": [
        xxXDataXxx
    ]
}"""
    let payload = template.Replace("xxXCreatedAtXxx", DateTimeOffset.Now.ToString("O"))
                          .Replace("xxXStreamXxx", stream)
                          .Replace("xxXDataXxx", response)
    let! response = s.PostAsync("http://localhost:8080", new StringContent(payload, Encoding.UTF8, "application/json") ) |> Async.AwaitTask
    sprintf "%s done sending response code %A" stream response.StatusCode |> printSync
}

let scraper (url:string) stream = async {
    while true do
        try
            let! response = c.GetStringAsync(url) |> Async.AwaitTask
            do! sendToDestination stream response
            sprintf "scraped %40s sendTo %s" url stream |> printSync
        with
        | _ -> sprintf "failed to scrape/or send %40s" url |> printSync

        do! Async.Sleep 1000
}

let urls = [
    "https://jsonplaceholder.typicode.com/posts", "123"
    "https://jsonplaceholder.typicode.com/posts", "124"
    "https://jsonplaceholder.typicode.com/posts", "125"
]

urls
|> List.map (fun (url, stream) -> scraper url stream)
|> Async.Parallel
|> Async.Ignore
|> Async.Start

// Async.CancelDefaultToken()

Things to keep in mind:

  • always have a try/catch all exceptions in async/tasks/threads
    • you don't want your thread to die without you knowing
  • always set a timeout when scraping (default timeout in .NET is 100s which is excessive for this script)

A minimalistic http server to listen to our scrapers:

open System.Net
open System.Text

// https://sergeytihon.com/2013/05/18/three-easy-ways-to-create-simple-web-server-with-f/
// run with `fsi --load:ws.fsx`
// visit http://localhost:8080

let host = "http://localhost:8080/"

let listener (handler:(HttpListenerRequest->HttpListenerResponse->Async<unit>)) =
    let hl = new HttpListener()
    hl.Prefixes.Add host
    hl.Start()
    let task = Async.FromBeginEnd(hl.BeginGetContext, hl.EndGetContext)
    async {
        while true do
            let! context = task
            Async.Start(handler context.Request context.Response)
    } |> Async.Start

listener (fun req response ->
    async {
        response.ContentType <- "text/html"
        let bytes = UTF8Encoding.UTF8.GetBytes("thanks!")
        response.OutputStream.Write(bytes, 0, bytes.Length)
        response.OutputStream.Close()
    })