I feel like MS actually learned their lesson with synchronously integrating the language intelligence into the IDE. Old versions of VS would hang or crash based on bugs in the language tooling trying to provide intellisense. You'd restart and it'd work fine till you hit some other weird edge case. Generally this settled to a level of rare-bugginess where you were happy enough with the advantages not to go back to Emacs/VIM, but still annoyed at the occasional restart needed.
In no way does this mean LSP is a perfect solution, but anything synchronous would be a step backwards.
I'm very happy with the performance and ability of AI. I think the best software ever will come out of this. It's very clear to me that AI empowers one to make "just ok" software effortlessly, and "incredibly excellent" software with the same level of effort we previously used to make "just ok" software. So there will be a lot of slop, but there will also be some real diamonds created by those who put the extra work in. The extra work might not involve manually writing or even reading much code, but it is work nonetheless.
I still count myself as "dejected" overall. I'm dejected by the economic shape of AI which seems destined to keep concentrating outsized rewards to the top few who own it or have enough capital to shackle it to their will. I'm fearful that intelligence, once it becomes a commodity you can rent any time you may need it, will be socially devalued in our already anti-intellectual society. I worry that in a couple decades we may resemble the world of "The Machine Stops" (1909), where nobody really knows how things work.
Of course we already live in such a complex world, no one person has complete knowledge of the "stack" they rely on. I can write software in C or assembler but I can't design a CPU and don't really understand how one is manufactured. (I may someday study that field, but I haven't yet!) But I take solace in the fact that somebody out there, has put the time in to study each of those things and is an expert in them. At every layer and in every niche of our engineering world there are masters of their craft, and there are students learning the fundamentals to keep that knowledge alive and growing. What if in 30 or 40 years that is not the case, and there are whole corners of knowledge the modern world depends on where everybody is reduced to "I dunno how it works, but Claude said..."?
Map and filter usually have only one arg and if they have 2, the 2nd is almost always a 0-based index. They look identical in most languages, even when Microsoft chooses to call them Select and Where.
Reduce has an accumulator and a 2-arg function and languages are not very consistent amongst each other as to whether it's reduce(initial_acc, callback(acc, elem)) or reduce(callback(acc, elem), initial_acc) or reduce(callback(elem, acc), initial_acc) or what.
Hard to remember. Also some languages have a version of reduce that doesn't take an initial accumulator at all, which is just a footgun waiting for you to hit an empty collection. Also ALSO, the accumulator can easily become awkward in languages that don't support anonymous types or don't support easy mutation of an anonymous type record. Which is most of them!
Haskell got this right. You have foldr (right fold) and foldl' (left fold), and the order of the callback is opposite. If you do a left fold, then the initial accumulator is applied on the left; if you do a right fold, then the initial accumulator is applied on the right.
foldr f z [x1, x2, ..., xn] == x1 `f` (x2 `f` ... (xn `f` z)...)
foldl' f z [x1, x2, ..., xn] == (...((z `f` x1) `f` x2) `f`...) `f` xn
The mnemonic here is that the folding function (aka the callback) replaces the comma.
I find this slightly easier to remember than other languages. In contrast most other languages do not simultaneously provide a left fold and a right fold, so they do not consider this aspect, making things more difficult to remember.
That said I totally agree this requires more brainpower to read and write than map or filter. For this reason I have sometimes refactored code to use foldMap instead of foldr or foldl', so one no longer needs to think of the direction of the fold or the order of arguments.
Ruby's `[1, 2, 3].inject(:+)` alias for `reduce` (name borrowed from Smalltalk) nicely reinforces that very mnemonic: you inject the operator _between_ the elements: 1 + 2 + 3.
:+ here is symbol meaning "send + message", more general form takes a block:
[1, 2, 3].inject { |a,b| a + b }
The downside of the "between" mnemonic is encouraging not handling an empty collection! Ruby's separate syntax to pass (args) {block} at least offers natural place to add initial value, but with symbol shorthand the API is harder to guess:
numbers.inject(0) { |a,b| a + b }
numbers.inject(0, :+)
(Also, it's always a left fold ((a) OP b) OP c, even if you give a symbol like :* which parses right-assiciative without parens — making the "inject operator between values" mental model less accurate.)
It's still a complex and more abstract function than map or filter. Those do a single thing that's easy to grasp. reduce/fold can be easily abused to duplicate the effect of most other collection functions, at the cost of making the code less readable. Although for slightly-too-clever people, that could mean you only need to know one function instead of all of them.
But it hurts readability. If you're going to do it, at least don't use it anonymously, but give it a name that clearly describes what's going on.
But even then, there can be hidden performance traps. I've often seen javascript that used reduce and created the new accumulator by using a spread on the old accumulator and adding the new one: `[...acc, newValue]`. But that spread is another iteration inside a loop, turning it from O(n) to O(n^2). A for loop where you append it is much faster.
That's what I tend to do, but since foldr/foldl' is so ubiquitous in Haskell it would be nice if I could just remember the argument order of the callback. kccqzy's explanation (in particular "it replaces the comma") might just help me do that :)
When the accumulator isn't the second argument in a fold, left or right, it feels wrong and I waste some time cursing whomever made a silly mistake like getting the order wrong.
You want the accumulator second to match up with `cons` and similar functions that expect an initial/existing value second.
Luckily the functional languages I use the most are sane in that respect.
In GNU Guile `reduce` is described as a special case of `fold`, where the first element is suitable to be used as initial value, while `fold` is more general and lets you specify another initial value. I think that makes a lot of sense.
While map is a great name, I always struggle to remember if ‘filter’ keeps elements that match the condition or removes them.
I mean, it’s like a colander: you filter noodles and water, but which one do you keep? The noodles, right? But, replace noodles with tea and now you want to keep the water part.
There's always the Ruby strategy of just making all the names work. `select` and `filter` are buddies and you can use whichever you want or even go back and forth. Not a fan of `reduce`? That's fine, `inject` has got your back. Miss getting to type `collect` from Java or Rust? Don't worry, just use it instead of `map`, it's the same thing.
Talking about un-guessable, misleading function names,
C++ std::remove.
I would never have guessed what it does exactly. (It moves elements that match the filter to the front, and moves the end-marker forward. Leaves all the elements in the collection. You need to erase them yourself. )
`remove-if-not` was deprecated before the Common Lisp standard was approved and yet it remained (and will never be removed because the standard will never be updated). That's not deprecated for any practical purpose. And it's more convenient than using `(remove-if (complement #'some-predicate) sequence)`
Scheme has `filter` and `filter-not` in the SRFI-1 list library. Both of which can easily be written using a fold to bring this vaguely on topic.
Nope! Common Lisp's filter is in fact remove-if-not, which is exactly the same thing as "keep if": keep all items which match the predicate (removing those that do not).
I suspect the reason the function was deprecated was its naming, nothing more; had it been called retain-if or keep-if, it would not have attracted deprecating attention.
The smell added to your code is just the double-negative name of that function, not what it's doing for you.
The name filter smells even more. Is that filtering for items that match? Or filtering out?
In physical filters, sometimes the filtrate is considered the payload output (that which passes through the filter) and sometimes the retentate (that which is caught in the filter).
I agree that keep-if is a better name. But you reiterated my point: remove-if works in the counterintuitive way the original comment noted, i.e. not like the common connotation of 'filter.'
As for deprecation, IIRC the '-if-not' functions were deprecated because the committee felt the 'complement' function accomplished that task better.
Edit: My IIRC seems largely correct. More detail at
Nonetheless, they were fooled by that function, because it's just keep-if by a funny name that includes "not" suggesting that it contains a complement that might be factored out.
If you want keep-if, you don't want to use a different function, which forces you to complement your predicate. If you want (keep-if #'redp jellybean-list) to keep the red jelly beans, you don't want to write (remove-if (complement #'not-red-p) jellybean-list). If keep-if has a silly name remoe-if-not, you might nonetheless prefer (remove-if-not #'redp jelly-bean-list).
Shims like complements are ugly, and compilers won't optimize through them for arbitrary function definitions (whose source code is not even in scope), so it is good to have both keepers and removers. Heck, it's useful to have a function which does both in one pass returning two values: the filtrate and the retentate.
Smalltalk has #reject: which does that. You could, of course, just wrap a not around the test in the closure, but sometimes reject with a well-named predicate is easier to read.
The filter keeps the tea... it's just that you then lift the filter out of the cup, carrying the tea with it. Flip your brain around to see it from that direction and it might help you with the mnemonics.
In elixir we have Enum.filter (run a predicate over the enumerable keeping the things that match the predicate) and Enum.reject (run a predicate over the enumerable removing the thing that match the predicate)
I think since we have a pair of them and reject is so obvious it helps me remember which way filter works.
I think Enum.keep and Enum.reject might be a better pair, but I've used them enough to internalize it now
Kotlin has filter and filterNot (it also has separate "reduce" and "fold" functions, dependingon whether you want to specify an initial accumulator value or not)
I was thinking an apt analogy might be making stock -- you filter out all the solid food you don't want to keep in the liquid.
And it's a doubly-good analogy, because I have occasionally gotten that confused in real-life as well. Twice in the past ten years I've had a stock boil away for three hours, and then set a colander in the sink and poured it through, only to watch my beautiful stock swirl down the drain because motor-memory made me forget that I wasn't draining pasta but should have put the colander in a bowl...
in those cases its less ambiguous (to me anyways) that returning true means 'yes' to 'keep' or 'exclude', whereas saying yes or no to filter is like 'filter to exclude or include?'
In some programming languages with RPN you can avoid this problem, because it makes sense to put it in the stack as the initial value, and then you can as easily have multiple initial values; and then the callback function can read that from the stack that you had put there, like anything else you will push into the stack to read it back later. For example, in PostScript you can write something like:
0 exch {add} forall
However, this is not as good if you want to use the first element as the initial value instead, but still it can be done but it is then not as simple (unlike in programming languages that do not use RPN but instead with function call with arguments, in which case it might be simpler).
I guess names as SELECT and WHERE are like SQL (although SQL works differently than other programming langauges).
Reduces the list to another list three times as long.
It's a reduction in the sense of a transformation (also often seen in complexity theory), not in the "this makes this smaller" everyday usage that I think about first.
In rust iterators there's both fold (you supply the initial value) and reduce (it uses the first element as the initial value, doesn't work on empty iterators)
It doesn't help that fold/reduce often have different orders depending on the ecosystem. Every few months when I have a reason to reach for `fold` in nutshell I forget that it has the next element as the first arg instead of the second, which is what I'm used to from Rust. I guess I should just be happy I don't need to specify which direction I want like in OCaml.
> Map and filter usually have only one arg and if they have 2, the 2nd is almost always a 0-based index. They look identical in most languages, even when Microsoft chooses to call them Select and Where.
I don't understand. Map takes input of type a and size n and returns output of type b and size n.
Filter takes input of type a and size n and returns output of type a and size ≤ n.
I think their point was that map/filter _individually_ look identical in most languages, i.e. map looks the same across most languages, and filter looks the same across languages, not that map and filter look identical to each other in most languages.
More trivially, map and filter are operations that can be understood by what they do to individual elements, while reduce is a folding operation that is applied recursively on its own output. Much harder to think about.
I'm not the Paint.NET developer (just a happy user of that software), but to me the moral logic would go something like this:
If Anthropic can absorb proprietary or GPL code into their models, which they rent usage of for tens of billions of dollars in revenue, and successfully claim that this violates no licenses, then I can also consider the output of those models "clean" for purposes of my software I give away for free.
> ...even people in tech can vastly overestimate the capabilities...
I think people in (software) tech are currently more prone to overestimate the capabilities, because LLMs in a harness are genuinely excellent at programming. Programming is the perfect LLM task since 1. it's symbolic manipulation, 2. there is a vast corpus of high quality training data, and 3. most mistakes can be harmlessly caught at compile-time or unit-test-time. Especially point 3 makes it so that just throwing more "effort" at a problem, something the machine is endlessly willing to do, virtually guarantees an improved result.
In contrast there is no way for the machine to write a unit test to double-check its work when what it's offering the user is a legal document, or a medical diagnosis, or a recommendation of "yep that mushroom is safe to eat".
I've often seen the "Gell-Mann Amnesia Effect" referenced with respect to LLMs. It is said that most people can tell the LLM is not great at their own subject of expertise, yet they still trust it for other subjects where they can't personally assess the quality of its answers. Imagine how bad this is when the LLM actually is great at the thing you have expertise in.
It is just a coding mistake, except that fixing that mistake leaves you with clunkier abstractions.
If you have Foos, and users have permissions that control what they can do to a Foo, you'd like to have a function `GetPermissions : (UserId, FooId) -> Async<Permissions>`. If users can frob Foos you'd like to have a `FrobFoo : (FooId) -> Async<void>` function.
But as soon as you let users select multiple Foos, or god forbid, an entire folder containing Foos, and bulk-frob them now you have to write `FrobFoos : (List<FooId>) -> Async<void>`. And to avoid the implementation of that causing another 1+N checking permissions, you also need `GetPermissionsBulk : (UserId, List<FooId> -> Async<Dictionary<FooId, Permissions>>`. The singular forms of those functions, to avoid duplication, now become wrappers over the bulk forms.
The logic becomes harder to trace in the rewritten, bulk forms of the functions, but they are efficient.
Next the customer hits you with a request like "let's have a smart-frob function that works on all the selected foos. For foos that are red, it frobs them, if they are blue, it fizzles them". Now you have to bulk-load to select the redness or blueness of all your Foos, build two separate lists, red and blue, then call your bulk-frob and bulk-fizzle functions accordingly on the two lists. Again the machinery to turn the requirement into a batch-shaped thing is not a lot, but it does kind of obscure the original business requirement.
At various times in the life of the project you will have a feature that starts as a "always done on one Foo" thing because it's triggered by a button on the detail screen. Then somebody will possibly come along and want to do it in bulk later and you have to rewrite the implementation. Unless you have very strict code review that everything MUST be written in batch-style taking a list of IDs up to the API layer.
I wrote a library[1] many years ago to solve this problem and allow the straightforward, non-batch versions of the functions to be automatically batchable. The idea is kind of like what React did for frontend dev: React was not faster than mutating the page with jQuery soup, but it was much faster than replacing the entire DOM on every render, and it let you write your code as if that was what you were doing. That was a very simple mental model and much less buggy than jQuery soup.
The idea of my library was basically borrowed from other functional languages with a resumption monad, meaning that instead of an opaque async task to go do a thing, you have a "plan" which could either be a. done or b. waiting on some errand that requires firing off a query. If you have a list of plans like from a loop, you could step all of them to the next errand they are waiting on, then fire those off in a batch. So plans could be composed linearly or "batch-style" depending on your preference[2].
What makes it very powerful is the combination with an F# type provider that could analyze your SQL and automatically determine a caching profile for each query. It knows what tables the query reads from, what tables it writes to, whether it uses any impure functions like random(), etc. So within one transaction, it wouldn't re-run the same pure query again, it would pull the results from a local cache -- except if another command issued in that transaction updates those tables, the cache is automatically invalidated. This solves the other code smell that starts to accumulate as you try to write efficient database code in a complex app -- keeping materialized objects loaded in memory and passing them around to other functions so they don't have to re-query for them.
Anyway, it was a little too weird to catch on, and I was a little too burnt out to maintain it.
- Ask it to update the skill to warn it against making that mistake again
I think that, using that iterative process, I end up with something better than just asking it to perform the task plainly
One observation: ask an LLM to write a skill, it tends to make them overly verbose and prescriptive. Often, something briefer and human-written actually works better
Another: I generally let the LLM propose edits to the skill, but I review them carefully and often modify them, because I find it has a tendency to solve the current problem at the price of worsening the solution to a previous one
> - Ask it to update the skill to warn it against making that mistake again
Yep! I have a canned prompt that basically boils down to "scroll up, read through and give me the top `n` things that were difficult..." and more often than not a small skill or change to `agents.md` comes out of that.
Technically speaking: it can't, any more than it can actually answer any other thing you ask it to do.
But in practice it does produce fairly reasonable output fairly often, and if you've been there watching and correcting it you can probably validate the result quite easily. "Re-read everything before" and similar are definitely anthropomorphizing, but that doesn't mean they're ineffective.
> One observation: ask an LLM to write a skill, it tends to make them overly verbose and prescriptive. Often, something briefer and human-written actually works better
Incredibly so. All too often a “hey, you shouldn’t have done that here” turns into “NEVER X”, without it actually understanding or making an effort to understand why the correction was made, though I would say usually the context to do so should suffice. I have a rule for it to reason this through and use nuanced language and would say the frontier GPTs (5.4 to 5.6 Sol) get it right around half of the time (very rough estimate). The conclusions are really dumb sometimes.
I’d be interested in an expertly crafted skill here, maybe there are things about it you can still improve. This is an area in which I still find LLMs to be quite lacking.
It also made me appreciate the complexity of the nuances and levels of indirection of what I want to teach it: “Upload to the remote host, which in this case is this but might be another one in another use case, only do this on the local system and this on the remote, do this to X unless Y …, prefer doing Z unless another rule overrides it …”
I sometimes feel like there could be a more expressive way to structure to structure these rules (bring the snark!).
Yep. That is what I do too. But a lot of times the changes it makes are bandaids rather than addressing root cause. I have to periodically evaluate the skills for consistency and opportunities for simplification.
Oh, skills are absolutely useful for giving the LLM distilled knowledge of things that it doesn't just know ("off the top of its head" so to speak). For example, a company's coding style guide is something that could be very useful to express as a skill.
My agent skills are just sets of instructions for my preferred way for something to be done. Mostly code reviews listing classes of bugs and output format and the like.
But also development instructions focusing on how and what to research before implementing (some of the stuff I do ends up heavily influenced by papers and publications while the naïve implementation is often bad, old, or gets stuck.
Not true. At work I have skills for different sets of APIs for different scenarios. That saves some prompt bloat because you rarely need to straddle projects. So you just load whatever you're using and don't have to maintain a a giant prompt.
They are useful for packaging all the related bits together. Some instructions on the line to add to the main prompt, detailed policy doc that spells out details, troubleshooting docs, the code itself, etc.
I actually don't mean that skills are useless, I have them in my projects too. Just that any time I've seen one shared publicly, basically the introductory blurb of "what does this skill do?" would serve adequately as "the skill". Such is the nature of machines that understand natural language.
It makes it kind of funny publishing skills online because by the time somebody knows they want the skill, they already have it.
Projects like SkillOpt[1] are pretty good evidence that the ceiling for skills isn't "describe it in a sentence and let the LLM generate it". There's value in iterating on a skill and figuring out what the weak parts are. At that point, sharing the skill can indeed be useful. Just look at superpowers, GSD, etc. They're 'just' skills (and a few bash scripts), but the text in them is useful enough to share.
In no way does this mean LSP is a perfect solution, but anything synchronous would be a step backwards.
reply