# yasint.dev — full text > Every published post, newest first, concatenated as markdown. # Your agent is probably using git worktrees > Coding agents reach for git worktrees as the unit of parallel work. The mental model and four commands to stay in control. April 20, 2026 · 12 min read · https://yasint.dev/agents-and-git-worktrees/ Tags: git, engineering, workflow --- I've used git worktrees for years. Mostly to review a PR without blowing up my current checkout, or to keep a long-running build isolated while I hacked on something else. Useful. Not life-changing. Then I started running coding agents on multiple tasks at once, and the work suddenly had to go _somewhere_. The obvious somewheres don't really work. A second branch? I already have one checked out. A fresh clone of the repo for every task? Slow, wasteful, everything drifts apart the moment I fetch. `git stash`? Fine for one task. Not four. That's when worktrees stopped being a nice little trick and started feeling like the thing they were always built for. My agents lean on them constantly now. Most of the time I don't even see it happen. --- ## What a worktree actually is OK so here's the thing. A git repo has one `.git/` directory. That's where every commit, every tree, every blob you've ever made lives. It's the database. A _worktree_ is a separate working directory that shares that database, but has its own HEAD, its own index, its own files on disk. That's it. That's the whole concept. In plain terms: multiple branches checked out at the same time, in different folders. No stashing. No "you have uncommitted changes, cannot switch branch." Each worktree minds its own business, and the shared `.git/` is the only thing tying them together. ![Shared .git with three working dirs on different branches](./worktrees.png "One database, three worktrees. Each one gets its own branch, its own index, its own files.") Before you ask: yes, you could just clone the repo three times. You'd also duplicate the object database three times, wait three times as long to set them up, and watch them drift independently every time you fetch. You could also branch-switch in your existing checkout. Fine, until you have uncommitted changes, a running dev server, or a build watcher that doesn't know the files changed out from under it. Worktrees sit in the middle. One database underneath, several working dirs alongside it. Making or deleting one stops feeling like a decision you have to think about. ```text my-repo/ ← main worktree (contains .git/) .git/ ← the shared object database src/ ../feature-x/ ← extra worktree on branch feature-x src/ ← fully independent working files ../bugfix-y/ ← extra worktree on branch bugfix-y src/ ``` --- ## Why agents reach for them Because isolation is the natural unit of parallel work. Two processes can't safely share a working directory. One agent runs `git checkout` mid-edit and the other's uncommitted changes vanish. A build writes to the same folder a watcher is staring at, and that watcher has a bad time. Give each task its own folder and most of this quietly stops happening. You could clone the repo for each task to get that isolation. Worktrees get you there without duplicating the object store or waiting out a fresh setup every time. Creating a worktree is nearly free. You get a fresh folder with its own HEAD and index, and the object database already on disk serves it. Nothing gets fetched or copied, which also means nothing to drift. So the shape of a reasonable agent is short: spin up a worktree on a new branch, do the work, merge or throw it away. That's the whole loop. If your agent runs multiple tasks in parallel without stomping on your working copy, this is almost certainly how. Mine does, constantly. --- ## What you give up by treating them as invisible plumbing Here's the part that snuck up on me. When _I_ create a worktree, I know it exists. I picked the path. I'll remember to clean it up. When an _agent_ creates one, none of that is true. The tool is fine. I've been using it for years. What's different now is that when the agent uses it, it all happens without me looking. A few ways this bites: **Orphan worktrees pile up on disk.** Agent crashed, got killed, forgot to clean up. Doesn't really matter why. The folder sticks around. `du -sh` quietly grows. `git status` in my main checkout doesn't know a thing about it. **Orphan branches multiply too.** I `rm -rf` the folder because it's the obvious move. The branch stays. The admin entry inside `.git/` stays. `git branch` gets longer and longer, and I start wondering where all those names came from. **`pwd` stops being reliable.** I open a terminal inside what I _think_ is my main checkout, run `git status`, and see unfamiliar changes. I'm in a worktree. Commands I run do things I didn't expect. **Commits get forgotten.** I made one in a worktree. Never merged it. The agent moved on. That commit is now reachable only from a branch I never look at, in a folder I've stopped opening. None of these are the agent's fault. The agent's doing what I asked. The gap is that I wasn't watching. --- ## The four commands you actually need Four. That's it. (Well, four plus one bonus at the end, but really, four.) ```sh git worktree add -b feature-x ../feature-x ``` Makes a new branch `feature-x` from HEAD, and sets up a new worktree at `../feature-x` checked out on it. Drop the `-b` if the branch already exists. Use this when you want to spin up an isolated checkout yourself. Your agent does it for you, yes. You can too. ```sh git worktree list ``` Every worktree. Its path, its HEAD, its branch. If something feels off, run this first. The annotations matter. `(bare)`, `(detached HEAD)`, `prunable` all tell you something. ```sh git worktree remove ``` The _right_ way to delete a worktree. Removes the folder and the admin entry git keeps inside `.git/`. `rm -rf` is what leaves the admin entry behind. Don't do that. ```sh git worktree prune ``` For when you already did the thing I just told you not to do. `prune` reaps admin entries whose folders have vanished. Safe to run whenever. Good thing to run after you've been sloppy. Oh, and: `git worktree add --detach ` makes a worktree in detached-HEAD state, no new branch. Useful for a one-off diagnostic checkout. That's the bonus. --- ## Go break it Here's the whole thing in one sandbox. Add a few worktrees. Try `rm -rf` on one. The folder vanishes but the admin entry inside `.git/worktrees/` is still there, flagged red. Run `list` and spot the `prunable` annotation. Then `prune` to reap it. That's the contrast worth learning.
--- Agents handle worktrees fine. They really do. Still, if you can't name what they're doing, you can't audit the work, recover when something fails, or clean up after. So look. Four commands. --- # So Much for O(1) > I'd known the theory for years but couldn't have built one from scratch. So I did, with interactive demos you can poke at. March 25, 2026 · 34 min read · https://yasint.dev/hash-maps-under-the-hood/ Tags: data-structures, interactive --- `HashMap`, `dict`, `Map`, `object`, whatever your language calls it, you probably use one every day. I definitely do. I'd known the theory for years. Hashing, buckets, O(1), sure. If you'd asked me to walk through what actually happens when you call `map.get("email")`, I would've hand-waved about hashing and buckets and moved on. I knew it was "O(1)" because that's what you say in interviews and everyone nods. Could I have built one? No. So I did. And honestly? It's not that complicated. It's just _really_ clever. This article walks through the whole thing with interactive demos you can poke at. By the end you'll understand hash maps better than most people who use them daily. --- ## It all starts with arrays OK so here's the thing. Arrays have this one superpower that makes everything else possible: **instant access by index**. When you do `arr[3]`, the CPU doesn't search for anything. It just calculates `base_address + 3 * element_size` and jumps straight there. O(1). Done. No questions asked. That's the magic trick we want for our hash map. Our keys aren't numbers though. They're strings like `"email"` and `"name"` and `"age"`. You can't just do `arr["email"]` (well, you can in JavaScript, but that's a whole different rabbit hole). Arrays need _numbers_. So we need something that takes any key and turns it into a number. A number we can use as an array index. That something is called a **hash function**. ## Turning gibberish into numbers A hash function takes any input (a string, a number, whatever) and spits out an integer. Same input, same output, every time. Different inputs _usually_ give different outputs. That "usually" is going to bite us later. Keep it in the back of your mind. Here's a real one. It's called **djb2**, first reported by [Dan Bernstein](https://en.wikipedia.org/wiki/Daniel_J._Bernstein) in `comp.lang.c` many years ago. Not the best hash function by modern standards, but fast, simple, and perfect for understanding the concept: ```c unsigned long hash(unsigned char *str) { unsigned long hash = 5381; int c; while (c = *str++) hash = ((hash << 5) + hash) + c; /* hash * 33 + c */ return hash; } ``` Each iteration multiplies hash by 33 (that's what `(hash << 5) + hash` does) and adds the next character. Dead simple. The interactive demos below use a JavaScript port of the same algorithm. Why 33? According to [oz's classic hash function page](https://web.archive.org/web/2024/http://www.cse.yorku.ca/~oz/hash.html), "the magic of number 33 — why it works better than many other constants, prime or not — has never been adequately explained." Why 5381? [Nobody really knows](https://stackoverflow.com/questions/10696223/reason-for-5381-number-in-djb-hash-function). Bernstein picked these constants and they just work. Sometimes engineering is just vibes and benchmarks. The raw hash is a massive number, way bigger than our array. So we squeeze it into range with modulo: `hash % capacity`. If we have 8 slots, `djb2("email") % 8` gives us a number between 0 and 7. That's our index. (Technically the hash function itself is O(key length), not O(1), but keys are usually short so we treat it as constant.) Go ahead, type anything in here and watch it get hashed:
## Where stuff actually goes Cool, so now we have a function that turns any string into a number between 0 and 7. Now what? We make an array (usually called a **bucket array**) and use that number to decide _which slot_ each key-value pair lands in. You want to store `"name": "Alice"`? Hash `"name"`, get a bucket index, drop the entry there. If the key already exists, just update its value. That's it. Let me walk you through it. Hit "Next" to step through each insertion one by one:
Three insertions, three different buckets. Each lookup is just: hash the key, jump to the bucket, grab the value. O(1). This is the happy path. Remember that "usually" from earlier? Yeah. Time to deal with that. ## When things go wrong (kind of) Here's a fact that might make you uncomfortable: we have 8 buckets and an _infinite_ number of possible strings. There is literally no way to guarantee every key gets its own slot. This is the [pigeonhole principle](https://en.wikipedia.org/wiki/Pigeonhole_principle): more pigeons than holes means at least two pigeons are sharing. Math doesn't care about your feelings. So what happens when `"name"` and `"city"` both hash to bucket 6? Does it just... break? Nope. The simplest solution is called **chaining**. Instead of each bucket holding one entry, it holds a _list_ (a chain) of entries. On lookup, you hash to the bucket, then walk the chain comparing keys until you find yours. It's a tiny linear search, but only within that one bucket. This is way easier to see than to explain. Step through it:
So a collision doesn't break anything. It just adds one extra comparison per chained entry. As long as the chains stay short (one or two entries) you barely notice. Wait. What _stops_ the chains from getting long? If I keep cramming entries in, won't everything pile up into a few long chains and basically become a linked list? At that point our "O(1)" hash map is just an O(n) list with extra steps. So much for constant time. Turns out the people who designed this thought of that. ## When the table says "I'm full" The **load factor** is dead simple: `entries / capacity`. It tells you the average number of entries per bucket. With a good hash function, keeping the load factor bounded keeps chain lengths bounded too. That's the whole trick. [Java](https://docs.oracle.com/javase/8/docs/api/java/util/HashMap.html) draws the line at **0.75**. Other languages choose differently: [Python](https://github.com/python/cpython/blob/main/Objects/dictobject.c) uses ~0.67, [Rust](https://github.com/rust-lang/hashbrown/blob/master/src/raw.rs#L182) ~0.875, [C++](https://en.cppreference.com/w/cpp/container/unordered_map/max_load_factor) defaults to 1.0. Lower means more wasted space, higher means more collisions before a resize. Cross the threshold, and the hash map _doubles_ its size and **rehashes every single entry** into the new, larger array. This blew my mind when I first got it. The old bucket indices are completely invalid after a resize because `hash % 4` gives a totally different result than `hash % 8`. So _every_ entry has to be re-placed. It's expensive, but it happens rarely enough that the math works out. Keep pressing "Insert" here and watch the load factor bar creep up. When it crosses the line, watch closely:
See what happened there? The table doubled, every entry found a new home, and the load factor dropped back down. Chains got shorter. Performance restored. This is why people say hash maps have **amortized** O(1) performance. _Most_ operations are instant. Every now and then, one unlucky insert triggers a resize that costs O(n). Each resize doubles the capacity, so you need _n_ more cheap inserts before the next expensive one. The total cost of all resizes is n + n/2 + n/4 + ... which is less than 2n. Spread across n inserts, that's O(1) each. Pay a little now, save a lot later. --- ## Go break it OK, theory's done. Here's a full hash map sandbox. You can `put`, `get`, and `del` keys. Try to cause a collision on purpose (hint: try `"name"` and `"city"`). Fill it up until it resizes. Look up a key that's buried deep in a chain and watch it traverse. Or hit "flood bucket 0" and watch what happens when an attacker crafts keys that _all_ land in the same bucket. Your O(1) map becomes a linked list. This is a real attack vector: web frameworks parse query strings, JSON, and form data into hash maps, so an attacker controls the keys by controlling the HTTP request. The fix is using a keyed hash function (like SipHash) with a per-process random secret, so an attacker can't predict which keys collide. Python added [hash randomization](https://www.ocert.org/advisories/ocert-2011-003.html) in 3.3 (2012) after a disclosure in late 2011, then switched to [SipHash in 3.4](https://en.wikipedia.org/wiki/SipHash#Usage) (2014).
The whole thing in one line: > **key → hash(key) → index = hash % capacity → bucket[index] → walk chain → value** Everything above is just unpacking this pipeline. ## What I left out (the rabbit hole goes deep) What I showed you is **chaining**, the simplest collision strategy. Most production hash maps today actually use a different approach, and the optimizations go deep: **Open addressing** is what most modern hash maps use instead of chaining. When there's a collision, you don't build a linked list, you just look for the next empty slot in the array itself. This has way better [cache locality](https://en.wikipedia.org/wiki/Locality_of_reference) because you're scanning contiguous memory instead of chasing pointers all over the heap. The tradeoff: deletion requires tombstones and is genuinely tricky, which is part of why chaining persisted as long as it did. [Robin Hood hashing](https://programming.guide/robin-hood-hashing.html) is a particularly beautiful variant that "steals from the rich" by letting entries with longer probe distances bump out entries with shorter ones. Cute name, clever algorithm. (Java's `HashMap` still uses chaining but converts long chains into red-black trees at length 8, so its worst case is O(log n) not O(n). Clever hybrid.) **SIMD probing** is what Google's [Swiss Table](https://abseil.io/about/design/swisstables) design does (implemented in Abseil C++ and, independently, in Rust's [hashbrown](https://github.com/rust-lang/hashbrown) which backs its standard `HashMap`). It uses CPU vector instructions to check 16 slots' metadata bytes in a single operation, then only does full key comparison on matches. Extremely fast. If you want to go deep, [Matt Kulukundis's CppCon talk](https://www.youtube.com/watch?v=ncHmEUmJZf4) is an absolute banger. **Power-of-two sizing** is a neat trick: when your capacity is always a power of 2, you can replace the expensive modulo with a bitwise AND: `hash & (capacity - 1)` does the same thing, way faster. Most modern implementations do this. **Better hash functions** fall into two camps. [SipHash](https://en.wikipedia.org/wiki/SipHash) is a keyed PRF designed for adversarial resistance: give it a per-process secret key and an attacker can't predict collisions. [xxHash](https://cyan4973.github.io/xxHash/) and [wyhash](https://github.com/wangyi-fudan/wyhash) are built for raw speed and better distribution, not security. Different tools for different problems. --- The core idea never changes though: hash the key, find the bucket, handle collisions, resize when it gets full. I wish I'd looked inside sooner. --- # We Might All Be AI Engineers Now > The models are good now. But most people still miss the point. March 5, 2026 · 5 min read · https://yasint.dev/we-might-all-be-ai-engineers-now/ Tags: ai, engineering, tools, agents --- I enjoy writing code. Let me get that out of the way first. The problem solving, the architecture decisions, the feeling when something clicks into place. That hasn't changed. What *has* changed is everything around it. Lately I've been spending most of my time writing agents and tools. Building systems that supervise AI agents, training models, wiring up pipelines where the AI does the heavy lifting and I do the thinking. Honestly? I'm having more fun than ever. Everyone knows the models are good now. That's not news. But most people still miss the point. They see AI-generated code, call it slop, and move on. Sure, unguided, it *is* slop. But guided? The models can write better code than most developers. That's the part people don't want to sit with. **When guided.** When you know what you want. When you know what architecture to reach for. When you understand the tradeoffs and can articulate them clearly. The game goes on easy mode. I'm building something right now. I won't get into the details. You don't give away the idea. But it involves concurrent graph traversal, multi-layer hashing strategies, AST parsing, and file system watchers all wired together. That's not a weekend hack. But the AI is writing the traversal logic, the hashing layers, the watcher loops, while I design the architecture and decide how the system should behave when state changes propagate. I'm shipping in hours what used to take days. Not prototypes. Real, structured, well-architected software. Debugging? Debugging is on steroids now. I run multiple agents at once, feed them my thinking. Here's what I suspect, here's where I'd look, here's what doesn't make sense. They fan out and dig. It's like having my problem-solving instincts multiplied across five brains at the same time. I still drive the intuition. The agents just execute at a speed I never could alone. I haven't written a boilerplate handler by hand in months. I haven't manually scaffolded a CLI in I don't know how long. I don't miss any of it. The problem is: you can't justify this throughput to someone who doesn't understand real software engineering. They see the output and think "well the AI did it." No. The AI *executed* it. I designed it. I knew what to ask for, how to decompose the problem, what patterns to use, when the model was going off track, and how to correct it. That's not prompting. That's engineering. When someone *without* that intuition tries the same thing? They get spaghetti. Code that compiles but doesn't scale. An architecture that falls apart the moment you add a second requirement. The model doesn't save you from bad decisions. It just helps you make them faster. The skill isn't writing code anymore. The skill is *knowing what to build and how it should work*. The code is just the output. I'm not worried. I can still reverse a binary tree without an LLM. I can still reason about time complexity, debug a race condition by reading the code, trace a memory leak by thinking. Because I studied my ass off before any of this existed. That foundation isn't decoration. It's the reason the AI is useful to me in the first place. Without it, you don't know when the model is wrong. You don't know what questions to ask. You don't know what *good* looks like. Most people underestimate how much that matters. Here's the thing though. That foundation isn't gatekept anymore. You can learn anything now. I mean *anything*. The resources, the tools, the mentors-on-demand, it's all there. The barrier to entry has never been lower. So if you haven't built that intuition yet, you have no excuse. Start now. If you've spent years building it, understanding systems, understanding architecture, understanding why things break, you're not being replaced. You're being amplified. I think we all might be AI Engineers now, and I'm not sure how I feel about that. What I do know is this: when I look at a team or a workplace, one of the first things I notice now is how they think about AI. Not whether they've adopted every tool, but whether they're curious. Whether they're paying attention, because this isn't a phase. This is the direction. Teams that get it? Those are the ones I want to be on. --- **Edit** — 2026-03-06T17:06:41Z**:** This post got some great discussion on [Hacker News](https://news.ycombinator.com/item?id=47272734), and a lot of the pushback was fair. So I want to clarify a few things. I'm not vibe coding. Every line of AI-generated output gets reviewed. Every statement. If I don't understand what it's doing, it doesn't ship. That's non-negotiable. There's a line between using AI well and blindly delegating to it. For me that line is scope. Small, well-defined tasks with verifiable output? That's where agents shine. But when the problem requires deep context about the system, the kind of knowledge you only have from working in it, I'm faster doing it myself. Knowing when to use the tool and when to put it down is half the skill. I also want to be clear about something: everything I know, I learned from people. Senior engineers who reviewed my rough pull requests. Colleagues who took the time to explain why my thinking was off. Books. Feedback. Years of writing bad code and slowly understanding why it was bad. That foundation is the reason AI is useful to me now. Without it, you can't tell when the model is wrong. You can't course-correct what you don't understand. I look at code I wrote 10 years ago and I'm humbled by how far off I was. But that struggle is exactly what built the intuition. We studied fundamentals for a reason. That reason didn't go away just because the tools got better. --- # The Hardest Bug I Ever Fixed Wasn't in Code > Software engineering is 50% code and 50% people — and almost everything written about it ignores the people part. What I learned the hard way about speaking up. February 7, 2026 · 6 min read · https://yasint.dev/hardest-bug-wasnt-in-code/ Tags: engineering, career --- I was sitting in sprint planning a few years ago, already working on three things I hadn't finished, when someone added "one more small thing" to the sprint. Everyone nodded. I nodded too. I knew it wasn't going to work. I said nothing. I think about that moment a lot. Not because of the task itself — I don't even remember what it was... but because of how automatic the silence felt. Like pushing back wasn't even _an_ option. And the thing is, nobody ever prepared me for that part of the job. I'd read hundreds of tutorials. I could set up a Kafka pipeline, debug a race condition, containerize pretty much _anything_. But telling a lead that the timeline was broken? That I was drowning? There's no Stack Overflow thread for that. Software engineering is 50% code and 50% people. But almost everything written about it pretends the people part doesn't exist. ## Why we only talk about code Code is safe. You can write "How to build a REST API with Spring Boot" and _nobody_ gets uncomfortable. There's a clear input, a clear output. It's clean. But writing about how to tell a VP of Product that their deadline is physically impossible? That involves emotions, power dynamics, ego — yours and _theirs_. Technical platforms tend to wave it away. Soft skills. Not real engineering. Except there's nothing soft about it. It's the thing that burns people out. The thing that quietly breaks teams while everyone's busy debating framework choices. ## The pressure flows downhill Something I've learned over 8+ years: leaders aren't villains. Most of the time they're under just as much pressure as you are, it's just a different shape. Stakeholders pressure the VP. The VP pressures the engineering manager. The engineering manager pressures the lead. The lead pressures the engineer. And the engineer is the last stop — there's nobody left to pass it to. What makes it harder is the complexity gap. You're working on three things at once. One is blocked by an infrastructure issue nobody else knows about. Another looks simple from the outside but depends on a fix that touches half the system. You know that releasing one API endpoint means bumping a kernel version, and god knows what tests that breaks. But to leadership, it's just a ticket on a board. A checkbox. That's not their fault. But making the invisible _visible_ — that falls on you. ## The things we say yes to Let me ask you something. Have you ever taken on more than you could handle, not because someone forced you, but because your ego wouldn't let you say _no_? I have. More than once. You say yes to everything because saying no feels like admitting you can't keep up. So you pile it on. You tell yourself you'll figure it out. And then the procrastination kicks in — not because you're lazy, but because you're so overwhelmed you don't know where to start. So you start nothing. Then mid-sprint someone asks about that task, and you hear yourself say "yeah I've started it, got through the initial setup" when you haven't opened the file once. You were buried in something else entirely. I think most of us have been there. We just don't admit it. ## What happens when you don't speak up I want to share something personal here. Not as a failure story — as a lesson that took me too long to learn. A few years ago, at a previous workplace, I burned out. It wasn't sudden. It built up over months—task after task, sprint after sprint, saying yes when I should've said "not right now." And on top of work, things in my personal life took a turn I hadn't expected. Family stuff. Pressure I couldn't have planned for. It all stacked up in a way that left me with nothing. I didn't rage-quit. I wrote a calm, professional resignation letter and sent it to all my leads and my engineering manager. One-month notice. Done. But I didn't actually want to leave. The email wasn't really a resignation — it was the sound of someone who had run out of quieter ways to say "I can't do this anymore." I ended up withdrawing it, staying on, and carrying the awkwardness of what I'd just put everyone through. My leaders didn't expect it. It caught them off guard in a way that I'm sure was uncomfortable for them too. The whole thing felt like a burden I'd created out of a conversation I never had. What I wish I'd done was have one honest conversation. Not a formal email blast. Just sitting down with one lead or one manager and saying "I'm carrying more than what's visible right now." Even the personal stuff — not every detail, but enough. I wish I'd learned earlier that saying _no_ to a few things doesn't make you a failure. It buys you time to do the things you said _yes_ to properly. ## How to actually have the hard conversation So what do you do when the deadline is wrong and everyone's pretending it's _fine_? Timing matters more than being right. Don't drop bad news in a standup with fifteen people listening. Find a 1:1, a quieter moment. Give the other person room to actually hear what you're saying. And be specific. "We need more time" is easy to wave away. But "this endpoint depends on a service that needs a kernel version bump, which means full regression testing, and we haven't even scoped that" — that's a lot harder to _dismiss_. The goal is to make the complexity real for someone who isn't in the code _every day_. One conversation usually _isn't_ enough, and that's fine. Come back to it. Not aggressively—just consistently. "I flagged this last week and nothing's changed on our end, so I want to raise it again." That kind of persistence isn't annoying. It's responsible. And here's the thing I wish someone had told me earlier: most leaders are decent people. Every manager and lead I've worked with, past and _present_, has been supportive when I approached them honestly. They want to make good decisions. They just need the full picture, and sometimes you're the only person who can give it to them. ## The last thing The hardest bug I ever fixed was learning to _speak up_. Learning that "no" isn't shame — it's just buying time to do things right. That a resignation email should never be the first time your manager hears you're struggling. I don't have all the answers. I'm still figuring this stuff out. But if I could go back and tell myself one thing, it would be this: just have the conversation. One person. One honest moment. Everything else follows from that. --- # Why I Switched to Podman (and Why You Might Too) > I was a Docker loyalist for years, until I needed rootless containers. What won me over — and what still bites. February 2, 2026 · 3 min read · https://yasint.dev/why-i-switched-to-podman/ Tags: docker, tools, linux --- I've been a Docker loyalist for years. But lately, I've been experimenting with [Podman](https://podman.io/), and honestly? It's grown on me. The switch started out of necessity. I've been working on [FEGA](https://github.com/ELIXIR-NO/FEGA-Norway) for a while, I needed rootless containers for security reasons. Docker can do rootless, but it always felt like an _afterthought_. Podman was built for it from day one. ## What's the Difference? At the surface, not much. Podman is CLI-compatible with Docker. This works: ```bash alias docker=podman ``` Seriously. Most commands just work. But [architecturally they're different](https://www.redhat.com/en/topics/containers/what-is-podman#what-makes-podman-different). Docker runs a daemon. Every container talks to `dockerd`, which runs as _root_. Podman **doesn't have a daemon**. Containers run as child processes of your shell. No daemon means no single point of failure, no root process sitting there waiting. ## The Gotchas ### A few things bit me: 1. Compose — There's `podman-compose` and `podman play kube`, but honestly? We still use `docker-compose` with Podman as the backend. It just works. Set `DOCKER_HOST` to your Podman socket and your existing compose files run unchanged. Sometimes the boring solution is the right one. 2. Networking — Docker's bridge network just works out of the box. Podman needs more hand-holding when containers need to talk to each other: ```bash podman network create mynet podman run --network mynet --name app1 myimage podman run --network mynet --name app2 myimage ``` 3. Build caching — This is where Docker still wins. BuildKit has SPOILED ME. ### Docker BuildKit It builds independent stages in parallel. If our multi-stage Dockerfile has a `frontend` and `backend` that don't depend on each other, they build at the same time. It also does [content-addressed caching](https://docs.docker.com/build/cache/)! Meaning that it can reorder your Dockerfile and the cache still hits if the files haven't changed. For example: ```dockerfile FROM node:20 AS frontend # build frontend (base image #1) ... FROM golang:1.22 AS backend # build backend (base image #2)... FROM alpine # build final stage (base image #3)... COPY --from=frontend /ui/dist /usr/share/nginx/html COPY --from=backend /api/app /app ``` ![BuildKit vs Buildah](./buildkit-vs-buildah.png) Podman uses [Buildah](https://github.com/containers/buildah), which doesn't do parallel stages _yet_[^1]. For simple images you won't notice. For anything with heavy multi-stage builds, you will. [^1]: If _you_ run multiple `buildah bud` commands at the same time (e.g., via CI jobs), those builds can run _concurrently_ — but that’s just external parallelism, not Buildah optimizing one build. ## When to Use What I still reach for Docker when I need fast builds, or I'm just spinning up something throwaway. For anything _security-sensitive_ or closer to production? Podman. They read the same Dockerfiles, pull from the same registries, produce OCI-compliant images. Switching between them costs nothing. --- Been away from writing for most of 2025. Feels good to be back. --- # The World is Stochastic > Stein W. Wallace: Embrace uncertainty, curiosity, and adaptability in careers. October 18, 2024 · 2 min read · https://yasint.dev/the-world-is-stochastic/ Tags: career, philosophy --- Yesterday, I had the chance to listen to a talk by [Stein W. Wallace](https://www.nhh.no/en/employees/faculty/stein-w.-wallace/), an alumni from the University of Bergen (UiB), and it was incredibly insightful. As someone who's had an impressive career, he shared thoughts that were both relatable and motivating. What really stuck with me was his **emphasis on uncertainty in building a career**. He reminded us that no matter how much we try to plan things, life has a way of throwing curveballs—and that's _okay_.

One key takeaway was how important it is to _stay curious_. According to Wallace, curiosity is what keeps the doors open. When you’re open to new opportunities, new ideas, and even unexpected changes, that’s when real growth happens. He encouraged us to never shy away from uncertainty but to embrace it, because the best things often come from the paths we never thought we’d take, drawing from his own career and life experiences.

Wallace also touched on the balance between our actions and our thoughts. He emphasized that we often prepare for potential risks, investing time and resources into safeguarding our futures. However, this can lead to a dilemma: while we might make prudent choices to mitigate risk, we can’t predict whether those preparations will ultimately be necessary. This illustrates the challenge of navigating uncertainty—how to weigh the potential for loss against the possibility that our worries might never materialize. It was refreshing to hear someone with his experience say that success isn’t about following a set blueprint, dictated by anyone else, but about exploring, learning, and being willing to step through new doors when they open. He even mentioned how this idea ties back to his [books](https://scholar.google.com/citations?user=U1WExVYAAAAJ&hl=en), which delve into these themes of uncertainty, decision-making, and adapting in both personal and professional contexts. --- # Debugging a running Java app in Docker > How to Attach a Debugger to a Running Java Application Inside a Docker Container May 29, 2024 · 5 min read · https://yasint.dev/debugging-running-java-app-docker/ Tags: java, docker, debugging --- Debugging a Java application running inside a Docker container can be a straightforward process if you follow these simple steps. In this guide, I will walk you through the necessary steps to attach a debugger to your Java application. ## Java application First of all, let's create a sample Java application to debug. We'll be using [Javalin](https://javalin.io/) for creating a simple API. ## Project structure Here is the folder structure for our project: ```txt . ├── Dockerfile ├── pom.xml ├── src/ │ ├── main/ │ │ ├── java/ │ │ │ └── dev/ │ │ │ └── yasint/ │ │ │ └── Main.java │ │ └── resources │ └── test/ │ └── java └── target ``` ## Maven configuration (`pom.xml`) Create the pom.xml file with the following content: ```xml isClosed 4.0.0 dev.yasint debugger 1.0-SNAPSHOT jar app 21 21 UTF-8 io.javalin javalin 6.1.3 org.slf4j slf4j-simple 2.0.10 ``` ## Docker configuration (`Dockerfile`) Create the Dockerfile with the following content: ```Dockerfile FROM eclipse-temurin:21-jre-alpine COPY target/app.jar /app/app.jar WORKDIR /app EXPOSE 7070 CMD ["java", "-jar", "app.jar"] ``` ## Application code (`Main.java`) Create the main application file `Main.java` under `src/main/java/dev/yasint/`: ```java package dev.yasint; public class Main { public static void main(String[] args) { Javalin.create() .get("/hello", Main::hello) .start(7070); } private static void hello(Context ctx) { ctx.result("Hello World!").status(200); } } ``` ## Building the application To build the application, run the following commands: ```bash mvn clean install package ``` ## Running the application in Docker Now let's build and run the Docker container with our application. We can use the following commands: ```bash docker build -t app . docker run -d -p 7070:7070 --name java_app app ``` Now our application should be running and accessible at http://localhost:7070/hello. ## Attaching the debugger Now the fun part begins! To enable debugging, we need to modify the Docker configuration to include the necessary JVM options. ## Debugging configuration Before configuring our IDEs, we need to adjust how we run our Java application. Specifically, we need to enable the [Java Debug Wire Protocol (JDWP)](https://docs.oracle.com/en/java/javase/21/docs/specs/jdwp/jdwp-spec.html). JDWP is the protocol that facilitates communication between a debugger and the Java Virtual Machine (JVM) being debugged (referred to as the target VM)[^1]. I am using JDK `21` and the following option to enable debugging. You can use the same option if you are using JDK `9` or later. ```txt -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005 ``` However, if you're using a JDK version below `9`, the configuration is slightly different. Below, I outline how to configure various versions of the JDK. ```bash caption="Alternative configs for different Java versions" // For JDK 5 to 8: -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 // For JDK 1.4.x -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 // For 1.3.x and below -Xnoagent -Djava.compiler=NONE -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=5005 ``` The next step is to modify the Dockerfile's `CMD` command to include the debugging configuration: ```bash CMD ["java", "-agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:5005", "-jar", "app.jar"] ``` ## Running the application with debugging enabled Rebuild and run the Docker container with the updated configuration: ```bash docker stop java_app && docker rmi app --force docker build -t app . docker run -d -p 7070:7070 -p 5005:5005 --name java_app app ``` > **Publishing the Debugging Port** > > It's crucial to map the debugging port (`5005`) to the host machine, especially since we are running inside a container. This allows the debugger on the host machine to communicate with the JVM inside the container, enabling efficient debugging. ## Configuring your IDE Now we can set up our IDE to connect to the remote JVM for debugging. ## For IntelliJ IDEA: 1. Go to `Run` > `Edit` Configurations. 2. Click the `+` icon and select `Remote JVM Debug`. 3. Configure the remote debugger with the following settings: - **Name**: `MyApp` (literally anything you want) - **Host**: `localhost` - **Port**: `5005` 4. Apply the changes and click `OK`. ![IntelliJ IDEA remote debugger configuration steps](./intellij-steps.png) ## For Eclipse: I don't have a detailed explanation for configuring Eclipse, but the steps are quite similar to what I've described above. For a reasonably up-to-date guide, I found this helpful article from Eclipse.org: [Eclipse Debug Configuration](https://www.eclipse.org/community/eclipse_newsletter/2017/june/article1.php). ## Starting the debugging session 🐞 Start the remote debugging session from your IDE. You should now be connected to the JVM running inside your Docker container. You can set breakpoints, inspect variables, and step through your code as if it were running locally. ![Starting the debugging session in IntelliJ](./intellij-run.png "Starting the debugging session in IntelliJ") ## Conclusion By following these steps, we have successfully created a simple Java application with Javalin, packaged it into a Docker container, and attached a debugger to it. This setup allows for effective debugging, enabling you to troubleshoot and optimize your application efficiently. [^1]: Note that JDWP is optional and may not be available in all JDK implementations. Its presence allows for the use of the same debugger across different JVMs --- # Why is it UTC and not CUT? > The timekeeping compromise February 21, 2024 · 2 min read · https://yasint.dev/why-utc-is-not-cut/ Tags: time, history --- Have you ever wondered why we use the abbreviation UTC for [Coordinated Universal Time](https://en.wikipedia.org/wiki/Coordinated_Universal_Time) instead of the more intuitive CUT? This peculiar choice is the result of a linguistic compromise that highlights the importance of international collaboration. And this is what I thought exactly. I work with UTC time all the time and how come I never thought of this, huh? It's funny how some things can be right under our noses, yet it takes a moment of curiosity to uncover the fascinating stories. ## The linguistic standoff The term "Coordinated Universal Time" translates to _Temps Universel Coordonné_ in French, leading to a debate over the abbreviation: `CUT` in English and `TUC` in French. To avoid favoring one language over the other, the international community sought a neutral ground. ## The solution: `UTC` The compromise was the abbreviation `UTC`, a decision that underscored the collaborative spirit of the global community. Adopted in **1960** by the [International Telecommunication Union (ITU)](https://en.m.wikipedia.org/wiki/International_Telecommunication_Union) and the [International Astronomical Union (IAU)](https://en.m.wikipedia.org/wiki/International_Astronomical_Union), UTC stands as a testament to the importance of finding common ground, beyond linguistic barriers. ## Significance and impact UTC is now integral to our global timekeeping, essential for everything from internet synchronization to international travel. This choice of abbreviation reflects a broader commitment to inclusivity and cooperation, setting a precedent for how global standards can transcend cultural and linguistic differences. The abbreviation `UTC`, rather than `CUT` or `TUC`, symbolizes more than just a compromise; I think it represents the power of international collaboration. It's a reminder that through mutual understanding and shared goals, we can overcome linguistic divides and establish standards that serve the global community. It's a decision that highlights the importance of unity and collective action in shaping a world that recognizes and respects _diversity_. --- # Deep prop drilling in ReactJS > ReactJS Deep Prop Drilling: Effective Solutions and Best Practices December 26, 2023 · 9 min read · https://yasint.dev/react-deep-prop-drilling/ Tags: react, javascript, frontend --- Welcome to the intricate world of ReactJS, where prop drilling often becomes a tricky puzzle to solve. You're probably used to passing props down the component tree, but have you noticed how this gets messier as your app grows? In this article, I'm going to demonstrate this exact challenge. Forget about the basic _what_ and _why_ of React—let's tackle the _how_ to properly manage props in complex applications. Ready to simplify your React life? Let's dive in! ## Problem? Essentially, deep prop drilling is all about passing props through _multiple_ component layers. Lets picture a scenario: you have a grandparent, parent, and child component. The top-level application holds data that the child needs, but to get there, it must travel through the grandparent and parent, even if the parent doesn't need _it_. ![An annotated diagram depicting deep prop drilling in ReactJS](./react-prop-drilling-example.png "Prop drilling through four component layers: Application provides and mutates props, intermediate components pass them through unconsumed, and Child renders the final value.") This seemingly simple task can lead to several issues: - **Maintainability Concerns**: As your application grows, tracking and managing these props through various layers becomes a Herculean task. - **Increased Complexity**: With props weaving through multiple components, the relationship between them becomes convoluted, turning your code into a complex web that's hard to untangle. - **Potential for Bugs and Decreased Readability**: More props snaking through more components increase the chance for bugs. It also makes your code less readable, turning what should be a simple update into a debugging nightmare. When we peel back the layers of our React applications, the repercussions of deep prop drilling are laid bare. It’s not just about the extra code; it’s the ripple effect on code quality and the daily life of a developer that deserves attention. ## On code quality Consider a feature as simple as adding a user's preference. If this preference needs to reflect across multiple components, without deep prop drilling, the implementation is straightforward. However, with deep prop drilling, you must thread this preference through various unrelated components, bloating each with unnecessary props. This bloat can _obscure_ the intended purpose of components, leading to a codebase that’s harder to understand and modify. ## On developer experience For the person writing the code, this means more headaches. Every time you want to add or fix something, you have to follow a trail of breadcrumbs through your code to find where everything connects. It's like untangling a knotted-up necklace — time-consuming and frustrating. ## A real example Let's say you have a little switch in your app that changes the application theme. Simple, right? But with deep prop drilling, you need to send that switch's "_light_" or "_dark_" state through every level of your app. As your app grows, this once-simple switch can become a big hassle, turning a quick update into a big project. This is what I mean. The following `App` component holds the state for the `theme` and a method called `toggleTheme` to change it. ```js name="App.js" caption="The top-level component" const App = () => { const [theme, setTheme] = useState('light'); const toggleTheme = () => { setTheme(theme === 'light' ? 'dark' : 'light'); }; return (
); }; ``` The `theme` and `toggleTheme` are passed down through `Grandparent` and `Parent` components. ```js name="Grandparent.js" caption="Two layers above the toggle switch" const Grandparent = ({ theme, toggleTheme }) => { return (
); }; ``` ```js name="Parent.js" caption="One layer above the toggle switch" const Parent = ({ theme, toggleTheme }) => { return (
); }; ``` And finally, the `Child` component contains a button that actually toggles the theme. ```js name="Child.js" caption="The component that contains the toggle switch" const Child = ({ theme, toggleTheme }) => { return (
); }; ``` See? This example clearly shows what deep prop drilling looks like: we're passing the `theme` and `toggleTheme` all the way down to the `Child` component that actually needs to use them.

Honestly, I'm not a fan of this approach. Having worked with many React codebases, I find it _frustrating_ to wade through such code. It feels like being in a maze, trying to trace back where everything comes from and where it's supposed to go. But nonetheless, we sometimes have to deal with it, especially when working with older React codebases where this pattern is all too common.

This is the scenario we are aiming to refactor in later sections to avoid deep prop drilling. ## Navigating away from deep prop drilling In the React world, deep prop drilling is like navigating a maze. But no worries, we have smart ways to bypass this. We’re going to dive into two common techniques. ## Using React Context This is our first approach to avoid deep prop drilling. React Context acts like a messenger, delivering props directly to components, no matter their level in the tree. It's a straightforward way to share data across different components without the hassle of passing props through each level. React Context allows you to share values like state and functions across your component tree without having to pass props down manually at every level. To use React Context, you first create a context using `createContext`. Then, you wrap your component tree with a `Context.Provider`, which allows all child components to access the context's value. Here's our refactored code: ```js // Create a Context for the theme const ThemeContext = createContext({ theme: "light", toggleTheme: () => {} }); // A component that provides the theme to its children const ThemeProvider = ({ children }) => { const [theme, setTheme] = useState("light"); const toggleTheme = () => { setTheme(t => (t === "light" ? "dark" : "light")); }; return ( {children} ); }; const Grandparent = () => (
); const Parent = () => (
); // Use the Context in the Child component const Child = () => { const { theme, toggleTheme } = useContext(ThemeContext); return ( ); }; // Layout reads the theme from context and owns the top-level wrapper const Layout = () => { const { theme } = useContext(ThemeContext); return (
); }; // App only renders the provider — it does not read context itself const App = () => ( ); ``` In the above example, we create a `ThemeContext` and a `ThemeProvider` component that holds the theme state. `ThemeProvider` wraps the tree so that any descendant can access the theme. `Layout` reads `theme` from context to apply the correct class to the top-level wrapper, and `Child` reads both `theme` and `toggleTheme` to render the toggle button — all without prop drilling. Pretty simple eh? ## Component composition While React Context is a useful tool for certain scenarios, it's not always the best solution for prop drilling. The more recommended approach is [component composition](https://eli.cx/blog/an-introduction-to-component-composition-by-example). This method involves creating distinct components for specific functionalities, thereby reducing the need to pass props across many layers.

Instead of consuming the context directly in the `Child`, we create a separate `ThemeToggle` component. In component composition, instead of embedding all logic within a single component or passing props deeply, we break down our UI into smaller, reusable components. Each component takes care of its own functionality, leading to a cleaner and more modular structure.

This approach not only simplifies the component structure but also enhances reusability and maintainability. Alongside component composition, state management libraries can be used selectively when necessary to further _streamline_ state handling in your React application. Now, shall we? ```js const ThemeToggle = ({ theme, setTheme }) => { const toggleTheme = () => { setTheme(theme === "light" ? "dark" : "light"); }; return ( ); }; const Grandparent = ({ children }) => (
{/*Grandparent specific code*/} {children}
); const Parent = ({ children }) => (
{/*Parent specific code*/} {children}
); // Use the Context in the Child component const Child = ({ children }) => { return (
{/*Child specific code*/} {children}
); }; const App = () => { const [theme, setTheme] = useState("light"); return (
); }; ``` See? Focus on the `ThemeToggle` component. It directly receives `theme` and `setTheme`, encapsulating the theme toggling functionality. This approach allows parent components (`Grandparent`, `Parent`, `Child`) to simply pass down their children, _streamlining_ the component structure. The `App` component, acting as the state holder for `theme`, directly provides the necessary props only to `ThemeToggle`. This setup exemplifies the power of composition in creating a cleaner, more maintainable React architecture, avoiding the pitfalls of prop drilling. ## Conclusion In wrapping up, the main idea in avoiding prop drilling is to smartly pass props where needed. With our `ThemeToggle` component, we show how to provide necessary props directly, bypassing the need to drill through several component levels. This method simplifies our React code, making it cleaner and easier to maintain. In essence, using component composition in React helps us build more modular and understandable components, leading to more efficient and streamlined development. Thanks for reading! 🥰 ## Reading list - [Alex Sidorenko's Prop drilling article](https://alexsidorenko.com/blog/react-prop-drilling-composition/) - [Passing data deeply with context](https://react.dev/learn/passing-data-deeply-with-context) - [Passing props to a component](https://react.dev/learn/passing-props-to-a-component) - [Marco Heine's Prop drilling article](https://marcoheine.com/blog/what-is-prop-drilling-and-how-to-avoid-it) - [Using Composition in React to Avoid "Prop Drilling"](https://www.youtube.com/watch?v=3XaXKiXtNjw) - [Composition vs. Inheritance](https://legacy.reactjs.org/docs/composition-vs-inheritance.html) --- # Eigenvectors > Exploring Spectral Graphs October 24, 2023 · 5 min read · https://yasint.dev/eigenvectors/ Tags: math, linear-algebra --- ## Notes to myself Eigenvectors are vectors that **only scale** (i.e., change in magnitude, not direction) when a given linear transformation (represented by a matrix) is applied to them. The scaling factor by which an eigenvector is multiplied when the transformation is applied is called an eigenvalue.

Given a square matrix $A$ any vector $v$ is considered an eigenvector of $A$ if $v$ is not the zero vector and there is some scalar $\lambda$ such that applying $A$ to $v$ results in a scalar multiple of $v$, i.e., the direction of $v$ remains unchanged. In equation form, this is written as: $A \cdot v = \lambda \cdot v$, where $"\cdot"$ denotes the multiplication operation (either matrix multiplication or scalar multiplication, depending on context).

$\lambda$ is the eigenvalue corresponding to the eigenvector $v$ in the above equation. It represents the scalar multiple by which the eigenvector is _stretched_ or _compressed_ (if you can't recall linear transformations you can refer [Khan Academy](https://www.khanacademy.org/math/linear-algebra)'s Matrix Transformations lecture for a refresher). To find the eigenvalues of a matrix $A$, we follow two steps. First we set up the characteristic equation, and then we solve for $\lambda$: - ☝️ **Characteristic Equation:** You set up the equation $det(A-\lambda \cdot I)=0$, where $det$ represents the determinant of a matrix, and $I$ is the identity matrix of the same size as $A$. This equation is derived from the eigenvector equation $A \cdot v= \lambda \cdot v$ and the fact that $v$ is non-zero. - ✌️ **Solve for $\lambda$**: Solving the characteristic equation will give you the eigenvalues $\lambda$ of the matrix $A$. Once the eigenvalues $\lambda$ are known, the eigenvectors can be found by: - **Substitution:** For each eigenvalue $λ$, you substitute $\lambda$ back into the equation $A \cdot v= \lambda \cdot v$ (which can be rewritten as $(A− \lambda \cdot I) \cdot v=0)$ and solve for $v$. - **Solving the System:** Typically, you'll get a system of linear equations for $v$, which you'll need to solve. Any non-zero vector that satisfies the system of equations is considered an eigenvector corresponding to the eigenvalue $\lambda$. --- Let's consider a `2x2` matrix $A = \begin{bmatrix} 4 & 1 \\ 2 & 3 \end{bmatrix}$ 1. Characteristic Equation: First, we find the determinant of $A - \lambda \cdot I$ {/*@formatter:off*/} $$ det(A - \lambda \cdot I) = det \begin{pmatrix} \begin{bmatrix} 4 & 1 \\ 2 & 3 \end{bmatrix} - \lambda \cdot \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} \end{pmatrix} \\~\\ = det \begin{pmatrix} \begin{bmatrix} 4-\lambda & 1 \\ 2 & 3-\lambda \end{bmatrix} \end{pmatrix} \\~\\ = (4-\lambda)(3-\lambda) - (2)(1)=\lambda^2 - 7\lambda + 10 $$ {/*@formatter:on*/} 2. Solving for $\lambda$: We solve $\lambda^2 - 7\lambda + 10$ to find the eigenvalues. The solution to this quadratic equation are the eigenvalues of $A$, which are $\lambda = 5$ and $\lambda = 2$. Now comes the real magic. We can find the eigenvectors by plugging each eigenvalue into the equation $(A− \lambda \cdot I) \cdot v=0$ and solving for $v$. For $\lambda = 5$: $$ \begin{bmatrix} -1 & 1 \\ 2 & -2 \end{bmatrix} \cdot \begin{bmatrix} v_1 \\ v_2 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \end{bmatrix} $$ The system simplifies to $-v_1 + v_2 = 0$, so one eigenvector _could_ be $v = [1, 1]$ for $\lambda = 5$. $$ \begin{bmatrix} 2 & 1 \\ 2 & 1 \end{bmatrix} \cdot \begin{bmatrix} v_1 \\ v_2 \end{bmatrix} = \begin{bmatrix} 0 \\ 0 \end{bmatrix} $$ Similarly, for $\lambda = 2$, the system simplifies to $2v_1 + v_2 = 0$, so one eigenvector _could_ be $v = [1, -2]$ for $\lambda = 2$ This process reveals the eigenvalues $\lambda=5$ and $\lambda=2$, with corresponding eigenvectors $[1,1]$ and $[1,−2]$, respectively. Each eigenvector is associated with one eigenvalue, and these vectors indicate the "directions" in which the linear transformation represented by matrix $A$ acts by stretching/compressing, without rotating. --- Using `numpy` to find the eigenvalues and eigenvectors. ```python caption="I used 'np.linalg.eig' here and this method is for general square matrices, and there are optimized versions like 'np.linalg.eigh' for symmetric or Hermitian matrices." A = np.array([[4, 1], [2, 3]]) ## The eigenvectors are normalized so their Euclidean norms are 1. eigenvalues, eigenvectors = np.linalg.eig(A) print("Matrix A:") print(A) print("\nEigenvalues:") print(eigenvalues) print("\nEigenvectors:") print(eigenvectors) ``` ```bash Output: Matrix A: [[4 1] [2 3]] Eigenvalues: [5. 2.] Eigenvectors: [[ 0.70710678 -0.4472136 ] [ 0.70710678 0.89442719]] ``` In this output, the eigenvalues are `5` and `2`, which match the mathematical solution I calculated. The eigenvectors in `numpy` are normalized (i.e., their "unit length" of 1 in Euclidean space), so they may look different from the one I calculate by hand, but they are indeed pointing in the same directions.

The first eigenvector is approximately $[0.707, 0.707]$, which points in the same direction as $[1,1]$, and the second eigenvector is approximately $[−0.447, 0.894]$, which points in the same direction as $[1,−2]$. The direction is the critical property of the eigenvector, not the magnitude.

We can verify this by normalizing the vector. It involves dividing each component of the vector by its length. For example, suppose the vector is $[1, -2]$. First, we calculate the magnitude ($m$) (Euclidean norm): $\small{m = \sqrt{(1)^2 + (-2)^2} = \sqrt{1+4} = \sqrt{5}}$ Then, divide each component of the original vector by this magnitude: $\text{normalized}\ \text{vector} = \begin{bmatrix} \frac{1}{\sqrt{5}},\frac{-2}{\sqrt{5}}\end{bmatrix}$ Or just use `numpy`. ```python ## Define the original vectors vectors = np.array([[1, 1], [1, -2]]) ## Function to normalize a vector def normalize_vector(vector): # Calculate its magnitude (Euclidean norm) magnitude = np.linalg.norm(vector) normalized_vector = vector / magnitude return normalized_vector ## Normalize the vectors and print the results for vector in vectors: normalized_vector = normalize_vector(vector) print(f"Original vector: {vector}") print(f"Normalized vector: {normalized_vector}\n") ``` Both approaches will give us the same normalized vector: ```bash Output: Original vector: [1 1] Normalized vector: [0.70710678 0.70710678] Original vector: [ 1 -2] Normalized vector: [ 0.4472136 -0.89442719] ``` Dassit 👋 ## Reading list - [Stanford Spectral Graph Theory](https://web.stanford.edu/class/cs168/l/l11.pdf) - [CMU: Spectral Graph Theory and its Applications](https://www.cs.cmu.edu/afs/cs/user/glmiller/public/Scientific-Computing/F-11/RelatedWork/Spielman/SpectTut.pdf) - [Yale Spectral Graph Theory](http://www.cs.yale.edu/homes/spielman/PAPERS/SGTChapter.pdf) --- # Java's fork/join framework > Java goes Forking Crazy! October 21, 2023 · 8 min read · https://yasint.dev/java-fork-join/ Tags: java, concurrency --- Hey there 👋! With multicore processors now standard, it's essential for high-performance applications to harness this power through effective concurrency. [Java's Fork/Join framework](https://docs.oracle.com/javase/tutorial/essential/concurrency/forkjoin.html), part of the `java.util.concurrent` package since JDK `7`, optimizes the efficiency of multi-threaded tasks, fully utilizing the capabilities of modern hardware. In this article, I'm going to delve into the **Fork/Join framework**, explaining its purpose, significance, and application, all illustrated with a practical example. ## What the fork? The Fork/Join framework is an implementation of the `ExecutorService` interface that helps developers solve problems using [divide-and-conquer algorithms](https://en.wikipedia.org/wiki/Divide-and-conquer_algorithm). These algorithms work by breaking down a task into smaller, more manageable pieces, solving each piece separately, and then combining the results. In this framework, any **task can be forked (split)** into smaller tasks, and the **results can be joined** subsequently, hence the name "Fork/Join." You may wonder, "Why opt for the Fork/Join Framework when traditional threading is an option?" ## Hmm, why? Picture multitasking. But it's Java juggling your tasks with _finesse_! Aaaaand it's all about efficiency, and here's why: - - **Efficient Thread Utilization:** It makes use of a [work-stealing algorithm](https://en.wikipedia.org/wiki/Work_stealing), where idle threads "steal" tasks from busy threads' queues. This ensures that all threads are actively used, reducing overhead and improving performance. - **Handling Recursive Tasks:** The framework excels at handling recursive computations, a common scenario in _divide-and-conquer_ algorithms. - **Improved Scalability:** It's designed to scale well to available parallelism, which means better performance on multicore processors. ## How does it work? Consider a scenario where you're faced with an array of **10 tasks**, each representing operations that are resource-intensive, such as database or I/O operations. Your objective is to expedite the processing of these tasks efficiently without overburdening the system resources. Sequential execution is off the table due to time constraints, and a single thread can handle a maximum of two tasks consecutively. Intriguing challenge, isn't it? Let's tackle this problem by employing a _divide-and-conquer_ strategy. ![Recursively dividing the task array until we match a certain threshold](./divide-and-conquer.png) As demonstrated, our goal is to systematically divide tasks until they're manageable enough, aligning with our defined threshold. Wondering how this translates into Java? Stay with me; it's simpler than it seems. First, let's establish our _base scenario_. Imagine a `Task` class, responsible for handling time and resource-intensive operations—think bulk updates, I/O, network calls, and more. Here's a glimpse of what it looks like: ```java name="Task.java" public class Task { private final int id; // Other necessary fields... public Task(int id) { this.id = id; } public void process() { // Note: exception handling omitted for brevity System.out.printf("Processing task %d...%n", id); Thread.sleep(TimeUnit.SECONDS.toMillis(3)); } } ``` However, we can enhance our approach by abstracting the `process()` method, leading us to define a `Computable` functional interface and implement it in our `Task` class, like so: ```java @FunctionalInterface public interface Computable { void process(); } public class Task implements Computable { // Existing code... } ``` Noice 😎! That's so much neater, isn't it? Next, we generate an array of random tasks, shifting our focus to the crux of the issue: **parallel execution**. ```java private Task[] generateTasks(int count) { Task[] tasks = new Task[count]; for (int i = 0; i < tasks.length; i++) { tasks[i] = new Task(i + 1); } return tasks; } ``` But how do we execute these tasks in parallel? Enter Java's Fork/Join framework. Here's a simplified guide: 1. Extend `RecursiveTask` or `RecursiveAction`, depending on whether you need a result. 2. Override the `compute()` method to specify the task's logic and the conditions for further splitting or direct execution. 3. Engage a `ForkJoinPool` to invoke the root task. Let's start with _step 1_, creating a class named `TaskProcessor` extending `RecursiveAction`, and override the `compute()` method: ```java name="TaskProcessor.java" public class TaskProcessor extends RecursiveAction { // We do our initialization here... @Override protected void compute() { // Task execution logic... } } ``` In _step 2_, we refine our `TaskProcessor` to accept an array of tasks and employ a loop within `compute()` to handle tasks sequentially. But there's a catch: we haven't set the base condition for task division. Here's where `start` and `end` come into play, marking the range of tasks processed by each `TaskProcessor` instance. This range helps us determine when to divide tasks further or process them directly, based on a `THRESHOLD`. ```java public class TaskProcessor extends RecursiveAction { private static final int THRESHOLD = 2; // This can vary private final Computable[] tasks; private final int start; private final int end; public TaskProcessor(Computable[] tasks, int start, int end) { this.tasks = tasks; this.start = start; this.end = end; } @Override protected void compute() { if (end - start <= THRESHOLD) { for (int i = start; i < end; i++) { tasks[i].process(); } } else { int middle = start + (end - start) / 2; TaskProcessor leftProcessor = new TaskProcessor(tasks, start, middle); TaskProcessor rightProcessor = new TaskProcessor(tasks, middle, end); ForkJoinTask.invokeAll(leftProcessor, rightProcessor); } } } ``` The `middle` calculation `(start + (end - start) / 2)` ensures a consistent split of tasks[^1], with `leftProcessor` and `rightProcessor` handling each segment. The call to `ForkJoinTask.invokeAll()` initiates parallel execution. After setting up our tasks and creating the `TaskProcessor` class, we need to instantiate a `ForkJoinPool` and start our tasks. Here's how we can do it: ```java public class ForkJoinTest { @Test public void forkJoin() { final Task[] tasks = generateTasks(10); TaskProcessor taskProcessor = new TaskProcessor(tasks, 0, tasks.length); ForkJoinPool pool = ForkJoinPool.commonPool(); pool.invoke(taskProcessor); pool.shutdown(); } private Task[] generateTasks(int count) { Task[] tasks = new Task[count]; for (int i = 0; i < tasks.length; i++) { tasks[i] = new Task(i + 1); } return tasks; } } ``` And just like that, it works! Super quick and almost like magic. Your tasks are done before you know it! So, it works _super-fast_, but how, huh? Let's take a simple look at the main code that makes this happen: - We first generate our tasks using the `generateTasks(10)` method, which returns an array of _ten_ Task objects. - We create an instance of `TaskProcessor`, passing the tasks along with the `start` and `end` indices. - We then instantiate the `ForkJoinPool`. We can either use `ForkJoinPool.commonPool()`, which reuses the common pool shared among all ForkJoinTasks, or create a new instance with a specific number of threads using `new ForkJoinPool(numberOfThreads)`. > **Pool Selection**? > > The common pool is generally recommended unless you have specific reasons for wanting to separate the tasks from the common pool (like different thread settings, priority, etc.). - Then, we initiate the task with `pool.invoke(taskProcessor)`. This starts the process, invoking the `compute()` method of `TaskProcessor`. The `invoke()` method is _synchronous_—it blocks until the task is complete. - Finally, it's a best practice to shut down the pool after all tasks are complete using `pool.shutdown()`, especially if you created a new instance of ForkJoinPool. Not shutting it down can lead to resource leaks. Moving from best practices to performance considerations, it's essential to evaluate the efficiency of our implementation. Because `TaskProcessor` is a `RecursiveAction` with no merge step — work happens only at the leaves — each of the $n$ tasks is processed exactly once, giving $O(n)$ total work. The recursion tree has depth $O(\log n)$ (the number of times we halve the array), which is the parallel span: the longest chain of sequential steps. With enough threads, the ideal wall-clock time is therefore $O(\log n)$, and the parallelism — total work divided by span — is $O(n / \log n)$. ## Conclusion The `ForkJoinPool` handles the heavy lifting of worker thread management, task synchronization, and other low-level details. The `invoke()` method of the pool executes the specified task and any subtasks that it may create, utilizing the available threads in the pool. The framework ensures **balanced distribution** of tasks among threads and efficient execution, leveraging the _divide-and-conquer_ principle we implemented in the `compute()` method of our `TaskProcessor`. I hope this guide made Java's Fork/Join framework easier for you! Thanks for sticking around 🥰. [^1]: **Consistent split of tasks:** The divisor for `middle` typically is `2`, dividing the task list in _half_, but it can be adjusted depending on how you want to split tasks. ```java int oneThird = start + (end - start) / 3; int twoThirds = start + 2 * (end - start) / 3; TaskProcessor firstThird = new TaskProcessor(tasks, start, oneThird); TaskProcessor secondThird = new TaskProcessor(tasks, oneThird, twoThirds); TaskProcessor finalThird = new TaskProcessor(tasks, twoThirds, end); invokeAll(firstThird, secondThird, finalThird); // Fork new subtasks ``` Above is a hypothetical example if you were to split the task into three subtasks instead of two. --- # TypeScript's omit and pick > How to Pick your TypeScript battles and when to Omit the drama! August 10, 2023 · 4 min read · https://yasint.dev/ts-omit-and-pick/ Tags: typescript, frontend --- Hey there! If you're like me, diving deep into TypeScript has been a roller coaster of discovery. Among the many gems I've stumbled upon, the [Omit](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys) and [Pick](https://www.typescriptlang.org/docs/handbook/utility-types.html#picktype-keys) utility types have been game-changers. Let me share a bit about my experiences with these two. ## The `Pick` interface What's the deal, huh? So, `Pick` is like your personal shopping assistant for types. You tell it which properties you want from an existing type, and voilà, you get a _brand-new_ type with just those properties. Given a type $T$ and a set of properties $P$, the Pick operation can be represented as $T \cap P$. ```ts type User = { id: number; name: string; email: string; }; type UserName = Pick; ``` In set theory terms, the `UserName` type is the intersection of the `User` type with a set that only contains the name property. Diving into Pick, there's some seriously good stuff to rave about. First off, its flexibility is unmatched—it's like crafting the perfect sandwich, picking only the ingredients I'm craving. And talk about clarity! It's a breeze to see which properties I'm juggling, making my code a lot more readable. But, it's not all rosy.

I'll admit, there are times I get a bit too enthusiastic and end up complicating things more than necessary. And, oh boy, the maintenance! As in every codebase's foundational types shift and change, you've got to be vigilant, ensuring that the types you've "picked" are still vibing well. It's like keeping tabs on a growing chain of dependencies, but hey, that's the coder's life for you!

## The `Omit` interface Think of `Omit` as the sibling of `Pick`. Instead of telling it what you want, you tell it what you don't want. It's like ordering a pizza and saying, **"Hold the olives 🤨."** Given a type $T$ and a set of properties $P$, the Omit operation can be represented as $T - P$. ```ts // User type from above type UserWithoutEmail = Omit; ``` The `UserWithoutEmail` type is the difference of the `User` type with a set that only contains the email property.

When I first started using `Omit`, I was blown away by its precision—it's _super handy_ when I want to leave out just a thing or two, especially from those chunky types. Plus, it's a lifesaver in ensuring I don't accidentally toss in stuff I'd rather keep out. But, it wasn't all sunshine and rainbows.

In the beginning, I'd often jumble up `Omit` and `Pick` since they're kinda like two sides of the same coin. And, just like with Pick, I've realized I've got to be on constant alert, especially when my main types undergo changes. It's been a learning curve, but totally worth it! Before we conclude, let's delve deeper into the intricacies of these utility types and their impact on our coding journey. ## Properties and implications ### Idempotence $\odot$ Applying Pick or Omit multiple times with the same parameters will yield the same result. For instance, `Pick, 'name'>` is the same as `Pick`. ### Commutativity $\oplus$ The order in which you pick properties doesn't matter. `Pick` is the same as `Pick`. This is because the intersection of two sets is _commutative_. ### Associativity $\otimes$ You can chain Pick or Omit operations, and the order of operations won't affect the result. For example, `Pick, 'name'>` is the same as `Omit, 'email'>`. ### Identity $\circ$ If you pick all properties of a type or omit none, you get the original type back. Similarly, if you omit all properties or pick none, you get an empty type. ## Conclusion Omit and Pick have been real MVPs in my TypeScript toolkit. They've given me so much control over my types, but I've also learned to use them wisely. If you're diving into TypeScript, give them a shot and see how they fit into your coding style. Happy coding! 🚀 Thanks for reading! 🥰 --- # JavaScript's new immutable array methods > Exploring toReversed(), toSorted(), toSpliced(), and with() from ECMAScript 2023. June 28, 2023 · 5 min read · https://yasint.dev/javascript-new-immutable-array-methods/ Tags: javascript, frontend --- In JavaScript, when it comes to manipulating the order of elements within an array, `reverse()` and `toReversed()` are two commonly used methods, each having its unique functionality and application. ## Old school `reverse()` The [`reverse()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse) method, a member of JavaScript's [`Array.prototype`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array), alters the order of the array's elements, essentially flipping them. This method performs the operation _in-place_, implying that it directly modifies the original array. Let's consider the example below: ```js let fruits = ['🍎', '🥭', '🍍']; console.log(fruits); // Output: ['🍎', '🥭', '🍍'] let reversedFruits = fruits.reverse(); console.log(reversedFruits); // Output: ['🍍', '🥭', '🍎'] // Note: reverse() mutates the original array. console.log(fruits); // Output: ['🍍', '🥭', '🍎'] ``` In the above code, after applying `reverse()`, the original fruits array is also reversed. This is because `reverse()` is a _destructive_ method and modifies the original array in-place. ## Introducing `toReversed()` 🥳 In contrast to reverse(), the `toReversed()` method **does not mutate** the original array. Instead, it creates a new array, mirroring the elements of the original array in reverse order. This method comes handy when preserving the initial order of elements is crucial. Let's consider a similar example: ```js let numbers = [10, 20, 30]; console.log(numbers); // Output: [10, 20, 30] let reversedNumbers = numbers.toReversed(); console.log(reversedNumbers); // Output: [30, 20, 10] // toReversed() is non-destructive -- the original array remains unchanged. console.log(numbers); // Output: [10, 20, 30] ``` In this example, the original numbers array remains unaffected after applying the `toReversed()` method, exhibiting its _non-destructive_ nature. ## Handling sparse arrays with `toReversed()` An additional feature of `toReversed()` is its behaviour with _sparse arrays_. In JavaScript, a sparse array is an array where **some elements are missing**. The `toReversed()` method treats empty slots in sparse arrays as if they hold the `undefined` value. ```js console.log([10, , 30].toReversed()); // Output: [30, undefined, 10] ``` Here, the empty slot in the array is considered as undefined when reversed. ## When did the `toReversed()` method become available? To enhance the adaptability and fluidity of array manipulation in JavaScript, the [ECMAScript 2023 specification](https://tc39.es/ecma262/2023/) brings about a significant proposal named [Change Array by Copy](https://tc39.es/proposal-change-array-by-copy/).

This proposal enriches the set of methods on `Array.prototype` by introducing a suite of functions that operate on the array and return a new copy, instead of manipulating the original array. The newcomers to this function suite include [`toReversed()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toReversed), [`toSorted()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted), [`toSpliced()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSpliced), and an additional method named [`with()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/with).

These methods are designed to mirror the behavior of existing methods [reverse()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse), [sort()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort), and [splice()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/splice), and the typical array element replacement operation performed using bracket notation. However, these new methods ensure that the original array remains unaltered. ## `toSorted()` The `toSorted()` method, just like `sort()`, arranges the array elements in a certain order. The crucial difference is that **it does not modify the original array**. ```js let jumbledNumbers = [30, 10, 20]; console.log(jumbledNumbers); // Output: [30, 10, 20] let orderedNumbers = jumbledNumbers.toSorted(); console.log(orderedNumbers); // Output: [10, 20, 30] // The original array remains unchanged console.log(jumbledNumbers); // Output: [30, 10, 20] ``` ## `toSpliced()` Similarly, `toSpliced()` acts as a non-destructive version of splice(). It returns a new array, incorporating the changes specified by the arguments, while the original array stays intact. ```js let oldArray = ['🍎', '🍌', '🥭']; console.log(oldArray); // Output: ['🍎', '🍌', '🥭'] // In here we just say, start from index 1 and delete 0 elements, and insert '🍊' let newArray = oldArray.toSpliced(1, 0, '🍊'); console.log(newArray); // Output: ['🍎', '🍊', '🍌', '🥭'] // The original array remains unchanged console.log(oldArray); // Output: ['🍎', '🍌', '🥭'] ``` ## `with()` Additionally, the `with()` method is an invaluable tool that replaces an element at a specified index with a given value and returns a new array, without touching the original array. ```js let initialFruits = ['🍎', '🍌', '🥭']; console.log(initialFruits); // Output: ['🍎', '🍌', '🥭'] let updatedFruits = initialFruits.with(1, '🍊'); console.log(updatedFruits); // Output: ["🍎", "🍊", "🥭"] // The original array remains unchanged console.log(initialFruits); // Output: ['🍎', '🍌', '🥭'] ``` ## The motivation behind these new methods Incorporating these methods in the language specification echoes the increasing emphasis on promoting functional programming styles in JavaScript, where data is usually treated as immutable. This development is a welcome addition for developers who frequently need to perform operations on arrays but wish to keep the original arrays unmodified. For me personally, these new features significantly simplify array manipulation, particularly in scenarios where avoiding side effects is critical. By preventing [accidental mutations](https://blog.sapegin.me/all/avoid-mutation/), these methods make the code more predictable and easier to reason about, which is a key factor in reducing bugs and enhancing code readability. ## Conclusion The ECMAScript 2023 **'Change Array by Copy'** proposal introduces four powerful methods that align JavaScript with modern functional programming practices: `toReversed()`, `toSorted()`, `toSpliced()`, and `with()`. These non-destructive alternatives keep your original arrays untouched, making code more predictable and safer, especially when working with frameworks like React that rely on immutability. Supported in modern browsers (Chrome 110+, Firefox 115+, Safari 16+), in my opinion, these methods should be your default choice for array manipulation. Use the traditional mutating methods only when you explicitly need in-place modification. Happy coding! 🚀 --- # Integrating JUnit 5 in Maven projects > A practical guide to setting up JUnit 5 testing in Maven projects with modern best practices. May 25, 2023 · 4 min read · https://yasint.dev/integrating-junit-in-maven-projects/ Tags: java, testing --- [JUnit](https://junit.org/junit5/) is a simple, open-source framework to write and run repeatable tests in [Java](https://www.java.com/en/). It's an essential tool for any serious developer who wants to implement unit testing into their development lifecycle. Here's how you can integrate JUnit 5 into your Maven project. ## Prerequisites Before proceeding, make sure you have: - Java JDK installed (Java 8 or later) - [Apache Maven](https://maven.apache.org/) installed (3.6.0+) - Basic understanding of Maven project structure ## Step 1: updating the `pom.xml` file The first step to adding JUnit to your Maven project is to update your project's [`pom.xml` file](https://maven.apache.org/guides/introduction/introduction-to-the-pom.html). This XML file describes the software project being built, its dependencies, and the build order. You can include JUnit in your project by adding it as a dependency. Here is how you do that: ```xml org.junit.jupiter junit-jupiter-api 5.9.3 test org.junit.jupiter junit-jupiter-engine 5.9.3 test ``` In the example above, I'm using JUnit version [`5.9.3`](https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api/5.9.3). You can find the latest version on [Maven Central](https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api). The `` tag is set to `test`, meaning this dependency is not required for normal use of the application, and is only available for the test _compilation_ and _execution_ phases. ### Ensuring Maven Surefire compatibility For Maven to properly execute JUnit 5 tests, you need the Maven Surefire plugin (version 2.22.0 or later): ```xml org.apache.maven.plugins maven-surefire-plugin 3.0.0 ``` ## Step 2: writing test cases Once the `pom.xml` file is updated, you can begin writing your test cases. By convention, test cases are placed in the `src/test/java` directory, mirroring the package structure of your main code. Here is an example of a simple test case: ```java name="ExampleTest.java" public class ExampleTest { @Test @DisplayName("Should add two numbers correctly") public void testAddition() { int a = 5; int b = 10; assertEquals(15, a + b, "5 + 10 should equal 15"); } } ``` In this test case, we're using the `@Test` annotation to denote a test method and `@DisplayName` to provide a readable test description. The `assertEquals()` method checks if two values are equal, and the optional third parameter provides a failure message. ### JUnit 5 common annotations JUnit 5 provides several useful annotations for organizing tests: ```java public class LifecycleTest { @BeforeAll static void setupOnce() { // Runs once before all tests } @BeforeEach void setupEach() { // Runs before each test } @Test void testExample() { // Your test logic } @AfterEach void teardownEach() { // Runs after each test } @AfterAll static void teardownOnce() { // Runs once after all tests } } ``` ## Step 3: running tests You can run the tests using the Maven command: ```bash mvn test ``` Maven will automatically discover and run any test cases in the `src/test/java` directory. You should see output similar to the following if the tests pass: ```txt ------------------------------------------------------- T E S T S ------------------------------------------------------- Running ExampleTest Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.028 sec Results : Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 [INFO] BUILD SUCCESS ``` The output indicates that one test was run and that there were no failures, errors, or skipped tests. ### Running specific tests You can run specific test classes or methods: ```bash # Run a specific test class mvn test -Dtest=ExampleTest # Run a specific test method mvn test -Dtest=ExampleTest#testAddition ``` ## Best practices When integrating JUnit into your Maven projects, consider these best practices: - **Match package structure**: Keep your test packages mirrored to your source packages for easy navigation - **Naming conventions**: Name test classes with the `Test` suffix (e.g., `CalculatorTest`) - **Descriptive test names**: Use `@DisplayName` for human-readable test descriptions - **Arrange-Act-Assert**: Structure tests with clear setup, execution, and verification phases - **One assertion per test**: Keep tests focused on a single behavior (when practical) - **Use parameterized tests**: For testing multiple inputs, leverage `@ParameterizedTest` ## Troubleshooting **Tests not running?** Ensure you have both `junit-jupiter-api` and `junit-jupiter-engine` dependencies, and verify your Maven Surefire plugin version is 2.22.0 or later. **Import errors?** Run `mvn clean install` to refresh dependencies. ## Conclusion Integrating JUnit 5 into your Maven project is straightforward and brings significant benefits to code quality and maintainability. With proper test coverage, you can refactor with confidence, catch bugs early, and maintain a robust codebase. ## Further reading - [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide) - Official comprehensive documentation - [Baeldung JUnit 5 Guide](https://www.baeldung.com/junit-5) - Practical tutorials and examples - [Maven Surefire Plugin](https://maven.apache.org/surefire/maven-surefire-plugin/) - Plugin documentation --- # My take on ChatGPT and prompt engineering > ChatGPT is powerful, but it's not magic. Here's how I actually use it. March 11, 2023 · 3 min read · https://yasint.dev/chatgpt-prompt-engineering/ Tags: ai, prompts --- I've been using ChatGPT for a while now, and I want to share my honest perspective on what it is, what it isn't, and how I think about prompt engineering. ## What ChatGPT actually is ChatGPT is a large language model trained on vast amounts of internet text data using the [Transformer architecture](https://arxiv.org/abs/1706.03762). It can generate human-like responses to natural language queries, making conversations feel surprisingly natural. But here's the thing: it's not generating new knowledge. It's reorganizing and presenting what already exists in its training data in a more digestible form. Each model version has a knowledge cutoff date - the point beyond which it hasn't seen new information. The specific cutoff varies depending on which model you're using and when it was trained. This means ChatGPT might not know about recent events, current trends, or information published after its training period. The model also sometimes generates false information or repeats itself. You need to know what accurate and inaccurate information looks like before trusting any generated content. ## How I approach prompting I've learned that how you ask matters. A lot. Instead of treating it like Google with simple queries, I build context through conversation. I might start broad, then narrow down, asking follow-up questions to refine the output. But I've also discovered that being specific and directive in a single prompt often works better. For example, when I need technical writing, I specify the tone, structure, and style I want. I tell it to use evidence, ask transitional questions, write in an academic context, or mimic a particular writing style. I can limit word counts, require specific keywords, or ask it to transition between contexts. The key is being deliberate about what you want. Prompt engineering is a skill that develops over time through experimentation. ## My honest assessment ChatGPT is a helpful research sidekick, not a replacement for critical thinking. I use it to: - Quickly understand unfamiliar topics - Generate initial drafts that I heavily edit - Explore different perspectives on a subject - Save time on routine writing tasks But I never blindly trust its output. I verify facts, check sources when possible, and apply my own judgment. The convenience is real, but so are the limitations. At the end of the day, nothing beats human critical thinking. ChatGPT is a tool, and like any tool, its value depends on how skillfully you use it. I focus on what I'm trying to learn and let the tool assist, not replace, my thinking process. ## Worth exploring If you want to dive deeper into prompt engineering, check out [this Twitter thread by Rob Lennon](https://twitter.com/thatroblennon/status/1610316022174683136). He shares clever techniques for creating prompts that generate more cohesive outputs. You can start experimenting at [chat.openai.com](https://chat.openai.com/chat) with the free tier. --- # Declarative events in ReactJS > Explore how to handle pathological events declaratively in ReactJS applications. March 9, 2023 · 1 min read · https://yasint.dev/declarative-events-in-reactjs/ Tags: react, javascript, frontend --- If you want to handle _pathological_ use cases inside React using JavaScript's `setInterval` function, use this library called [`@use-it/interval`](https://www.npmjs.com/package/@use-it/interval) developed by [Donavon](https://github.com/donavon). ```bash terminal npm i @use-it/interval ``` Suppose you want to run a function _periodically_ with a specified **interval**, but you need to control its behavior dynamically. Here's how you can do it: ```js function Component() { const [play, setPlay] = useState(false); useInterval(() => { // do something periodically }, play ? 1000 * INTERVAL : null); // rest of your code ... } ``` ![useInterval() example](./interval.gif "In this example, clicking the _play_ button periodically executes another procedure every 1 second to refresh this component in the background (blinks green). As soon as you click _pause_, it stops running that hook (idle state is gray).") Here's the [codesandbox demo](https://codesandbox.io/s/use-it-interval-g1gocz) for this. Similarly, if you want to attach or detach event-handling logic to an element or global scope (like `window` or `document`) inside a React Hook, you can use [`@use-it/event-listener`](https://www.npmjs.com/package/@use-it/event-listener). How convenient! ### Reading list - [Dan Abramov's article about `setInterval`](https://overreacted.io/making-setinterval-declarative-with-react-hooks/) - [Pathological (Mathematics)](https://en.wikipedia.org/wiki/Pathological_(mathematics)) - [Twitter thread by Donavon](https://twitter.com/donavon/status/1093612936621379584) --- # Positive Lookaheads > Oh, you're looking for something? Well, I'm looking for something too. March 7, 2023 · 1 min read · https://yasint.dev/positive-lookaheads/ Tags: regex, tools --- Find expression $$x$$ where expression $$y$$ follows:- Suppose you need to match all the first names ending with last name _Picasso_. You could write the following pattern to solve it. ```regexp hideLineNums ([a-zA-Z ]+)(?=Picasso) ``` ![Regex Breakdown](./regex-breakdown.png) ```txt hideLang Paloma Picasso // => true Maya Picasso // => true Steve Ross // => false ``` To experiment, navigate to the [Regex101 sandbox](https://regex101.com/r/wfl5Ad/1) I've created. This RegEx only works with Non-Deterministic Automata (NFA) regex engines. ### Reading list - [Regular Expressions with Lookahead (Martin et. al, 2021)](https://www.diva-portal.org/smash/get/diva2:1641657/FULLTEXT01.pdf) - [NFA vs DFA by Daniel Bazaco](https://www.abstractsyntaxseed.com/blog/regex-engine/nfa-vs-dfa) --- # Functors > Yet another design pattern from category theory in mathematics. March 6, 2023 · 1 min read · https://yasint.dev/functors/ Tags: functional-programming, math --- ### Note to self A Functor is a _design pattern_ that evolves from category theory in mathematics. Fundamentally it's a mapping between categories that preserves the structure of the original categories involved. It satisfies two laws: - 1. Identity law: $$\small{F(\operatorname{id}_A) = \operatorname{id}_{F(A)}}$$ 2. Composition law: $$\small{F(g \circ f) = F(g) \circ F(f)}$$ ### Reading list - [Category Theory](https://plato.stanford.edu/entries/category-theory/) - [Covariance and Contravariance](https://en.wikipedia.org/wiki/Covariance_and_contravariance_(computer_science)) - [Java Functors](https://www.baeldung.com/java-functors) --- # Fast forward videos with ffmpeg > Learn how to speed up your videos effortlessly using ffmpeg. January 18, 2023 · 9 min read · https://yasint.dev/fast-forward-videos-with-ffmpeg/ Tags: ffmpeg, tools --- The `ffmpeg` command line tool is a hyper-fast video and audio converter that can also grab from a live audio/video source. You can use it to convert arbitrary sample audio/video rates and resize/trim videos on the fly with a high-quality _polyphase_ filter. > **Installing ffmpeg** > > If you don't have `ffmpeg` installed in your system you can refer [how to install ffmpeg in your operating system](https://www.hostinger.com/tutorials/how-to-install-ffmpeg). Or if you're on macOS just do `brew install ffmpeg`[^1] Suppose you want to fast-forward this masterpiece by `2x`:-