What I am trying to solve
Agents now write a large share of the code in the systems I work on, and the share is growing. That changes what a programming language is for. It used to be enough that a language let a human tell a machine what to do. Now a language also has to be the shared medium in which humans and agents describe a system, reason about it, change it, and check each other's work.
That raises the bar a lot. Such a language has to be:
- readable by both — a person reviewing an agent's change, and an agent reading a person's intent, must see the same thing;
- able to state hard things precisely — not only "call this API", but "this part of the system may do these things and nothing else", "this action must be approved", "this must happen exactly once";
- checkable by a machine — whatever is stated formally should be verified automatically, at compile time where possible, because no one can review every line an agent writes;
- portable — the same ideas have to work in the browser, natively, on the JVM, in the cloud, and in the different languages one organisation already uses;
- built on real abstraction — without it, every large system eventually collapses under its own cognitive weight: people stop understanding how it is put together, agents make more and more mistakes, and nobody can show that it is correct.
The last point is the one that worries me most. Agents are very good at producing code locally. They are much worse at keeping a large structure coherent, and so are we. If the language does not carry the structure, the structure is lost.
So the question is: what language, and how would we know it is the right one?
The ideas are already known — they look scattered
Functional programming has been answering parts of this question for decades, and almost every mainstream language has borrowed from it. The list of ideas is long:
- the lambda calculus;
- referential transparency and the equivalence of expressions;
- algebraic data types and pattern matching;
- type theory and category theory [1];
- continuation-passing style and delimited continuations;
- coroutines and generators;
- tail calls without limit;
- non-blocking asynchrony;
- reactive streams.
Each language implements some of these, usually as separate features with separate rules: one mechanism for exceptions, another for async/await, another for generators, another for dependency injection, another for transactions, another for logging. They look unrelated.
They are not. One idea contains all of them as special cases, is simple enough to explain in a few paragraphs, and is mathematically exact. It is called algebraic effects and handlers [2, 3, 4].
Algebraic effects, as data types
Here is the idealised model. It is small on purpose.
An operation is a question, written as data. It names what it asks and the type of its answer:
Get : Op[State[S], S] -- "what is the state?" answered by an S
Put(s) : Op[State[S], Unit] -- "set the state to s" answered by nothing
Charge(c) : Op[Payments, Receipt] -- "charge this card" answered by a receipt
An effect is a family of operations — an algebraic data type. State[S] is Get | Put(s). Payments is
Charge(c) | Refund(r). An effect is an interface, not an implementation. It says what may be asked, never how it
is done.
A computation is data too. A program that may ask the operations of a set of effects R and comes to a value of
type A is:
Comp R A = Done(a : A) -- finished, with a value
| Ask(op : Op[E, X], k : X -> Comp R A) -- asks op (E in R); k is the rest
That is all a program is: either a value, or a question together with the rest of the program, waiting for the
answer. Sequencing is substitution: Done(a) >>= f = f(a), and Ask(op, k) >>= f = Ask(op, x -> k(x) >>= f).
Mathematically this is the free monad over the signature of operations [5, 6]: the most general structure in which
programs can be written using those operations, with no meaning attached yet.
A handler gives an effect its meaning. It is two functions:
ret : A -> Comp R' B -- what to do with the final value
on : (op : Op[E, X], k : X -> Comp R' B) -> Comp R' B -- how to answer each operation
and handling is a fold over the program's structure:
handle h (Done a) = h.ret a
handle h (Ask op k), op of E = h.on op (x -> handle h (k x))
handle h (Ask op k), otherwise = Ask op (x -> handle h (k x)) -- passed on outward
Algebraically, a handler is a model of the effect's signature, and handle is the unique structure-preserving map from
the free model into it [3]. That is where the word algebraic comes from, and it is why the theory has laws you can
rely on:
handle h (pure a) = h.ret a
handle h (perform op >>= f) = h.on op (x -> handle h (f x)) -- op belongs to h's effect
handle h (perform op >>= f) = perform op >>= (x -> handle h (f x)) -- it does not
The type records what a program may do. A program's type is A ! R: a value A, with the effects R [7, 8].
Handling an effect removes it from R. A program can be run only when R is empty — when every question it may ask
has someone to answer it. So "this part of the system may charge cards and read configuration, and nothing else" is
not a comment or a convention. It is the type, and the compiler checks it.
Why this one idea contains the others
Look at what a handler's clause receives: the operation and k, the rest of the program, as an ordinary value.
Everything in the list above is a choice of what to do with k:
Resume k… |
…and you get |
|---|---|
| once, immediately | ordinary state, configuration, logging, dependency injection |
| never | exceptions, early exit, cancellation |
| many times | backtracking, search, probabilistic programming, every path of a choice |
| later, from elsewhere | async/await, non-blocking I/O, callbacks — with no "coloured" functions |
| once per value, on demand | generators, coroutines, lazy and infinite streams, reactivity |
| after asking something else | delegation: one effect implemented by others |
| after recording or checking | interception, audit, policy, approval |
And the other ideas follow from the representation itself:
- Referential transparency and equivalence of expressions: a program is a value, so equal programs can replace each other, and the laws above are equations you can use to reason and to refactor.
- Continuations and CPS:
kis a delimited continuation; the handler is its delimiter. - Unlimited tail recursion, stack safety: a program is data run by a loop, so its depth lives on the heap, not on the call stack.
- Algebraic data types and pattern matching: operations are an ADT, and a handler is a pattern match over them — exhaustive, so a forgotten case is a compile error.
- Category theory: the free monad, algebras and homomorphisms are the precise statement of all of the above [1, 5].
One concept, a handful of definitions, and the features that most languages add one by one fall out as special cases of a single rule.
Why this matters for code that agents write and read
This is the part I care about most.
1. A strict formal model, small enough to hold in mind. Humans and agents both do best with a model that has few rules and no exceptions. Effects give exactly that: operations are data, programs are data, handlers are folds, and three equations say what handling means. An agent that has learned the model once can apply it everywhere in the system, at every scale. So can a reviewer.
2. The business logic becomes pure and refined. A program written with effects reads as the business process and nothing else:
def checkout[R[_]: Has[Cart] : Has[Payments] : Has[Mail]](user: User): Receipt ! R =
for
items <- Cart.items(user)
receipt <- Payments.charge(user.card, items.total)
_ <- Mail.send(user.email, Confirmation(receipt))
yield receiptNo HTTP client, no retry policy, no connection pool, no transaction boilerplate, no logging framework. The type says what it may touch. That is the right level for a person to review and the right level for an agent to generate: there is far less room for the accidental complexity where agents make most of their mistakes.
3. Every technical detail lives in one understandable, predictable place: the handler. Where Payments.charge
really goes — a test double, a sandbox, the production provider, a queue for asynchronous processing, another service
in another language — is decided by a handler. Handlers are small, each concerns one effect, and each can be read and
tested on its own. When something goes wrong, there is one place to look.
4. Handlers compose like functions. A whole application is one line:
run(Mail.smtp(config)(Payments.stripe(keys)(Cart.postgres(db)(checkout(user)))))To run the same logic in a test, swap the handlers. To move a part to another process, swap a handler. To make a call asynchronous, swap a handler. The business code does not change, and the compiler confirms that every effect is still answered. Architecture becomes composition, and composition is checked.
5. Every action passes a point of control. Because every interaction with the world is an operation, every interaction can be intercepted: seen, changed, limited, recorded, sent for approval, or refused — without touching the program that asked for it. For AI systems this is decisive. An agent's next action was not written in advance by anyone. If its tools are effects, then what it may do is the type of the program it runs in, and what it actually did is a record kept by construction, not by discipline. An effect can even be marked sensitive, so that the compiler refuses any program that would perform it outside an audit.
6. Correctness can be checked, not hoped for. Rows are checked at compile time. Handlers can be tested against laws. The same program can be run against a recording and against the world. And because the model is mathematical, parts of it can be proved: I have the laws of interception and audit proved in Lean.
Why Scala — and why the idea is bigger than Scala
What I need from a language
I want one language in which I can build a whole product, and in which algebraic effects are an ordinary library rather than a special compiler feature. Scala is the only language I know that gives me all of this at once.
It is a full-stack language. Scala compiles to three targets: the JVM, JavaScript (Scala.js) and native code (Scala Native). The same code runs on a server, in a browser, in a serverless function and as a native binary. I use this in practice, not as a slogan:
- business websites with their order flows and back offices — static pages, the forms' code in the browser, the API on Cloudflare Workers, the data in Durable Objects — written entirely in Scala, with the same tests on the JVM and in JavaScript;
- serverless back ends that run unchanged on Cloudflare, on AWS Lambda or on a plain JVM host, the cloud's services being effects and each cloud a set of handlers;
- one application on every screen — a desktop window, a web page, native iPhone and Android apps, a terminal, a REST API, tools for an AI agent — written once (more on this below);
- machine learning from Scala: bindings to Apple's MLX on the JVM and natively, and a transformer engine of my own that runs a language model in the browser on WebGPU and WebGL2 — the neurosymbolic model described below runs on it.
It can express algebraic effects as a plain, purely functional library. Scala 3's type system — higher-kinded
types, type lambdas, intersection and union types, match types, given instances found by the compiler, opaque types,
inline and macros — is rich enough that effects need no special support. In my library okay-scala, the row of effects
is an ordinary type (State % Int + Choose), membership of an effect in a row is evidence the compiler finds, and the
machine that runs programs is a typed loop with no unchecked casts. Code written with it reads as if effects were
built into the language, as the checkout example above shows.
It is fast. The JVM's JIT compiler is one of the best optimising compilers there is. In my reference kernel a state operation through a handler costs on the order of ten nanoseconds, and a bind about six, on an ordinary laptop. Abstraction does not have to cost what people expect it to cost.
Its syntax is simple. A program is a for over its steps; a handler is a pattern match over an effect's
operations. Nothing else is needed to read it — which matters just as much for an agent as for a person.
Other languages have some of these qualities. Haskell has the type system and decades of effect research, but not the platforms. Rust has native speed and a strong type system, but effects there fight the ownership model and need macros or code generation. TypeScript and Kotlin have the platforms, but not types strong enough to carry rows of effects without compromise. No other language I know gives all of them together. That — and only that — is why I choose Scala.
An example of what the language and effects can do together: one application, every face
Desktop and mobile used to be the usual answer to "but can Scala do the front end?". With effects they are not a gap any more, and the way they stopped being one shows what the combination is capable of when it is used properly.
In okay-faces an application is one program over one effect, Face. The
program shows views and offers actions: fill asks for a form made from a type, confirm for a yes or no, show
tells something. Nothing in it says how. Each face is a handler of that effect:
- a desktop window (Swing on the JVM);
- a web page in the browser (Scala.js), or the same page driven by a program on a server;
- HTML from a server, with no script at all;
- a terminal dialogue;
- a REST API with links and affordances;
- an MCP server — the same dialogue as tools for an AI agent;
- a test face, which plays the program by a script and checks what was shown;
- and native phone apps: on an iPhone the program is compiled by Scala Native into a library and the screen is SwiftUI; on Android it runs on Android's own runtime and the screen is Jetpack Compose. On each phone it looks like that phone's own app — native lists and forms, pickers and switches, tables, pictures, the system's back gesture, dark mode and text size.
The program does not change between them; only the face does. A form comes from one typed description, which is at once the form a person fills, the JSON Schema an agent is given, and the decoder of what comes back. The phone's own capabilities — storage, the network, secrets in the Keychain or the Android Keystore, notifications, opening links — are effects too: the program says "keep this note" or "remind me in five seconds", and Swift does it on the iPhone, Kotlin on Android, and a test does it in memory. The Swift and Kotlin parts are small libraries shared by every app; they know nothing about any of them. The same UI scenarios run on the iPhone simulator and the Android emulator; the whole example app for the iPhone is 3.4 MB.
There is a point here beyond convenience. The MCP face gives an agent, at every step, exactly the actions a person would see. So an agent working through an application cannot do more than a person can — the boundary is the program, not a prompt.
That is the claim of this section in one example: a rich type system and effects as a library let one model, written once, reach the server, the browser, the desktop, the phone and the agent.
What remains genuinely outside Scala's reach today is embedded systems, where C++ and Rust rule — though Scala already compiles to native code. And the language keeps improving: capture checking, for example, adds safety that tracks who may use or hold a value.
The same model in every language
At the same time, nothing in the idea depends on Scala. Algebraic effects are a model of computation, not a feature of one compiler, and they make sense in any language. So I have built one compatible ecosystem of effect libraries for the languages in use today, and for some experimental ones:
- Scala 3 (and Scala 2.13 through a shared interface);
- Java 25 — both in the style of sealed interfaces and records, with the row of effects as a nested type checked
by
javac, and in direct style on Loom's continuations; - Kotlin, Rust, Go, Zig, TypeScript, Python;
- Lean 4, where the laws are not tested but proved;
- Goose, an experimental language.
Each takes the form its language allows: a program as data run by a machine, direct style on the language's own continuations, coroutines or generators, a function compiled into a state machine, or a plain function over a typed context. Where the compiler can check the row of effects, it does — in Scala, Java, Rust, TypeScript and Lean. Where a language cannot copy its own continuation (a Python or TypeScript generator), a program that must be resumed many times is written as data, never by re-running it from the start.
What holds them to one meaning is not resemblance but tests:
- a shared test kit of a thousand randomly generated programs whose results fold into one number, which several of the implementations reproduce exactly;
- a reference model — the same small kernel in Scala, Java and Rust — whose properties are the same tests in the three languages.
Many languages, one system
This opens a real possibility: to join programs in different languages into one system through effects.
The family speaks one protocol, the okay wire. A program runs in one process, in one language; when it asks an operation, the operation — a name and its arguments — crosses to another process, possibly in another language, and is answered by a handler there. The rest of the program stays where it is, as a value, known on the other side only by a number, so it can even be resumed more than once. To the program, a remote handler is just a handler.
This is checked, not only designed:
- in the reference model every host is run against every worker — Scala, Java and Rust, nine pairs, one answer;
- a separate check drives far sides written in Zig, Lean and TypeScript, and the platform's reference Python worker, from the Scala host;
- the platform's own host drives the reference model's three workers in turn: calls, held objects, programs resumed twice, streams under back-pressure, compression, authentication.
So the boundary between languages becomes the same kind of boundary as between modules: a set of effects. A team can keep its language. The system keeps one model.
Large heterogeneous systems, and taking a monolith apart
The property that makes effects good for agents also makes them good for architecture at scale: the business logic is written once, against effects, and where each part runs is a handler.
A monolith, taken apart in steps
Take a shop's checkout: reserve the item, charge the card, send a receipt. In a monolith these are three calls into modules the function reaches directly. With effects:
- Boundaries inside the monolith. The calls become an effect, as plain data; the logic keeps its shape. The modules are given at the top by a handler. It is still a monolith, but its boundaries are now explicit, and the compiler knows them.
- Services. A handler sends each operation over the wire, and the answer goes to the rest of the program. The business code does not change.
- Asynchronous. Each operation becomes an asynchronous wait; the handler resumes the program when the answer arrives. The business code still does not change.
The same test scenarios — themselves just handlers — check every step of the migration. Parts can be moved out one at a time, when each is ready; the rest stays local. Failures are handlers too: retries, timeouts, idempotency keys and circuit breakers wrap the remote handler, not the business code. This is the strangler-fig migration with a compiler checking each step.
Patterns that usually come as frameworks, as handlers
- CQRS: commands and queries are separate effects.
- Event sourcing: state is a fold over a log of events; commands decide on it and append to it.
- Sharding: the handler chooses a shard by the key the invariant is about.
- Eventual consistency: shards project their events into a CRDT view whose merge converges.
The program only performs effects. Storage, history, scale and consistency are all handlers.
What this means for those who manage information systems
Management Information Systems is a whole discipline: how information systems serve people, processes and decisions inside organizations [9]. Much of it is about control — who may do what, how every action is audited, how IT stays aligned with the business, how you change a vendor without breaking the company.
In practice, three things get mixed together in most enterprise systems:
- business logic — what the process does;
- policy — who is allowed, what must be checked, what must be logged;
- implementation — which database, which vendor, which cloud.
Because they are tangled, every audit is a project, every migration is a risk, and every policy change touches code that it shouldn't.
Algebraic effects separate exactly these layers. A program only describes what it wants: "read the invoice", "ask a manager for approval", "pay the supplier". These requests are effects. Separate handlers decide how each request is carried out. What this gives an organization:
- Audit by construction. Every action goes through a handler, so a complete audit trail is a property of the architecture, not an add-on someone forgot. It serves record-keeping, traceability of decisions and access control obligations — as evidence, not as a compliance claim.
- Controls as interceptors. Segregation of duties, limits, approvals: each is a handler wrapped around the process. Change the policy, not the process. An action can even be marked sensitive, so that a process which would perform it outside its control does not compile.
- Vendor independence. Same process, different handler. Moving from one payment provider or cloud to another does not touch business logic. What stays tied to a vendor is only what that vendor alone offers.
- One process, many interpretations. The same expense-approval process can be executed for real, simulated ("what if the limit were lower?"), costed, tested, or explained to an auditor — by running it under different handlers. Today these are often five separate artifacts that drift apart.
- Human-in-the-loop. "Waiting for manager approval" is just an effect. The process pauses there and continues when the answer arrives, even days later, because the paused rest of the process is an ordinary value that can be kept.
To be fair, BPM engines [10], hexagonal architecture [11] and middleware aim at similar goals. What effects add is precision: typed requests, composable handlers with a clear scope, and paused processes as real values. And effects won't fix adoption, culture or resistance to change: information systems are still mostly about people. But for the control layer, I think they are the right foundation.
A few more consequences, seen from management rather than code. Where a benefit is an argument rather than a measurement, I say so.
- The cost of change. Moving from a monolith to services, from on-premises to the cloud, from synchronous to asynchronous, becomes a change of composition, not a rewrite. The argument: the logic is not rewritten, so the cost of the move is the cost of writing and testing the new handlers. What still has to be done by the usual means: data migration, networking, accounts, contracts.
- Risk. The whole business flow can be tested without the outside world. An incident can be reproduced from the record. What a component — or an AI agent — is able to do is limited by its type and enforced at the point of control. I am building exactly this in practice: an effects runtime in which every tool call an AI agent makes passes through a policy handler before it runs.
- Decomposition and teams. An effect is a contract that a machine checks: the operations one part asks of another and the type of each answer. One team owns an effect and its production handlers; others write against it and test against a handler it supplies. When a module starts to use payments, payments appear in its declared effects — a reviewer sees that line change. A change to an effect breaks the compilation of every user, so the size of a change is known before the work starts. The same boundaries are where a service can later be cut out.
- Adoption without a rewrite. Start at one boundary of an existing system: one integration that hurts, or one audit requirement. Wrap today's code in a handler. Add a record or a limit at that handler, and a test handler. Then measure — test time, lines changed outside handlers when an arrangement changes, whether the record answered a real question — and widen module by module, or stop. Stopping after the first step loses little.
- People and cost. Writing business logic against existing effects is the easier part. Designing effects and writing handlers needs someone at home in typed functional programming, and a second such person so that the first can leave. The libraries are young. Abstraction has a measured, small but non-zero cost per operation.
- Where it does not pay. Small systems, pure computation with no interaction, hard real-time code, and teams that will not own the approach.
What I am building
I maintain the family of open-source libraries described above under the GitHub organisation okay-effects, with applications built on it: business sites and shops, serverless back ends, user interfaces as data, a guard that puts every tool call of a coding agent through a policy.
My current focus is applying all this to the architecture of AI itself.
What I am working on right now: a neurosymbolic micro language model
The project is a small neurosymbolic language model that can solve logic problems and work with data in external sources: tables, files, open data on the web. It runs in the browser, offline, on a laptop, and is meant to work on a phone. It is live at okay-effects.github.io/okay-desk.
The question behind it is simple. Large language models are impressive and unreliable. They compute, remember and reason in one opaque process, and when they are wrong, they are wrong fluently. Making them bigger makes them wrong less often, not never. For many tasks — "which customers have no orders", "who lives in the green house", "how did GDP change between these years" — an answer that is right 95% of the time is not an answer. We need a system that is exactly right, or says that it does not know. This is the old programme of neurosymbolic AI [12], and the more recent practice of letting a model write programs or call tools instead of computing [13, 14].
The principles
1. The language model does not compute. It formalizes. The model's job is to read a question written in natural language and turn it into a formal statement: a query over the data, or the facts and constraints of a logic puzzle. It also chooses the solver for that statement. Everything after that is exact: a query runs on a Datalog engine, a puzzle on a constraint solver.
2. A number never comes from the model. Every value in an answer is a cell of the data or a result of the exact engine. If the model writes a summary, the summary is kept only if every number in it is in the computed result.
3. Read the model where it decides. I do not ask the model to generate a program token by token and hope it is well formed; small models fail at that, and on my benchmarks a free-form program from a small model was almost never fully correct. Instead, small learned readers — linear probes [15] — read the model's internal states at the exact point where it makes each decision: which table, which column, which relation, which direction. Each decision is read separately and checked against the data, and the formal statement is assembled from those decisions exactly.
4. Abstain rather than guess — and show the reading. Each decision is either closed by the data, closed by the model with a margin above a threshold measured on held-out data, or closed by the exact side. A decision that is still open is not guessed: it is asked, in the words of the question. The user always sees how the system read the question, so a wrong reading is visible, not hidden behind a confident answer.
5. The model is a handler. In the architecture, "ask the model" is an effect like any other. Where a decision can be filled from — the data, the model's tokens, a reader, the user, a stronger model — is a choice of handler. Models are therefore interchangeable: a stronger one can be plugged in where needed. But the goal is that even a very weak one copes with almost any task, because it only has to read, not to know or to compute.
6. Small enough for a phone. The base is a 0.6-billion-parameter open model, cut to the layers the readers actually need and distilled back. It runs in the browser on WebGPU, on WebGL2 where WebGPU is missing, and partly on the CPU on a phone, through the transformer engine I wrote in Scala that compiles to the browser.
7. Knowledge as loadable, exact packs. What the model does not need to know — family relations, units, the rules of a domain — is not trained into weights. It comes as an exact definition that can be loaded, checked and replaced.
Where it stands
These results are modest, and real:
- Data questions. Over sample tables, the untouched 0.6B model with readers and exact facts answered all twelve sample questions — the score of an untouched 4B model, and of a fine-tuned 1.7B. A question takes about half a second to a second on WebGPU.
- Logic problems. A collection of 233 tasks in twelve classes: Einstein-style grids, who is where, who is who, orderings, deductions, family relations, routes, knights and liars, and more. On tasks phrased the way the training corpus phrases them, the reader is exact on 99.7–100% of decisions.
- What the gate shows. On the collection, the system shows answers to about two thirds of the tasks it can state and asks about the rest. In the browser it gave 59 right answers, asked about 20 and was wrong once out of 80; after the latest training, none of the answers it showed was wrong.
- What is not solved. On families of wordings it has never seen, it is right on roughly half or less, and on a sealed set I do not look at, it mostly refuses. Refusing is the intended behaviour there, but it is also the frontier: generalising the reading is the next work.
Programs inside the transformer: a translator of effects
The second line of research goes the other way: not reading exact structure out of the model, but putting exact computation into the transformer itself.
In okay-inlay I built a small language for programs that a transformer can run, in the spirit of RASP and Tracr [16, 17]. Its building blocks are the transformer's own:
- fields of the residual stream (categories, bits, flags);
- attention heads that look up the latest or every matching earlier position, or count them;
- rule neurons in the MLP.
A program written in this language is an effect, and it has several handlers:
- an interpreter, which is the reference;
- a compiler to transformer weights;
- a forward pass on MLX, run beside a real language model.
The law that the tests check is that the weights compute exactly what the reference computes. So exact operations — counting, quoting, following a relation to its closure — run inside the same machinery as the language model, layer by layer, not as an external tool call.
Combined with readers, this already works on specific tasks. On counting and on transitive closure, a 4B model alone fails. With a learned reader that takes the relation from the model's middle layers into a compiled exact machine, the exact part never erred, and every wrong answer traced back to a misreading.
The limits are concrete, and I keep them in view:
- a compiled program has a fixed depth, so it works on a window, not on unbounded input;
- half-precision arithmetic breaks some lookups;
- a compiled program cannot simply be merged into a standard weights file: today it is a side model that the engine runs together with the language model.
Why this is the same idea
Both lines are applications of the principle from the first half of this text. The question a user asks is a program. Each decision in it is an operation. The language model is one handler among several, the exact engines are others, and the compiled machine inside the transformer is one more. Handlers compose. The boundaries between what is guessed and what is computed are explicit, typed and checked. And every step is visible: what was read, from where, and why.
Why I think it is necessary
The more code agents write, the more the shape of a system has to be carried by the language rather than by the memory of the people who built it. Algebraic effects give that shape a precise form: a small set of definitions, with laws, checked by the compiler, readable by humans and agents alike, the same at every scale of a system and in every language it is written in. And the same principle can structure the reasoning of an AI system itself, so that the part that can be exact is exact, the part that cannot is marked, and both can be checked.
That is the language I believe we need. My work is to show, in code that runs, that it is practical — and to make it easy to use.
References
Algebraic effects and handlers
- E. Moggi. Notions of computation and monads. Information and Computation 93(1), 1991.
- G. Plotkin, J. Power. Algebraic operations and generic effects. Applied Categorical Structures 11(1), 2003.
- G. Plotkin, M. Pretnar. Handlers of algebraic effects. ESOP, 2009; extended as Handling algebraic effects, Logical Methods in Computer Science 9(4), 2013.
- A. Bauer, M. Pretnar. Programming with algebraic effects and handlers. Journal of Logical and Algebraic Methods in Programming 84(1), 2015.
- W. Swierstra. Data types à la carte. Journal of Functional Programming 18(4), 2008.
- O. Kiselyov, H. Ishii. Freer monads, more extensible effects. Haskell Symposium, 2015.
- D. Leijen. Type directed compilation of row-typed algebraic effects. POPL, 2017.
- D. Hillerström, S. Lindley. Liberating effects with rows and handlers. Workshop on Type-Driven Development, 2016.
Also: M. Pretnar, An introduction to algebraic effects and handlers, ENTCS 319, 2015 (a tutorial); O. Kammar, S. Lindley, N. Oury, Handlers in action, ICFP 2013; J. I. Brachthäuser, P. Schuster, K. Ostermann, Effects as capabilities, OOPSLA 2020; K. C. Sivaramakrishnan et al., Retrofitting effect handlers onto OCaml, PLDI 2021.
Information systems: management, control, architecture
- K. C. Laudon, J. P. Laudon. Management Information Systems: Managing the Digital Firm. Pearson (many editions).
- M. Weske. Business Process Management: Concepts, Languages, Architectures. Springer (3rd ed. 2019).
- A. Cockburn. Hexagonal architecture (ports and adapters). alistair.cockburn.us, 2005.
Also: J. H. Saltzer, M. D. Schroeder, The protection of information in computer systems, Proceedings of the IEEE 63(9), 1975 (least privilege, complete mediation); COSO, Internal Control — Integrated Framework, 2013 (segregation of duties); ISACA, COBIT 2019 Framework; ISO/IEC 27001:2022; Regulation (EU) 2022/2554 (DORA); Regulation (EU) 2024/1689 (the AI Act); S. Newman, Monolith to Microservices, O'Reilly, 2019.
Artificial intelligence: neurosymbolic reasoning, agents, transformers
- A. d'Avila Garcez, L. C. Lamb. Neurosymbolic AI: the 3rd wave. Artificial Intelligence Review 56, 2023.
- L. Gao et al. PAL: program-aided language models. ICML, 2023.
- T. Schick et al. Toolformer: language models can teach themselves to use tools. NeurIPS, 2023.
- G. Alain, Y. Bengio. Understanding intermediate layers using linear classifier probes. ICLR Workshop, 2017.
- G. Weiss, Y. Goldberg, E. Yahav. Thinking like transformers. ICML, 2021.
- D. Lindner et al. Tracr: compiled transformers as a laboratory for interpretability. NeurIPS, 2023.
Also: L. Pan et al., Logic-LM: empowering large language models with symbolic solvers for faithful logical reasoning, Findings of EMNLP 2023; T. X. Olausson et al., LINC: a neurosymbolic approach for logical reasoning, EMNLP 2023; S. Yao et al., ReAct: synergizing reasoning and acting in language models, ICLR 2023; K. Greshake et al., Not what you've signed up for: indirect prompt injection, AISec 2023; the Model Context Protocol specification, modelcontextprotocol.io; Y. Geifman, R. El-Yaniv, Selective classification for deep neural networks, NeurIPS 2017.
My work
- The organisation and its pages: okay-effects.github.io — the libraries; the Scala implementations compared; effects in other languages; effects as an architecture for those who manage information systems.
- One application on every screen, phones included: github.com/okay-effects/okay-faces.
- The reference model in Scala, Java and Rust: github.com/okay-effects/okay-effects.
- The neurosymbolic model: okay-effects.github.io/okay-desk; programs in transformer weights: github.com/okay-effects/okay-inlay.