Declarative Programming with AI/LLMs
blog.codesolvent.com
blog.codesolvent.com
Note:
WS-MENU-ITEM(1) OF WS-LABEL
When the syntax, which is similar to natural language is: elementary-var IN|OF group-var
It is a good example if you want to prove to people that they are a bad idea.I prefer to use these tools as a form of red/green/refactor workflow, where I don't use them in the refactor step or the test case step.
“This function processes logins using a complex and unclear logic. Exceptions are not thrown and instead most failures are represent using a success code that sets the current logged in user to either null or a user object with a null or empty string as the name.”
Here is an example:
/**
* Switches the current timeout settings to use the SPARQL-specific timeouts.
* This method should be called when making SPARQL SERVICE calls to apply
* shorter timeout values.
*
* <p>
* The SPARQL-specific timeouts are shorter to ensure that unresponsive or slow
* SPARQL endpoints do not cause long delays in federated query processing.
* Quick detection of such issues improves the responsiveness and reliability
* of SPARQL queries.
* </p>
*/
public void setDefaultSparqlServiceTimeouts() {...I have had the opposite experience. For complex tasks, LLMs fail in subtle ways which requires inspection of its output: essentially “declarative” to “imperative” is bug ridden.
My trick has been to create DSLs (I call them primitives) that are loaded as context before I make declarative incantations to the LLM.
These micro-languages reduce the error space dramatically and allow for more user-friendly and high-level interactions with the LLM.
>Declarative processing of configurations generated via AI is a way to ground the AI, this requires a lot of work since you don't just offload requests to an AI but rather your processing logic serves as a guardrail to ensure what's being done makes sense. In order for AI to be used in applications that require reliability, this work will need to be done.
When I was playing around with AI for data analysis last year, the best results for me came from something like this but more on the imperative side: RAG with snippets of R code. My first attempt was taking snippets directly from existing scripts and adding comments to explain the purpose and caveats. That didn't work well until I put in a lot of effort replacing the "common knowledge" parts with variables or functions. For example, no more `sex = 1`, but `sex = "male"`. Common tasks across scripts were refactored into one or a few functions with a couple parameters, and then placed in a package. The threshold for applying the DRY principle was lowered.
In the end, I decided a custom solution wasn't worth the effort. The data had identifying details of people, so any generated code would have to be checked and run by analysts who already had access to the data. But the process of refactoring stuff into descriptively-named objects was such a big benefit, the AI code wasn't doing enough to justify the effort. Again, this was using a custom system made by a total ML noob (myself) with GPT 3.5. The execs banned usage of LLMs until they could deal with the policy and privacy concerns, so I don't know what's possible these days.
> Do you know the industry term for a project specification that is comprehensive and precise enough to generate a program?
> Code, it's called code.
Obviously machines need code to execute. But humans don't need to write every line of it. Transitioning to using (future) LLMs as a programming language is transitioning from the role of a programmer to the role of a customer (or at least PM). Conversely, as a programmer (or technical manager), if your job is to extract a precise spec from a customer that doesn't know what they want, your job is exactly the one "LLMs as a programming language" are going to replace.
I actually am sympathetic to your point about the value of LLMs in programming, but more from the perspective that LLMs can help us to do the precise description gradually and interactively in a much better way that a dumb REPL.
If you think your job as implementing only specifications (in form of Jira tickets), then maybe you don’t see the difference. But more often, you’re trying to define the problem in the first as the customers can only describe the current situation and needs. The job is to design a system that could satisfy these needs and going iteratively from natural language to code, removing ambiguity in the process. Stopping midway in the process and hoping an LLM can continue down is just playing slot machine with code. And then there’s the whole system evolution and maintenance.
Would you mind sharing more details of your approach and setup?
-----------------
Now consider the following Prolog predicates:
biomarker(Name, Status) where Status will be one of the following integers -
Wildtype = 0
Mutated = 1
Methylated = 2
Unmethylated = 3
Amplified = 4
Deleted = 5
Positive = 6
Negative = 7
tumor(Name, Status) where Status will be one of the following integers if know else left unbound -
Newly diagnosed = 1
Recurrence = 2
Metastasized = 3
Progression = 4
chemo(Name)
surgery(Name) Where Name may be an unbound variable
other_treatment(Name)
radiation(Name) Where Name may be an unbound variable
Assume you are given predicate atMost(T, N) where T is a compound term and N is an integer.
It will return true if the number of 'occurences' of T is less than or equal N else it will fail.
Assume you are given a predicate atLeastOneOf(L) where L is a list of compound terms.
It will succeed if at least one of the compound terms, when executed as a predicate returns true.
Assume you are given a predicate age(Min, Max) which will return true if the patient's age is in between Min and Max.
Assume you have a predicate not(T) which returns true if predicate T evaluates false and vice versa.
i.e. rather than '\\+ A' use not(A).
Do not implement the above helper functions.
VERY IMPORTANT: Use 'atLeastOneOf()' whenever you would otherwise use ';' to represent 'OR'.
i.e. rather than 'A ; B' use atLeastOneOf([A, B]).
EXAMPLE INPUT:
Patient must have recurrent GBM, methylated MGMT and wildtype EGFR. Patient must not have mutated KRAS.
EXAMPLE OUTPUT:
tumor('gbm', 2),
biomarker('MGMT', 2),
biomarker('EGFR', 0),
not(biomarker('KRAS', 1))
Express the following constraints as a Prolog conjunction.
Do not enclose the code in a code block. Return only the Prolog code - no commentary.
Be careful to use only the supplied constraints, do not add any:
$constraintJust to be clear, your "EXAMPLE OUTPUT" is what is then fed to your prolog meta-interpreter to generate executable code in some other language (you mentioned Groovy) which is actually run i.e. answers the user query. Essentially then, a context is bounded by "pidgin Prolog" (i.e. Prolog+Natural Language) for the LLM and then user queries in Natural Language are submitted against it to generate valid Prolog code. This can be thought of as the logic/constraints of Prolog inference engine in the input modulating the interpretation/inference of the accompanying natural language by the LLM to keep it "on the straight and narrow" towards an accurate output.
I was actually thinking of using "Structured English" (https://en.wikipedia.org/wiki/Structured_English) for this and maybe build a CASE Tool using LLMs for round-trip software engineering.
tumor('gbm', 2),
biomarker('MGMT', 2),
biomarker('EGFR', 0),
not(biomarker('KRAS', 1))
Note well: this is not valid Prolog code. If you put it in a file and consult it with a Prolog interpreter you'll get multiple errors.You could call it Prolog pseudocode but in that case you don't need Prolog-like notation. You can just state your "constraints" in natural language. Have you tried this?
As others have pointed out, natural language is often insufficient to describe precisely the operations that you want. Declarative programming solves this with specialized syntax; AI codegen solves this by guessing at what you left out, and then giving you specific imperative code that may or may not do what you want. Personally, I'll be investing my time and resources into the former.
As any software dev knows, the problem is not the language. The problem is that the vague musings of an "ideas guy" cannot be turned into an actual program without a lot of guesswork, refinement, clarification, "filling-in", and outright lying. The required understanding and precision of description is just not that far from the understanding and precision required to write the code.
Here is a code example
ReadFileAndUpdate
- read file.txt in %content%
- set %content.updated% as %now%
- write %content% to file.txt
I call this intent based programming. There isn't a strict syntax (there are few rules), the developer creates a Goal (think function) and writes steps (start with -) to solve the goalI've been using it for clients with very good results, and from the 9 months I've been able to build code using it, the experience has shown far less code needs to be written and you see the project from different perspective.
Plang being analog language I see the LLM is able to code so much more and it never has syntax, library or other build errors.
They can already take descriptions of tasks and write computer programs to do those tasks, because they have a genuine understanding of the tasks (again no qualia implied).
I never said there are no limits to what LLMs can do, or no limits to what logic can prove, or even no limits to what humans can understand. Everything has limits.
EDIT: And before you accuse me of saying LLMs can understand all tasks, go back and re-read the post a second time, so you don't make that mistake again.
I kind of regret when NFTs were all the rage, at least it was fun.
Also please show us a real code that is generated.
SetUpdatedToNow
- set %content.updated% as %now% in the file "file.txt"
The whole reading and writing feels like a leftover from the days of programming. Reading it in, modifying and then writing assume it fits in memory, leaves out ideas around locking, filesystem issues, writing to a temp and swapping, etc. Giving the actual intent lets the LLM decide what is the best way which might be reading in , modifying and then writing.But also the main reason is that it's much more difficult to solve the intent when mixing multiple action into the same step. In theory it's possible but the language isn't there yet.
[0] https://github.com/jmaczan/text-to-ml
[1] Page four - https://pagedout.institute/download/PagedOut_004_beta1.pdf#p...
Plain language is not precise enough.
The idea that "I tell the AI what I want, and it writes the code for me!" is "declarative programming" is so wrong it's almost comical... except that it's yet another instance of the LLM-idiots confusing their lack of knowledge with not needing to know or understand it. In the process the term "declarative programming" as a concept may end up flushed down the shitter.
For instance:
// pseudocode input
fizzBuzz(count)
for each i in count
if divisible by 3, print 'fizz'
if divisible by 5, print 'buzz'
if both, print 'fizz buzz'
// rust output
fn fizz_buzz(count: i32) {
for i in 1..=count {
match (i % 3, i % 5) {
(0, 0) => println!("fizz buzz"),
(0, _) => println!("fizz"),
(_, 0) => println!("buzz"),
_ => println!("{}", i),
}
}
}I'd say that natural language enables shared understanding because it *allows* ambiguity. We can construct abstractions and find analogs to put them into words. We expect the hearer to reconstruct those abstractions from a different starting point and by referring to a different set of experiences. The potential for ambiguity is a necessary part of that process. We get closer to a reliably shared construction by layering our analogs and testing our respective mental models against each other.
Formalism and DSLs definitely do a better job by giving us an initial set of shared meanings to work from. An LLM might be able to wrap a fuzzy interface around a DSL but I agree that sharing a common semantic framework without fuzzy mediation might be a much better idea.
Because natural language is not a good tool to describe computational processes.
Which one would you rather write:
(a) p(x) = x^2 + 5x + 4
(b) Let us consider the object in the ring of univariate polynomials with complex coefficients defined by the square of the variable, plus five times the variable plus four.
Every scientific discipline moves away from natural language as soon as possible. Low-code, no-code and the likes have been a dismal failure precisely for this reason. Why would I move back to natural language if I can effectively express my problem in a formal language that I can manipulate at-will?
Have you seen a scientific paper that only had mathematics?
Natural language is still necessary for scaffolding, exposition, contextualization.
Boole’s Laws of Thought or Church’s The Calculi of Lambda-Conversion are mostly describing how to be so precise that the description of the problem equates its solution. But formal languages have their own issues.
By all means use some formal language to describe LLM capabilities and so forth, but the most fantastic thing about using LLMs is that you can convey the why along with the what and get better results and the "why" does not lend itself to expression in formalized notation.
First, on imperative versus declarative. I would describe imperative as "giving a list of instructions to follow". The words "instruction" and "direction" are largely synonyms in my mind and the difference may be subtler than the original words they are trying to describe. Instead, I would say that declarative programming gives "a goal and a set of constraints". We describe what we want, not how to get it. A large part of describing what we want is by describing what we don't want or can't do.
On using LLMs for declarative programming, I assert that we already do this. Prompt engineering is all about defining a set of constraints on the LLMs response. The goal is often within the system prompt: answer the user's question given the context. The user's request is just one of many constraints on the answer.
This declaration in the form of constraints is a direct result of the fact that LLMs operate on conditional probabilities. An LLM chooses each token by taking the list of all possible tokens and their a priori probabilities and conditioning those on the tokens that preceded it. By prefacing the generated output with a list of tokens describing constraints, we condition the LLMs generation to fit those constraints. The generated text is the result of applying the constraints to the space of all possible outcomes.
As we know, this isn't perfect. Most declarative languages and their engines use strict logic to limit the generated solutions, whereas LLMs are probabilistic. The constraints aren't specified in concrete terms but as a set of arbitrary tokens whose influence on the generated output is based on frequency of occurrence within a corpus of text rather than any logical rules.
Still, the fact that the generated output is the result of conditioning based on a set of tokens provided by the user means that it uses constraints to determine an outcome that fits those constraints, which is exactly how we solve a problem based on a declarative description.
People say that a microservice is something that "fits in your head". Once the logic gets too complex, you should separate it into another service. Maybe in the future the phrase will be "what fits in the AI's context". That would be a good litmus test: if a piece of software is hard for an AI, maybe we should simplify it - because it is probably too hard for the average human as well.
The real problem always comes back to the fact that the LLM cant just make code appear out of nowhere, it needs _your_ prompt (or at least code in the context window) to know what code to write. If you can't exactly describe the requirements - or what is increasingly happening - _know_ the actual technical descriptions for what you are trying to accomplish, its kinda like having a giant hammer with no nail to hit. I'm worried of a sort of future where we sort of program ourselves into a circle, all programs starting to look the same simply because the original "hardcore" or "forgotten" patterns and strategies of software design "just don't need to be taught anymore". In other words, people getting things to work but having no idea how they work. Yes I get the whole "most people dont know how cars work but use them", but as a software engineer not really knowing how the actual source code itself works? It feels strange and probably ultimately the wrong direction.
I also think the entire idea of a fully automated feature build / test / deploy AI system is just impossible... the complexity of such a landscape is just too large to automate with some sort of token generator. AGI, if course, but LLMs are so far from AGI it's laughable.