Full Width [alt+shift+f] Shortcuts [alt+shift+k]
Sign Up [alt+shift+s] Log In [alt+shift+l]
6
My Biggest Mistake… So Far or: The Unlikely Cause of Feline Obesity The Benefits of Being The Right Amount of Selfish ¶My Biggest Mistake… So Far ¶or: The Unlikely Cause of Feline Obesity Check out this video from a couple of weeks ago: Selfless vs SelfishDid you notice the guy’s sign? “Take what you need” … how many really needed the money they took? Probably none of them. Contrast that to the apparently homeless guy at the end. Did he need it? Those are some selfish people right there. But that’s not what I want to focus on. I want to focus on the guy wearing the suit of dollar bills. Think of the suit as selflessness and the sign “Take what you need” as what others see when you exert your selflessness. OK in real life we don’t go around wearing our selflessness like a suit, holding a sign that says “use me.” But there are plenty of ways we can essentially be doing exactly that. Selflessness is also a finite resource and it needs to be replenished. If you allow it to all be used up...
over a year ago

Improve your reading experience

Logged in users get linked directly to articles resulting in a better reading experience. Please login for free, it takes less than 1 minute.

More from Sometimes It Works :: simonhamp.me

On AI doomerism

So what's going on? This is a reflection on this post (of the same title) that I thoroughly enjoyed, by Flavio Copes. "I am the genuine article, therefore I don't have to try. I just have to be. You, on the other hand, have to try any passing bandwagon, because what else have you got?" Life Isn't All Ha Ha Hee Hee It seems that a great many people are feeling the pinch of a tech job market squeeze that seems to have lasted for over 2 years now. A lot of folks seem to be quick to dump the blame for this squarely on "AI". And what can you expect when the tech CEOs are all doing their stupid dance, touting the latest fadtech as the breakthrough that justifies them laying off swathes of employees in short-sighted attempts to prop up shareholder value? A more self-destructive spiral I have not seen. But the truth is masked by this conclusion. I don't believe for one second—and I don't think a lot of other popcorn-crunching devs believe it either—that "AI" is remotely close to the claims being made by these flaccid excuses for billionaires. Something else is afoot. "First, there’s a lot of people, and I mean a LOT, that went into programming and didn’t have a genuine passion for the job. For them, it’s just a way to make money." Hard agree! I've seen some of these people. I don't believe it's a bad reason to get into any line of work, but in my experience, they have made for some of the worst developers. Not because of a lack of technical capability (I truly believe almost anyone can learn to program well), but the money motivator only takes you so far. Like any job worth doing, it is hard. The rewards come to those who continue to put in the effort. If you've based your decision to become a programmer on seeing someone else claiming success on social media, I can only tell you that you've been duped. The vast majority do not have this story. And even some of the ones that appear to, don't. "Very few jobs out there give those kind of perks: high pay and comfortable life." It's all relative. My personal experience with programming hasn't been "high pay... comfortable life". I've worked damn hard for a very long time. I still don't feel sufficiently compensated and I've just had my best year ever. And that as a freelancer! I've never seen programming (or really any possible career path) as "easy". Currently "AI" makes my life a bit easier, but nothing about using it in and out every day to build things convinces me that it's going to do away with my job. "To them, those people that care about the craft, AI is not a problem at all." I totally agree. ¶So what's going on? "post-COVID companies hired like crazy" "the end of the zero-interest rate period" "increased competition" from the potential of a globally shifted workforce "investor pressure" "AI" Look, there's not just one answer. That's clear. And these are just part of the picture. I have another theory: a huge factor at play here is that, for most of these businesses, their revenue is based on ads. Therefore, consumers (for the most part) are the driving force of a lot of the top tech companies' bottom lines. But the consumers are getting sick and tired of: Privacy violations. Crypto scams. Income disparity between the workers and the C-suite. Ads! Ads! Ads! everywhere, all of the time. The lies and manipulation of it all. Many of these tech companies haven't innovated anything meaningful in a long time. They're jumping on AI right now because they're all hoping they'll discover the next big breakthrough and unlock megabucks. And in the meantime they prop themselves up by touting eye-watering sales figures (and net losses) of their next "always-on, internet connected, touchscreen, AI-powered, super agent whosawidget." Or their "giant, face-hugging, reality-distortion spectacles that no person in their right mind would be caught dead wearing in public" But are any of these "advances" really moving the needle on solving the bigger problems we're facing as a species? No. So the ads and PR BS isn't working as well as it used. If your business model is to be a big promotional platform that relies on real human beings to view the content that your advertisers want to promote, then you have to bring real value. And if you're the advertiser whose whole business model depends on crazy ad spend to penetrate into and reach your target market of what you hope are real people, then you need a lot of cheap money to get there. It's a poisoned lake. These big fish have done it to themselves. Sure, they're big, so they'll survive for a while, but it's still terminal. The good news is that, in their wake will come true innovators. I expect the next few years to be something of the beginnings of a rebirth period in this constant boom-bust circus. And I genuinely believe that "AI" will enable a lot of that. It is already enabling small teams to move faster whilst staying lean. In time, these little fish will grow and once again find opportunity in hiring fresh talent. Get in with those companies now while they're still small by seeking them out and starting a conversation. It might not be a job right away, but it's building the relationship. "You have to have a network... Local meetups, and local conferences... Go a few days before... hang out before the conference... Go to the after parties. That’s where you actually meet and get to know people." This is the way. You know, I've never once applied to FAANG. I've never felt worthy. In most cases, I don't meet the requirements as I don't have a degree. And honestly, the companies and their leaders do not align with my values at all, so I'm extremely disinclined to apply. But imagine if someone like me, a "self-taught" nobody dev, with no recognisable education, only having a connection or two, skips the queue ahead of you and lands a job at the next great innovative tech company. All for the sake of saying hello and getting to know people. This is why, especially in the age of "AI", cultivating genuine relationships with real people is so important. Work on silly side projects (if you have the capacity). Read! Share what you're working on. Share what you enjoy. Retweet. Reply. Write about it—in the abstract if you have to. Make videos. Do a podcast. Go meet new people, in and out of your field. Don't do it for the algorithm, or the likes and subscribes. Do it for the genuine relationships. Because in a world of fakes and forgeries, the genuine article sticks out like a sore thumb!

3 months ago 12 votes
2024: Just the Start

2024 has overall been a great year for me personally. I believe it represents a turning point. 2023 was tough, but 2024 went some way to redressing the balance and I am even more positive about 2025 because of it. Here are a few highlights: We went to Japan! My first time in Asia. The best trip I've been on so far. I managed to get NativePHP support for Windows released, amongst many other improvements. It's almost ready for production usage. With over 1,000 devs in the Discord, $1,000/month in sponsorships and tons of contributors helping, it really feels like it's flying. I started building ReelFlow with an old acquaintance and his business partner, and it's got some very promising signs of growth. I went to Laracon US in Dallas and it was epic! Laradir became Laradevs, blew past 4,000 registered developer profiles and has some awesome features on the way. I was interviewed by Eric from Laravel News. I started four (yes, four(!)) podcasts. I finally fixed my website so I could start publishing to it again. And to top it all off, I got invited to speak at Laracon EU 2025, which is coming up in February. But it wasn't without its challenges! Up to about March/April, I had almost zero income. I was in a rut with NativePHP because I was so stressed about finding paid work. It was really frustrating to have all these goals and intentions for NativePHP (and of course a backlog of PRs and issues to get through!) but not really having the time to focus on it. I managed to pick some up when we returned from Japan, but it wasn't quite enough, and then all of a sudden I had too much. Committing to open source in your spare time is hard at the best of times, but especially so when you've got more than a full plate of full-time client work and you're trying grow a SaaS (Laradevs) on the side... I can tell you now, I have (and still continue) to make poor decisions about the best use of my time, for which I can only say that I'm very grateful to and for my long-suffering wife. Bu, I'm sorry that I've not spent enough time with you. Honestly, I feel like I've been teetering on the edge of burnout. So I decided that I needed to drop one client and that freed up my time. Thankfully, we managed to get away for a week later in the year. It was just to Tenerife, our neighbouring island, and we went up into the hills, so despite being the tail end of summer (when you'd expect great weather and warm temperatures), we were cold and damp in amongst the clouds. But it was just what I needed. We chilled out, cooked food, sampled the local shops, bars and restaurants, spent good time with great friends and just really relaxed. What's more, I felt like I truly disconnected for once (even though my wife might disagree!) So I'm determined to do more of that in 2025. Saying that, January is going to be tough! I've got a talk to finish so that I can go on stage in front of the biggest audience I've ever spoken in front of, to talk about something that I feel like I have almost zero knowledge about. A lot of the past couple of years - of basically being forced to go back into freelancing during the height of a hiring crisis - has felt like when I tried to start a business back in 2008/9, at the peak of the recession: hard work and I'm way out of my depth. Although many things have changed over those years, the one factor that's really stood out as being different is me. I can see clearly how I've grown in so many ways. I feel like I'm doing some of the best work of my career and I'm enjoying it a lot. The other thing I've learned is that I want to go all-in on NativePHP. In my opinion, the potential for this tech is huge. I'm not under any illusion that this is going to happen quickly. So one of my goals for 2025 is to build up one of those side projects enough to give me more time to spend on NativePHP, a stepping stone on my way to making it my full-time focus. It might be Laradevs, it might be something else. I don't know for sure yet, but I'm putting my chips on a few numbers. Besides that, I'm looking forward to visiting Amsterdam for the first time. I'm cautiously optimistic about my talk (though I'm starting to get very nervous) and I'm hoping to be able to attend Laracon US again. I've also got a couple more personal challenge goals. I'm 40 in 2025, so: I want to get my body into shape - I have a little excess weight to lose and while I'm probably the fittest I've been since the pandemic, I am still not fit enough. I want to build 40 apps with NativePHP! I want to meet (virtually or IRL) 1,000 Laravel developers I've not met before. 2024 has been a year of finding clarity and focus. 2025 will be the year of doubling down and building bigger. I hope you've had an opportunity to reflect and find some positives about 2024. I know it has been especially hard for many of you. I also hope that you can find a way to look into 2025 with enthusiasm and energy. I'd love to hear more about your ups and downs and what you're excited about for 2025, so please feel free to reach out to me

4 months ago 13 votes
Slow Tech is Good Tech

Avoid the shiny Avoid comparison Some examples Laravel Build tools Typescript and JS frameworks The Web Platform What I keep up with This morning, I watched a video advertising a course aimed at developers. One of the first sentiments proclaimed is that of feeling left behind, that tech is moving too fast to keep up. I think we're probably one of only a few industries globally that have this problem. I don't see baristas learning how to use every kind of coffee machine, or carpenters buying and using all the different types of wood saw. I don't see how they could. I'm sure those industries don't move anywhere near as fast as tech, so it may be a bit of an unfair comparison, but that has led me to a really interesting axiom: After 20 years in tech, I've learned that slow tech is good tech. I'm happier when I'm not trying to be on the bleeding edge. ¶Avoid the shiny I realised over time that I have been quite a lot more dismissive of "the new shiny" than a lot of other devs. In the back of my mind I was always kinda worried that I was being left behind because I wasn't learning X or getting experience with Y. But I've learned over the years that it truly doesn't matter. Why? Most importantly: users won't notice. Second: many companies don't have the capacity or desire to be on the bleeding edge. Finally: my brain still works, I can learn new things any time, when I really need to. And I learn faster now anyway. ¶Avoid comparison It's led me to believe that "falling behind" is a made-up concept designed to sell you stuff you probably don't need. 🌶️ Or maybe it's just human nature, similar to "keeping up with the Jones's". We're competitive, we compare ourselves to others very quickly. It's a bad habit though. I'm here to tell you that it's not just ok to fall (a little bit) behind, it can even be A Good Thing™. I've been falling behind for my entire career. 😂 I'm probably more behind now than I've ever been. But I'm also doing better now - in many ways - than I ever have. ¶Some examples ¶Laravel I didn't use Laravel until a couple years after its first release. When I picked up v4.2 it was using Composer and had started the transition to Symfony components. It had DI and all sorts of other goodies that earlier versions lacked. ¶Build tools I never used Bower/Gulp/Grunt/Yarn; I settled on NPM after the war was over. I haven't switched to Bun (and probably won't). I barely touched Webpack thanks to Laravel Mix. I use Vite, but also hardly use it directly, thanks to Laravel's first-party Vite plugin. I waited so long on all of this stuff that when I actually needed it, the choices were easy. Sure, I wasn't there at the front lines; I didn't invent React. I didn't build Vite. I didn't write the Laravel plugin. But then, I didn't need to, I wasn't building the frontend to the biggest online social network ever or a toolchain for other developers. ¶Typescript and JS frameworks I looked at Typescript once. You can just write plain JavaScript and get the same results. I slept on Coffeescript, Backbone, Angular, React, Vue, Svelte etc. Though I've used some of them briefly during my time, I never went deep on any of them. I stuck with jQuery for a long time because it worked and I knew how to build things rapidly with it. Importantly, I know the value of these other tools and when to use them. That hasn't stopped me being able to work on teams that use them heavily. But I've learned that it probably won't need to be me that's the person who's working day-in, day-out with them. And I'm ok with that. I use Alpine and Livewire now, as they were built to work with the tools I know and love using, each with a small footprint and easy to learn. They're more than enough for my needs—and many of my clients! And you know what? Some of my clients still use jQuery. 🤷‍♂️ ¶The Web Platform I love seeing the latest features in HTML, CSS & JS. I'll play with them, but very rarely deploy them. It usually takes some time before new features are available on ALL browsers and devices. And even then, billions of potential users are still running older versions. Yes, it still tickles my curiosity learning new things. It's intellectually satisfying. And, yes, proficiency in multiple tools can make you a more valuable asset. 💰 I'm not saying "you should not...", I'm just saying "you don't need to". ¶What I keep up with The only things that have been really important for me to stay up to date on are the core technologies I use: PHP & Laravel. I make it a point to keep my apps up to date with close to the latest releases - mainly for security and performance reasons, but also so I can use the latest and greatest features. 😛 But I rarely upgrade apps in production to the very latest versions as soon as they're available... I always leave it a few weeks. In that way, my work is not dictated by someone else's release schedule! That's not to say I don't keep abreast of what's coming in future releases of those tools; they're core to the service I provide and the tools I build, so I would be remiss not to. Being aware of what's coming is the priority there. But I don't have to have hands-on experience with everything. Testing early ("beta") releases against existing code is a useful exercise from time to time, but not always required. So basically I only need to regularly monitor two technologies. Easily done with a reasonably well-curated Twitter or a couple of RSS feeds. Sure, I keep my finger on the pulse of all the other tech I use, glancing occasionally out the corner of my eye and paying attention when the sources I am more focused on mention them But by keeping it simple, I can focus on delivering the most value rather than spinning my wheels on all of the superfluous things adjacent to the. Everything else can wait. Don't try to learn and keep up to date with everything; pick your battles!

11 months ago 11 votes
Simon Shares

I've started a little podcast! If you give it a listen, I'd love to know what you think.

a year ago 11 votes
Why You (Probably) Shouldn't Start With an SPA

Some history, some context When does an SPA make sense? So, why are SPAs bad again? Your front-end and back-end get decoupled! It may hurt customisability Performance will suffer So what's the alternative? I came across this interesting article by @gregnavis the other day. I guess it's from a few years ago now, judging by some of the other posts on his blog. But it still holds up. It's maybe even more relevant now. The article is entitled The Architecture No One Needs and it makes a simple and clear case, arguing that SPA's are more expensive than a standard multi-page app (which may or may not be a monolith). I'm going to use SPA throughout this post to mean the whole umbrella of ways you might be building a front-end application that is not server-side rendered. I think this is in line with Greg's intent too. I don't want to split hairs over whether a particular framework can be used to build front-end apps that aren't strictly SPAs. Since I read it, I haven't stopped thinking about this article. I found myself agreeing with all of Greg's stated points and it made me realise I actually have a strong opinion about this topic. I believe Greg is right, and as time rolls on I think I'm becoming more bullish in my stance on SPAs too. ¶Some history, some context My foray into the dirty, hubbub streets of front-end frameworks came about because of Laravel Nova and Statamic. They both use Vue, so I learned Vue. Of course, I looked around. But React made me retch. Angular almost made me want to buy a katana just to perform seppuku. (Of course I'm being hyperbolic.) I hear things about all of these and more thanks to Twitter. I can say some of it is good, but most of it continues to push me away. I stuck with Vue. I actually like Vue a lot, even if v3 has caused a bit of headache—it's actually better for it in my opinion and yes I do like the composition API. Overall though, I'm definitely heading more towards preferring not to have to build my front-end using node/deno/bun or whatever tool becomes popular today. That said, you just try and pry Tailwind from my frigid, rigid fingers! I'm quite firmly in the Livewire camp now. In another life, I may even like HTMX? I've built a few SPAs, but I can probably count them on one hand. But not only will I probably not build another one, I think you shouldn't either. I'm strongly encouraging my clients not to. I do think SPAs have a place, but it is almost certainly not in your project. Yes, I know this argument has been made before and probably far more eloquently than I'm going to, but you know I just felt like I needed to get this out of my head in my own way. ¶When does an SPA make sense? Let's get this out of the way first before I dunk on SPAs some more. They do have a place: I believe that the main advantage of an SPA is/was/has always been the decoupling of the front-end from the back-end. As time goes on and engineers specialise in areas that interest them the most, roles become more well-defined. This is why we have 'front-end' and 'back-end' and 'full stack' engineers. Although there is principally a lot of overlap (this is all programming at the end of the day and many concepts are similar), the domain—the environment that the engineer is most familiar with—is what determines their preference. Some engineers will prefer back-end because they don't want to think about or deal with a certain class of bugs or issues that arise from the rapidly-evolving world of front-end development. They may be uncomfortable using 3 or 4 different languages at the same time to get their work done, or they groan as soon as there's another major version of some framework which is going to require a load of refactoring that doesn't add immediate value to your product. Consider: every user of your app could be using a different version of a particular browser, which uses one kind of rendering engine, and a specific level of conformity to various web standards e.g. ECMAScript (the official standard that underpins what most of us think of as JavaScript) or CSS. Keeping on top of these variations and differences across desktop and mobile is enough to make anyone's head spin. And other engineers will prefer the front-end for its stateful nature, or because they're more comfortable with JavaScript/TypeScript, they grok CSS, and they love the intersection of code and design. Or maybe they just dislike dealing with databases, concurrency, queues, messaging and APIs a lot more than they dislike wild browser evolution. In any case, whatever size your team is, there will be these preferences. For example, I consider myself a full-stack developer, but honestly I like to avoid front-end work as much as possible, so will generally take the easy route there. Developing an SPA could allow you/your business to split the work of front-end and back-end into separate teams, which may help each team focus so they can do what they do best and be the happiest they can be. Which is the most important metric that will make an appreciable difference to your bottom-line long term. Being intentional about this will see you hit Conway's Law head-on and potentially tackle that beast in the best way, as long as you build up the necessary lines of communication. It also allows you to scale the two parts of the system separately—both in terms of team scaling and resource scaling—which, if you ever need that flexibility, could end up saving you from a certain group of headaches. And I'm gonna be honest, building distributed systems like this is a cool problem to solve and will be a point of growth for many of your engineers. ¶So, why are SPAs bad again? Yeh, so far this all sounds great, right? Well, just letting ol' Conway right into your living room isn't exactly the best idea. Aside of all the points that Greg makes in his article (if you haven't read it yet, go read it), I want to present three extra reasons why I think an SPA is a bad idea. ¶Your front-end and back-end get decoupled! Your back-end and front-end are always coupled. So trying to split them in anything but the most extreme circumstances is an exercise in futility. I think this is probably the worst part of this whole story. If your back-end team want to move in one direction, they've got to align with the front-end team. If timings and priorities don't work out, it's going to force someone to either put a hold on some work that really needs sorting out or do some grunt work just to patch over a hole that's about to appear. This is communication overhead. It adds risk. It adds complexity. It adds meetings into engineers' calendars. It adds friction, and stress, and distraction. It flies in the face of that number one principle: let your teams focus and be happy. This literally costs you money one way or another, cost that you could avoid. Deployments get unavoidably riskier in ways that are super difficult to test because testing distributed systems is really hard. Again, this might all be fine, in the most extreme cases, where you need the decoupling. Then this extra expense, and complexity, and churn-causing evil, is just a necessary evil that you have to learn to swallow and live with. But I've got x engineers, y1 are front-end, y2 are back-end. What do I do? I would strongly recommend that one of your engineering groups need to roll up their sleeves and get on learning the other group's code, tooling and responsibilities. This will have multiple benefits: career progression and learning opportunities, increased bus factor, fewer meetings and more collaboration. Sure there will be challenges too, but they won't be as big or as painful as the other challenges you'd face with an SPA. ¶It may hurt customisability As I mentioned earlier, the reason I got into Vue at all was because other tech I was using required me to. In both Statamic and Laravel Nova, the choice of Vue—well, not specifically Vue, but rather a reactive front-end framework—made sense because at the time it really was the best way to build flexible, reactive front-ends. And both of those tools needed that power and have become fantastic tools because of it. But there is one pain point that it's created that's quite hard to escape: the customisation story for each of these is harder because of it. How so? Basically, because each tool needs to build the assets to ship their product. And once they're built it's hard for third-parties to build on what's already there. How is it harder? Let's say I'm creating a Statamic add-on that allows CMS owners to post to social media from their control panel. As Statamic uses Vue and already has a bunch of components I can leverage, I am going to use some of them. But I'm also going to add some of my own functionality that doesn't already exist within Statamic. Now what happens? Well I build the Javascript... but wait. I can't change the bundled JS that's part of Statamic core. I have to build my own JS and load it at the right time, something I'm not in control of. Thankfully the Statamic team (building on tooling from the Vue & JS community) have worked hard to make this relatively easy, but my tool choice is now limited by what they support - if they're using Vue 2, I have to use Vue 2; if I don't like Vite or Webpack, tough luck. And on top of that, the builds have to happen at the client's end, which means we're now offloading some of the responsibility of making this whole thing work to people who don't need or want to know anything about this stuff. They just want to install your thing and get on with their jobs and lives. Why is this such a pain though? It used to be (in other platforms) that I could just load some extra JS file into the admin interface and do what I want. Honestly, we probably should never have been doing that either. Hands up, how many of you have seen a WordPress installation that tries to load 2 or more different versions of jQuery? 🙋🏼‍♂️ So these build tools go some way to alleviating some problems, but in the process have introduced so many layers of protection and abstraction that it presents a brain-melting, Japanese puzzle box to unlock. And all this JavaScript flying around is really unsafe, because JavaScript is completely malleable on the client side. That's meant library creators have had to go to some unusual lengths to protect the state of the application and encapsulate the code in attempts to make it safer and more portable. I won't pretend to understand all of the requirements, pre-requisites and implications for why built JavaScript assets are packaged up in a complex soup of function calls and obfuscated code, but suffice to say this makes building on top of pre-existing code that much harder. The web standards track is working to make this easier: we have Web Components and module starting to come through which should alleviate some of this. But if you're not building with those standard in play—I understand, it might not be possible because of browser support etc—and you want to expose your user's to third-party plugins/add-ons, then you've got to figure out how to make it easy for other developers. Some of that's going to require the specific implementation, the other part is going to be documentation. No matter how you cut it, it's going to be harder to get right than if you had server-side rendered views that you allow your third-party developers to load at runtime. ¶Performance will suffer This isn't really an extra reason as Greg did touch on this a little already, but I wanted to go harder on performance. You should never choose to build an SPA because of some supposed performance benefit. That is the wrong hill to try and defend for many reasons, but primarily because you've got the whole of the web stack—on horses, with bazookas—nipping at your heels. Sure, it may take a little while for web standards to get ratified and then rolled out, but the reality is that it's only a matter of time. We now have wide browser support for QUIC / HTTP/3 (which brings faster downloads and reduced server load) and things like 103 Early Hints response headers (which let us tell browsers what they should prioritise pre-loading), making the standard, non-SPA web even more performant. (And yes, some of this benefits SPAs too!) Sure, you can argue some of this advancement may have been driven by SPAs and their apparent benefits. But there's some inevitability to all of this (both the appearance of SPAs and the advancement of HTTP) which makes the whole argument moot in my opinion. As adoption and overall performance of the web platform increases, SPAs will even start to feel slow in comparison. Some feel slow already! That's because so much of the heavy lifting is left to the userland threads of the in-browser JavaScript engine instead of the lower-level compiled languages, the ones used to build the core browser engine itself (C++, Rust, Swift etc). That translates to a poorer experience in your app, and a penalty for your users. While it's not impossible for JavaScript to be as fast or faster than the actual browser it's running in, it's just such a long way for it to get there it's a no-brainer at this stage to let the browser do what the browser does best instead of trying to replicate all of that in JS. So don't! Use JavaScript the way it was intended: as a sugar-coating to enhance the pages, not to build entire pages. I mean, you wouldn't eat a donut made entirely out of sugar, would you? Again, in the extreme cases, maybe you would. Maybe for this donut-sized/-shaped sugar torus, you have an appropriately-sized dough-only counterpart, both of which you consume in close proximity... I donut-know where this analogy is going. ¶So what's the alternative? You, dear reader, are not in the most extreme case. Probably not even close! And you may never be. So, if you haven't started building an SPA, don't! Keep your code together in a single application where the front-end is rendered by the back-end, and then you can test and deploy it as a single unit. And yes, you can even do that without Docker 😱 Create it as a fully server-rendered, multi-page application with page reloads and everything. Go on! I double dare you. If you really want/need the reactivity, try something like Livewire, Hotwire or HTMX. You can do this all the way up to many millions of users per day and it will be fine, which you are a long way from. Trust me, your front-end will never meaningfully need to move faster than your back-end, and vice-versa. If you're already running an SPA and are contemplating bringing the two parts of the donut back together, do it! Bite your lip, close your eyes, and just do it. Relevant

a year ago 8 votes

More in programming

Notes from Alexander Petros’ “Building the Hundred-Year Web Service”

I loved this talk from Alexander Petros titled “Building the Hundred-Year Web Service”. What follows is summation of my note-taking from watching the talk on YouTube. Is what you’re building for future generations: Useful for them? Maintainable by them? Adaptable by them? Actually, forget about future generations. Is what you’re building for future you 6 months or 6 years from now aligning with those goals? While we’re building codebases which may not be useful, maintainable, or adaptable by someone two years from now, the Romans built a bridge thousands of years ago that is still being used today. It should be impossible to imagine building something in Roman times that’s still useful today. But if you look at [Trajan’s Bridge in Portugal, which is still used today] you can see there’s a little car on its and a couple pedestrians. They couldn’t have anticipated the automobile, but nevertheless it is being used for that today. That’s a conundrum. How do you build for something you can’t anticipate? You have to think resiliently. Ask yourself: What’s true today, that was true for a software engineer in 1991? One simple answer is: Sharing and accessing information with a uniform resource identifier. That was true 30+ years ago, I would venture to bet it will be true in another 30 years — and more! There [isn’t] a lot of source code that can run unmodified in software that is 30 years apart. And yet, the first web site ever made can do precisely that. The source code of the very first web page — which was written for a line mode browser — still runs today on a touchscreen smartphone, which is not a device that Tim Berners-less could have anticipated. Alexander goes on to point out how interaction with web pages has changed over time: In the original line mode browser, links couldn’t be represented as blue underlined text. They were represented more like footnotes on screen where you’d see something like this[1] and then this[2]. If you wanted to follow that link, there was no GUI to point and click. Instead, you would hit that number on your keyboard. In desktop browsers and GUI interfaces, we got blue underlines to represent something you could point and click on to follow a link On touchscreen devices, we got “tap” with your finger to follow a link. While these methods for interaction have changed over the years, the underlying medium remains unchanged: information via uniform resource identifiers. The core representation of a hypertext document is adaptable to things that were not at all anticipated in 1991. The durability guarantees of the web are absolutely astounding if you take a moment to think about it. In you’re sprinting you might beat the browser, but it’s running a marathon and you’ll never beat it in the long run. If your page is fast enough, [refreshes] won’t even repaint the page. The experience of refreshing a page, or clicking on a “hard link” is identical to the experience of partially updating the page. That is something that quietly happened in the last ten years with no fanfare. All the people who wrote basic HTML got a huge performance upgrade in their browser. And everybody who tried to beat the browser now has to reckon with all the JavaScript they wrote to emulate these basic features. Email · Mastodon · Bluesky

21 hours ago 2 votes
Modeling Awkward Social Situations with TLA+

You're walking down the street and need to pass someone going the opposite way. You take a step left, but they're thinking the same thing and take a step to their right, aka your left. You're still blocking each other. Then you take a step to the right, and they take a step to their left, and you're back to where you started. I've heard this called "walkwarding" Let's model this in TLA+. TLA+ is a formal methods tool for finding bugs in complex software designs, most often involving concurrency. Two people trying to get past each other just also happens to be a concurrent system. A gentler introduction to TLA+'s capabilities is here, an in-depth guide teaching the language is here. The spec ---- MODULE walkward ---- EXTENDS Integers VARIABLES pos vars == <<pos>> Double equals defines a new operator, single equals is an equality check. <<pos>> is a sequence, aka array. you == "you" me == "me" People == {you, me} MaxPlace == 4 left == 0 right == 1 I've gotten into the habit of assigning string "symbols" to operators so that the compiler complains if I misspelled something. left and right are numbers so we can shift position with right - pos. direction == [you |-> 1, me |-> -1] goal == [you |-> MaxPlace, me |-> 1] Init == \* left-right, forward-backward pos = [you |-> [lr |-> left, fb |-> 1], me |-> [lr |-> left, fb |-> MaxPlace]] direction, goal, and pos are "records", or hash tables with string keys. I can get my left-right position with pos.me.lr or pos["me"]["lr"] (or pos[me].lr, as me == "me"). Juke(person) == pos' = [pos EXCEPT ![person].lr = right - @] TLA+ breaks the world into a sequence of steps. In each step, pos is the value of pos in the current step and pos' is the value in the next step. The main outcome of this semantics is that we "assign" a new value to pos by declaring pos' equal to something. But the semantics also open up lots of cool tricks, like swapping two values with x' = y /\ y' = x. TLA+ is a little weird about updating functions. To set f[x] = 3, you gotta write f' = [f EXCEPT ![x] = 3]. To make things a little easier, the rhs of a function update can contain @ for the old value. ![me].lr = right - @ is the same as right - pos[me].lr, so it swaps left and right. ("Juke" comes from here) Move(person) == LET new_pos == [pos[person] EXCEPT !.fb = @ + direction[person]] IN /\ pos[person].fb # goal[person] /\ \A p \in People: pos[p] # new_pos /\ pos' = [pos EXCEPT ![person] = new_pos] The EXCEPT syntax can be used in regular definitions, too. This lets someone move one step in their goal direction unless they are at the goal or someone is already in that space. /\ means "and". Next == \E p \in People: \/ Move(p) \/ Juke(p) I really like how TLA+ represents concurrency: "In each step, there is a person who either moves or jukes." It can take a few uses to really wrap your head around but it can express extraordinarily complicated distributed systems. Spec == Init /\ [][Next]_vars Liveness == <>(pos[me].fb = goal[me]) ==== Spec is our specification: we start at Init and take a Next step every step. Liveness is the generic term for "something good is guaranteed to happen", see here for more. <> means "eventually", so Liveness means "eventually my forward-backward position will be my goal". I could extend it to "both of us eventually reach out goal" but I think this is good enough for a demo. Checking the spec Four years ago, everybody in TLA+ used the toolbox. Now the community has collectively shifted over to using the VSCode extension.1 VSCode requires we write a configuration file, which I will call walkward.cfg. SPECIFICATION Spec PROPERTY Liveness I then check the model with the VSCode command TLA+: Check model with TLC. Unsurprisingly, it finds an error: The reason it fails is "stuttering": I can get one step away from my goal and then just stop moving forever. We say the spec is unfair: it does not guarantee that if progress is always possible, progress will be made. If I want the spec to always make progress, I have to make some of the steps weakly fair. + Fairness == WF_vars(Next) - Spec == Init /\ [][Next]_vars + Spec == Init /\ [][Next]_vars /\ Fairness Now the spec is weakly fair, so someone will always do something. New error: \* First six steps cut 7: <Move("me")> pos = [you |-> [lr |-> 0, fb |-> 4], me |-> [lr |-> 1, fb |-> 2]] 8: <Juke("me")> pos = [you |-> [lr |-> 0, fb |-> 4], me |-> [lr |-> 0, fb |-> 2]] 9: <Juke("me")> (back to state 7) In this failure, I've successfully gotten past you, and then spend the rest of my life endlessly juking back and forth. The Next step keeps happening, so weak fairness is satisfied. What I actually want is for both my Move and my Juke to both be weakly fair independently of each other. - Fairness == WF_vars(Next) + Fairness == WF_vars(Move(me)) /\ WF_vars(Juke(me)) If my liveness property also specified that you reached your goal, I could instead write \A p \in People: WF_vars(Move(p)) etc. I could also swap the \A with a \E to mean at least one of us is guaranteed to have fair actions, but not necessarily both of us. New error: 3: <Move("me")> pos = [you |-> [lr |-> 0, fb |-> 2], me |-> [lr |-> 0, fb |-> 3]] 4: <Juke("you")> pos = [you |-> [lr |-> 1, fb |-> 2], me |-> [lr |-> 0, fb |-> 3]] 5: <Juke("me")> pos = [you |-> [lr |-> 1, fb |-> 2], me |-> [lr |-> 1, fb |-> 3]] 6: <Juke("me")> pos = [you |-> [lr |-> 1, fb |-> 2], me |-> [lr |-> 0, fb |-> 3]] 7: <Juke("you")> (back to state 3) Now we're getting somewhere! This is the original walkwarding situation we wanted to capture. We're in each others way, then you juke, but before either of us can move you juke, then we both juke back. We can repeat this forever, trapped in a social hell. Wait, but doesn't WF(Move(me)) guarantee I will eventually move? Yes, but only if a move is permanently available. In this case, it's not permanently available, because every couple of steps it's made temporarily unavailable. How do I fix this? I can't add a rule saying that we only juke if we're blocked, because the whole point of walkwarding is that we're not coordinated. In the real world, walkwarding can go on for agonizing seconds. What I can do instead is say that Liveness holds as long as Move is strongly fair. Unlike weak fairness, strong fairness guarantees something happens if it keeps becoming possible, even with interruptions. Liveness == + SF_vars(Move(me)) => <>(pos[me].fb = goal[me]) This makes the spec pass. Even if we weave back and forth for five minutes, as long as we eventually pass each other, I will reach my goal. Note we could also by making Move in Fairness strongly fair, which is preferable if we have a lot of different liveness properties to check. A small exercise for the reader There is a presumed invariant that is violated. Identify what it is, write it as a property in TLA+, and show the spec violates it. Then fix it. Answer (in rot13): Gur vainevnag vf "ab gjb crbcyr ner va gur rknpg fnzr ybpngvba". Zbir thnenagrrf guvf ohg Whxr qbrf abg. More TLA+ Exercises I've started work on an exercises repo. There's only a handful of specific problems now but I'm planning on adding more over the summer. learntla is still on the toolbox, but I'm hoping to get it all moved over this summer. ↩

yesterday 2 votes
the penultimate conditional syntax

About half a year ago I encountered a paper bombastically titled “the ultimate conditional syntax”. It has the attractive goal of unifying pattern match with boolean if tests, and its solution is in some ways very nice. But it seems over-complicated to me, especially for something that’s a basic work-horse of programming. I couldn’t immediately see how to cut it down to manageable proportions, but recently I had an idea. I’ll outline it under the “penultimate conditionals” heading below, after reviewing the UCS and explaining my motivation. what the UCS? whence UCS out of scope penultimate conditionals dangling syntax examples antepenultimate breath what the UCS? The ultimate conditional syntax does several things which are somewhat intertwined and support each other. An “expression is pattern” operator allows you to do pattern matching inside boolean expressions. Like “match” but unlike most other expressions, “is” binds variables whose scope is the rest of the boolean expression that might be evaluated when the “is” is true, and the consequent “then” clause. You can “split” tests to avoid repeating parts that are the same in successive branches. For example, if num < 0 then -1 else if num > 0 then +1 else 0 can be written if num < 0 then -1 > 0 then +1 else 0 The example shows a split before an operator, where the left hand operand is the same and the rest of the expression varies. You can split after the operator when the operator is the same, which is common for “is” pattern match clauses. Indentation-based syntax (an offside rule) reduces the amount of punctuation that splits would otherwise need. An explicit version of the example above is if { x { { < { 0 then −1 } }; { > { 0 then +1 } }; else 0 } } (This example is written in the paper on one line. I’ve split it for narrow screens, which exposes what I think is a mistake in the nesting.) You can also intersperse let bindings between splits. I doubt the value of this feature, since “is” can also bind values, but interspersed let does have its uses. The paper has an example using let to avoid rightward drift: if let tp1_n = normalize(tp1) tp1_n is Bot then Bot let tp2_n = normalize(tp2) tp2_n is Bot then Bot let m = merge(tp1_n, tp2_n) m is Some(tp) then tp m is None then glb(tp1_n, tp2_n) It’s probably better to use early return to avoid rightward drift. The desugaring uses let bindings when lowering the UCS to simpler constructions. whence UCS Pattern matching in the tradition of functional programming languages supports nested patterns that are compiled in a way that eliminates redundant tests. For example, this example checks that e1 is Some(_) once, not twice as written. if e1 is Some(Left(lv)) then e2 Some(Right(rv)) then e3 None then e4 Being cheeky, I’d say UCS introduces more causes of redundant checks, then goes to great effort to to eliminate redundant checks again. Splits reduce redundant code at the source level; the bulk of the paper is about eliminating redundant checks in the lowering from source to core language. I think the primary cause of this extra complexity is treating the is operator as a two-way test rather than a multi-way match. Splits are introduced as a more general (more complicated) way to build multi-way conditions out of two-way tests. There’s a secondary cause: the tradition of expression-oriented functional languages doesn’t like early returns. A nice pattern in imperative code is to write a function as a series of preliminary calculations and guards with early returns that set things up for the main work of the function. Rust’s ? operator and let-else statement support this pattern directly. UCS addresses the same pattern by wedging calculate-check sequences into if statements, as in the normalize example above. out of scope I suspect UCS’s indentation-based syntax will make programmers more likely to make mistakes, and make compilers have more trouble producing nice error messages. (YAML has put me off syntax that doesn’t have enough redundancy to support good error recovery.) So I wondered if there’s a way to have something like an “is pattern” operator in a Rust-like language, without an offside rule, and without the excess of punctuation in the UCS desugaring. But I couldn’t work out how to make the scope of variable bindings in patterns cover all the code that might need to use them. The scope needs to extend into the consequent then clause, but also into any follow-up tests – and those tests can branch so the scope might need to reach into multiple then clauses. The problem was the way I was still thinking of the then and else clauses as part of the outer if. That implied the expression has to be closed off before the then, which troublesomely closes off the scope of any is-bound variables. The solution – part of it, at least – is actually in the paper, where then and else are nested inside the conditional expression. penultimate conditionals There are two ingredients: The then and else clauses become operators that cause early return from a conditional expression. They can be lowered to a vaguely Rust syntax with the following desugaring rules. The 'if label denotes the closest-enclosing if; you can’t use then or else inside the expr of a then or else unless there’s another intervening if. then expr ⟼ && break 'if expr else expr ⟼ || break 'if expr else expr ⟼ || _ && break 'if expr There are two desugarings for else depending on whether it appears in an expression or a pattern. If you prefer a less wordy syntax, you might spell then as => (like match in Rust) and else as || =>. (For symmetry we might allow && => for then as well.) An is operator for multi-way pattern-matching that binds variables whose scope covers the consequent part of the expression. The basic form is like the UCS, scrutinee is pattern which matches the scrutinee against the pattern returning a boolean result. For example, foo is None Guarded patterns are like, scrutinee is pattern && consequent where the scope of the variables bound by the pattern covers the consequent. The consequent might be a simple boolean guard, for example, foo is Some(n) && n < 0 or inside an if expression it might end with a then clause, if foo is Some(n) && n < 0 => -1 // ... Simple multi-way patterns are like, scrutinee is { pattern || pattern || … } If there is a consequent then the patterns must all bind the same set of variables (if any) with the same types. More typically, a multi-way match will have consequent clauses, like scrutinee is { pattern && consequent || pattern && consequent || => otherwise } When a consequent is false, we go on to try other alternatives of the match, like we would when the first operand of boolean || is false. To help with layout, you can include a redundant || before the first alternative. For example, if foo is { || Some(n) && n < 0 => -1 || Some(n) && n > 0 => +1 || Some(n) => 0 || None => 0 } Alternatively, if foo is { Some(n) && ( n < 0 => -1 || n > 0 => +1 || => 0 ) || None => 0 } (They should compile the same way.) The evaluation model is like familiar shortcutting && and || and the syntax is supposed to reinforce that intuition. The UCS paper spends a lot of time discussing backtracking and how to eliminate it, but penultimate conditionals evaluate straightforwardly from left to right. The paper briefly mentions as patterns, like Some(Pair(x, y) as p) which in Rust would be written Some(p @ Pair(x, y)) The is operator doesn’t need a separate syntax for this feature: Some(p is Pair(x, y)) For large examples, the penultimate conditional syntax is about as noisy as Rust’s match, but it scales down nicely to smaller matches. However, there are differences in how consequences and alternatives are punctuated which need a bit more discussion. dangling syntax The precedence and associativity of the is operator is tricky: it has two kinds of dangling-else problem. The first kind occurs with a surrounding boolean expression. For example, when b = false, what is the value of this? b is true || false It could bracket to the left, yielding false: (b is true) || false Or to the right, yielding true: b is { true || false } This could be disambiguated by using different spellings for boolean or and pattern alternatives. But that doesn’t help for the second kind which occurs with an inner match. foo is Some(_) && bar is Some(_) || None Does that check foo is Some(_) with an always-true look at bar ( foo is Some(_) ) && bar is { Some(_) || None } Or does it check bar is Some(_) and waste time with foo? foo is { Some(_) && ( bar is Some(_) ) || None } I have chosen to resolve the ambiguity by requiring curly braces {} around groups of alternative patterns. This allows me to use the same spelling || for all kinds of alternation. (Compare Rust, which uses || for boolean expressions, | in a pattern, and , between the arms of a match.) Curlies around multi-way matches can be nested, so the example in the previous section can also be written, if foo is { || Some(n) && n < 0 => -1 || Some(n) && n > 0 => +1 || { Some(0) || None } => 0 } The is operator binds tigher than && on its left, but looser than && on its right (so that a chain of && is gathered into a consequent) and tigher than || on its right so that outer || alternatives don’t need extra brackets. examples I’m going to finish these notes by going through the ultimate conditional syntax paper to translate most of its examples into the penultimate syntax, to give it some exercise. Here we use is to name a value n, as a replacement for the |> abs pipe operator, and we use range patterns instead of split relational operators: if foo(args) is { || 0 => "null" || n && abs(n) is { || 101.. => "large" || ..10 => "small" || => "medium" ) } In both the previous example and the next one, we have some extra brackets where UCS relies purely on an offside rule. if x is { || Right(None) => defaultValue || Right(Some(cached)) => f(cached) || Left(input) && compute(input) is { || None => defaultValue || Some(result) => f(result) } } This one is almost identical to UCS apart from the spellings of and, then, else. if name.startsWith("_") && name.tailOption is Some(namePostfix) && namePostfix.toIntOption is Some(index) && 0 <= index && index < arity && => Right([index, name]) || => Left("invalid identifier: " + name) Here are some nested multi-way matches with overlapping patterns and bound values: if e is { // ... || Lit(value) && Map.find_opt(value) is Some(result) => Some(result) // ... || { Lit(value) || Add(Lit(0), value) || Add(value, Lit(0)) } => { print_int(value); Some(value) } // ... } The next few examples show UCS splits without the is operator. In my syntax I need to press a few more buttons but I think that’s OK. if x == 0 => "zero" || x == 1 => "unit" || => "?" if x == 0 => "null" || x > 0 => "positive" || => "negative" if predicate(0, 1) => "A" || predicate(2, 3) => "B" || => "C" The first two can be written with is instead, but it’s not briefer: if x is { || 0 => "zero" || 1 => "unit" || => "?" } if x is { || 0 => "null" || 1.. => "positive" || => "negative" } There’s little need for a split-anything feature when we have multi-way matches. if foo(u, v, w) is { || Some(x) && x is { || Left(_) => "left-defined" || Right(_) => "right-defined" } || None => "undefined" } A more complete function: fn zip_with(f, xs, ys) { if [xs, ys] is { || [x :: xs, y :: ys] && zip_with(f, xs, ys) is Some(tail) => Some(f(x, y) :: tail) || [Nil, Nil] => Some(Nil) || => None } } Another fragment of the expression evaluator: if e is { // ... || Var(name) && Map.find_opt(env, name) is { || Some(Right(value)) => Some(value) || Some(Left(thunk)) => Some(thunk()) } || App(lhs, rhs) => // ... // ... } This expression is used in the paper to show how a UCS split is desugared: if Pair(x, y) is { || Pair(Some(xv), Some(yv)) => xv + yv || Pair(Some(xv), None) => xv || Pair(None, Some(yv)) => yv || Pair(None, None) => 0 } The desugaring in the paper introduces a lot of redundant tests. I would desugar straightforwardly, then rely on later optimizations to eliminate other redundancies such as the construction and immediate destruction of the pair: if Pair(x, y) is Pair(xx, yy) && xx is { || Some(xv) && yy is { || Some(yv) => xv + yv || None => xv } || None && yy is { || Some(yv) => yv || None => 0 } } Skipping ahead to the “non-trivial example” in the paper’s fig. 11: if e is { || Var(x) && context.get(x) is { || Some(IntVal(v)) => Left(v) || Some(BoolVal(v)) => Right(v) } || Lit(IntVal(v)) => Left(v) || Lit(BoolVal(v)) => Right(v) // ... } The next example in the paper compares C# relational patterns. Rust’s range patterns do a similar job, with the caveat that Rust’s ranges don’t have a syntax for exclusive lower bounds. fn classify(value) { if value is { || .. -4.0 => "too low" || 10.0 .. => "too high" || NaN => "unknown" || => "acceptable" } } I tend to think relational patterns are the better syntax than ranges. With relational patterns I can rewrite an earlier example like, if foo is { || Some(< 0) => -1 || Some(> 0) => +1 || { Some(0) || None } => 0 } I think with the UCS I would have to name the Some(_) value to be able to compare it, which suggests that relational patterns can be better than UCS split relational operators. Prefix-unary relational operators are also a nice way to write single-ended ranges in expressions. We could simply write both ends to get a complete range, like >= lo < hi or like if value is > -4.0 < 10.0 => "acceptable" || => "far out" Near the start I quoted a normalize example that illustrates left-aligned UCS expression. The penultimate version drifts right like the Scala version: if normalize(tp1) is { || Bot => Bot || tp1_n && normalize(tp2) is { || Bot => Bot || tp2_n && merge(tp1_n, tp2_n) is { || Some(tp) => tp || None => glb(tp1_n, tp2_n) } } } But a more Rusty style shows the benefits of early returns (especially the terse ? operator) and monadic combinators. let tp1 = normalize(tp1)?; let tp2 = normalize(tp2)?; merge(tp1, tp2) .unwrap_or_else(|| glb(tp1, tp2)) antepenultimate breath When I started writing these notes, my penultimate conditional syntax was little more than a sketch of an idea. Having gone through the previous section’s exercise, I think it has turned out better than I thought it might. The extra nesting from multi-way match braces doesn’t seem to be unbearably heavyweight. However, none of the examples have bulky then or else blocks which are where the extra nesting is more likely to be annoying. But then, as I said before it’s comparable to a Rust match: match scrutinee { pattern => { consequent } } if scrutinee is { || pattern => { consequent } } The || lines down the left margin are noisy, but hard to get rid of in the context of a curly-brace language. I can’t reduce them to | like OCaml because what would I use for bitwise OR? I don’t want presence or absence of flow control to depend on types or context. I kind of like Prolog / Erlang , for && and ; for ||, but that’s well outside what’s legible to mainstream programmers. So, dunno. Anyway, I think I’ve successfully found a syntax that does most of what UCS does, but much in a much simpler fashion.

2 days ago 5 votes
Coding should be a vibe!

The appeal of "vibe coding" — where programmers lean back and prompt their way through an entire project with AI — appears partly to be based on the fact that so many development environments are deeply unpleasant to work with. So it's no wonder that all these programmers stuck working with cumbersome languages and frameworks can't wait to give up on the coding part of software development. If I found writing code a chore, I'd be looking for retirement too. But I don't. I mean, I used to! When I started programming, it was purely because I wanted programs. Learning to code was a necessary but inconvenient step toward bringing systems to life. That all changed when I learned Ruby and built Rails. Ruby's entire premise is "programmer happiness": that writing code should be a joy. And historically, the language was willing to trade run-time performance, memory usage, and other machine sympathies against the pursuit of said programmer happiness. These days, it seems like you can eat your cake and have it too, though. Ruby, after thirty years of constant improvement, is now incredibly fast and efficient, yet remains a delight to work with. That ethos couldn't shine brighter now. Disgruntled programmers have finally realized that an escape from nasty syntax, boilerplate galore, and ecosystem hyper-churn is possible. That's the appeal of AI: having it hide away all that unpleasantness. Only it's like cleaning your room by stuffing the mess under the bed — it doesn't make it go away! But the instinct is correct: Programming should be a vibe! It should be fun! It should resemble English closely enough that line noise doesn't obscure the underlying ideas and decisions. It should allow a richness of expression that serves the human reader instead of favoring the strictness preferred by the computer. Ruby does. And given that, I have no interest in giving up writing code. That's not the unpleasant part that I want AI to take off my hands. Just so I can — what? — become a project manager for a murder of AI crows? I've had the option to retreat up the manager ladder for most of my career, but I've steadily refused, because I really like writing Ruby! It's the most enjoyable part of the job! That doesn't mean AI doesn't have a role to play when writing Ruby. I'm conversing and collaborating with LLMs all day long — looking up APIs, clarifying concepts, and asking stupid questions. AI is a superb pair programmer, but I'd retire before permanently handing it the keyboard to drive the code. Maybe one day, wanting to write code will be a quaint concept. Like tending to horses for transportation in the modern world — done as a hobby but devoid of any economic value. I don't think anyone knows just how far we can push the intelligence and creativity of these insatiable token munchers. And I wouldn't bet against their advance, but it's clear to me that a big part of their appeal to programmers is the wisdom that Ruby was founded on: Programming should favor and flatter the human.

2 days ago 8 votes
Tempest Rising is a great game

I really like RTS games. I pretty much grew up on them, starting with Command&Conquer 3: Kane’s Wrath, moving on to StarCraft 2 trilogy and witnessing the downfall of Command&Conquer 4. I never had the disks for any other RTS games during my teenage years. Yes, the disks, the ones you go to the store to buy! I didn’t know Steam existed back then, so this was my only source of games. There is something magical in owning a physical copy of the game. I always liked the art on the front (a mandatory huge face for all RTS!), game description and screenshots on the back, even the smell of the plastic disk case.

2 days ago 4 votes