-
-
Notifications
You must be signed in to change notification settings - Fork 911
Context based programming #5203
Replies: 1 comment · 6 replies
-
|
Hello! Thanks for the overview of implicit arguments in programming. If you would like to make a proposal for an addition to Gleam you'll need to do include the information detailed in this article, so we can reason about what to add and why. https://lpil.uk/blog/how-to-add-metaprogramming-to-gleam/ Note the post is about metaprogramming proposals, but it applies to all language feature proposals. |
Beta Was this translation helpful? Give feedback.
All reactions
-
Problem: Environment and Context Propagation in Real-World Gleam Code 2BackgroundIn real-world Gleam applications (web backends, bots, workers, background jobs), it is common to work with an application environment that contains infrastructure data such as:
A typical pattern is to define an pub type AppContext {
AppContext(
db: Db,
logger: Logger,
config: Config,
)
}This context is then threaded through most functions. The Problem1. Context Must Be Passed Through Functions That Do Not Fully Need ItIn practice, many functions only require a small subset of the application context, but are forced to accept the entire context record. pub fn handle(req: Request, ctx: AppContext) {
validate(req, ctx)
}
fn validate(req: Request, ctx: AppContext) {
ctx.logger.info("validating request")
}In this example, As the application grows, this leads to:
2. Manually Passing Only Required Fields Does Not Scale WellOne alternative is to pass only the required fields: fn validate(req: Request, logger: Logger) {
logger.info("validating request")
}However, this approach introduces new problems:
In larger codebases this becomes difficult to maintain. 3. Creating Smaller Context Records Adds Boilerplate and DuplicationAnother common approach is to define smaller context records: pub type LoggerContext {
LoggerContext(logger: Logger)
}But this leads to:
4. Modules Hide Dependencies Instead of Making Them ExplicitSome codebases rely on modules to provide implicit access to environment data. While this avoids argument passing, it has downsides:
This works against Gleam’s emphasis on explicitness and predictability. 5. Reader-Style Patterns Are Verbose and IndirectReader-like abstractions can model environment access functionally, but in practice they:
For many users, this is too heavy-weight for common application logic. Summary of the PainIn current Gleam code, developers often face a trade-off:
There is currently no language-level mechanism to:
Desired Properties of a SolutionA solution to this problem should:
Proposed Direction (High-Level)This proposal explores treating context as a first-class language abstraction:
This aims to reduce boilerplate while keeping dependencies explicit and statically checked. (Details of syntax and semantics would be discussed in subsequent sections.) Why This Is a Language ProblemWhile similar patterns can be approximated with existing Gleam features, they all involve trade-offs that scale poorly in larger codebases. This proposal argues that context propagation is a recurring, structural problem in real-world Gleam applications and may warrant language-level support rather than ad-hoc conventions. |
Beta Was this translation helpful? Give feedback.
All reactions
-
Contexts Are Useful for Business Logic, Not Only InfrastructureWhile contexts are often discussed in relation to infrastructure (configuration, databases, IO), the same problem appears in business logic. Business rules frequently depend on situational data:
These are not properties of domain entities themselves, but they influence how the same domain data should be interpreted or processed. Business Logic Often Depends on Situational ContextConsider a simple domain model: type Order {
Order(
total: Int,
status: Status
)
}A business rule such as “can this order be refunded?” is not purely a function of It may depend on:
Current Approaches Force Context Into Types or ArgumentsPassing Flags Explicitlyfn can_refund(order: Order, is_admin: Bool, region: Region) -> Bool {
case order.status {
Completed -> is_admin && region != EU
_ -> False
}
}Problems:
Embedding Logic Into Domain TypesAnother approach is to push logic into the domain: fn can_refund(order: Order) -> Bool {
order.status == Completed
}But this forces:
This breaks separation of concerns. Contexts Allow Business Logic to Stay Pure and AdaptiveWith contexts, business logic can depend on explicit, structured environment, without modifying domain types or bloating signatures. Define Business Contextscontext RefundPolicyContext {
user_role: Role
region: Region
}Business Logic Declares Its Requirementsfn can_refund(order: Order) with RefundPolicyContext -> Bool {
case order.status {
Completed ->
user_role == Admin && region != EU
_ ->
False
}
}Key points:
Same Domain Logic, Different Behavior in Different ContextsContext 1: Admin in USlet admin_us = context RefundPolicyContext {
user_role: Admin
region: US
}
can_refund(order)
// returns TrueContext 2: Regular User in USlet user_us = context RefundPolicyContext {
user_role: User
region: US
}
can_refund(order)
// returns FalseContext 3: Admin in EUlet admin_eu = context RefundPolicyContext {
user_role: Admin
region: EU
}
can_refund(order)
// returns FalseThe same function:
This behavior is:
Contexts Avoid Encoding Business Variants Into TypesWithout contexts, developers are often forced to:
Contexts allow business variation to be expressed:
Business Logic Evolves Like InfrastructureJust like infrastructure, business rules evolve over time:
Contexts allow this evolution without:
Summary
Key TakeawayTypes describe what something is. Both infrastructure and business logic benefit from treating context as a first-class abstraction. |
Beta Was this translation helpful? Give feedback.
All reactions
-
Composing Multiple Application Layers Using Contexts (Without Changing Types)In real applications, code is naturally split into layers:
A recurring problem is how to connect these layers without:
Domain Types Remain StableThe domain model should not change when new layers are added. type Account {
Account(
id: String,
status: Status,
balance: Int
)
}This type contains only domain data. Business Rules Depend on Business ContextBusiness logic often depends on situational rules. context AccountPolicyContext {
user_role: Role
region: Region
}fn can_withdraw(account: Account, amount: Int)
with AccountPolicyContext
-> Bool
{
amount <= account.balance
&& user_role == Admin
&& region != Restricted
}Here:
Application Layer Adds Workflow ContextNow consider application-level orchestration. context TransactionContext {
request_id: String
timestamp: Int
}fn process_withdrawal(account: Account, amount: Int)
with AccountPolicyContext, TransactionContext
{
if can_withdraw(account, amount) {
execute_withdrawal(account, amount)
}
}No new arguments. Infrastructure Layer Adds CapabilitiesInfrastructure concerns are introduced separately. context PersistenceContext {
db: Db
}fn execute_withdrawal(account: Account, amount: Int)
with PersistenceContext
{
db.update_balance(account.id, account.balance - amount)
}Infrastructure does not leak into:
Transport / UI Layer Adds Presentation ContextNow the same domain logic is used in a transport layer. context HttpContext {
request: HttpRequest
response: HttpResponse
}fn handle_withdraw_request(account: Account, amount: Int)
with AccountPolicyContext,
TransactionContext,
PersistenceContext,
HttpContext
{
process_withdrawal(account, amount)
response.send_ok()
}Again:
How Contexts Are Composed ExplicitlyContexts are composed where layers meet. fn handle_http_request(req: HttpRequest) {
let policy_ctx = context AccountPolicyContext {
user_role: req.user.role
region: req.user.region
}
let tx_ctx = context TransactionContext {
request_id: req.id
timestamp: now()
}
let infra_ctx = context PersistenceContext {
db: connect_db()
}
let http_ctx = context HttpContext {
request: req
response: make_response()
}
handle_withdraw_request(account, amount)
}Each layer:
Same Domain, Same Functions, Different Layer CombinationsThe same business logic can be reused in different environments. CLI toolfn cli_withdraw(account, amount)
with AccountPolicyContext, PersistenceContext
{
process_withdrawal(account, amount)
}Background jobfn scheduled_withdrawal(account, amount)
with AccountPolicyContext, TransactionContext, PersistenceContext
{
process_withdrawal(account, amount)
}No changes to:
Only the available context differs. Why This Is Hard to Achieve with Types AloneWithout contexts, developers typically resort to:
All of these approaches:
What Contexts Provide InsteadContexts allow layers to be:
Key InsightTypes define what data is. By separating these concerns, layers can be connected and recombined without rewriting domain models or widening APIs. One-Sentence SummaryContexts allow multiple application layers to be composed around stable domain types, enabling behavior to emerge from the environment rather than from changes to the data itself. |
Beta Was this translation helpful? Give feedback.
All reactions
-
Composing Multiple Application Layers Using Contexts (Without Changing Types)In real applications, code is naturally split into layers:
A recurring problem is how to connect these layers without:
Domain Types Remain StableThe domain model should not change when new layers are added. type Account {
Account(
id: String,
status: Status,
balance: Int
)
}This type contains only domain data. Business Rules Depend on Business ContextBusiness logic often depends on situational rules. context AccountPolicyContext {
user_role: Role
region: Region
}fn can_withdraw(account: Account, amount: Int)
with AccountPolicyContext
-> Bool
{
amount <= account.balance
&& user_role == Admin
&& region != Restricted
}Here:
Application Layer Adds Workflow ContextNow consider application-level orchestration. context TransactionContext {
request_id: String
timestamp: Int
}fn process_withdrawal(account: Account, amount: Int)
with AccountPolicyContext, TransactionContext
{
if can_withdraw(account, amount) {
execute_withdrawal(account, amount)
}
}No new arguments. Infrastructure Layer Adds CapabilitiesInfrastructure concerns are introduced separately. context PersistenceContext {
db: Db
}fn execute_withdrawal(account: Account, amount: Int)
with PersistenceContext
{
db.update_balance(account.id, account.balance - amount)
}Infrastructure does not leak into:
Transport / UI Layer Adds Presentation ContextNow the same domain logic is used in a transport layer. context HttpContext {
request: HttpRequest
response: HttpResponse
}fn handle_withdraw_request(account: Account, amount: Int)
with AccountPolicyContext,
TransactionContext,
PersistenceContext,
HttpContext
{
process_withdrawal(account, amount)
response.send_ok()
}Again:
How Contexts Are Composed ExplicitlyContexts are composed where layers meet. fn handle_http_request(req: HttpRequest) {
let policy_ctx = context AccountPolicyContext {
user_role: req.user.role
region: req.user.region
}
let tx_ctx = context TransactionContext {
request_id: req.id
timestamp: now()
}
let infra_ctx = context PersistenceContext {
db: connect_db()
}
let http_ctx = context HttpContext {
request: req
response: make_response()
}
handle_withdraw_request(account, amount)
}Each layer:
Same Domain, Same Functions, Different Layer CombinationsThe same business logic can be reused in different environments. CLI toolfn cli_withdraw(account, amount)
with AccountPolicyContext, PersistenceContext
{
process_withdrawal(account, amount)
}Background jobfn scheduled_withdrawal(account, amount)
with AccountPolicyContext, TransactionContext, PersistenceContext
{
process_withdrawal(account, amount)
}No changes to:
Only the available context differs. Why This Is Hard to Achieve with Types AloneWithout contexts, developers typically resort to:
All of these approaches:
What Contexts Provide InsteadContexts allow layers to be:
Key InsightTypes define what data is. By separating these concerns, layers can be connected and recombined without rewriting domain models or widening APIs. One-Sentence SummaryContexts allow multiple application layers to be composed around stable domain types, enabling behavior to emerge from the environment rather than from changes to the data itself. |
Beta Was this translation helpful? Give feedback.
All reactions
-
|
I'm going to close this as spam. Please feel free to open a new discussion, but refrain from generating text in future. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
-
What a problem.
A lot of languages have context.
Different programming scopes (gaming, OS, backend, frontend, desktop apps, database operations, etc.) have the same fundamental concept: context. I do not mean something abstract; I mean different data in different contexts and different restrictions and capabilities.
Games (ECS):
Components are data in some context; a system is a function that demands that data/context.
The whole game is a container of contexts.
But the language does not know about it; it is on the developer’s shoulders.
UI (React / Svelte / Solid)
Svelte:
You have global and local state context, but context is outside the language.
Dependencies are not explicit, not in function signatures, and the type system does not see them.
You have context, but it is not built into the language.
You need to pass
envthrough all functions, even those that do not need it.You also get something like a God object that contains all context.
Functions do not have minimal demands. I mean it is not explicit.
A function may need only two variables from the context, but you pass the entire context.
Yes, you can pass only those two variables, but it is painful every time to create new arguments with context for each function.
In OOP languages like TypeScript, C#, Java you have DI, but it is hard to scale.
It is a myth that they are good for large-scale codebases.
Operating systems also have their own contexts, but for simplicity I do not want to talk about that.
I hope you now understand that context is fundamental, but no languages have syntax for this.
Your code, your app, your 10-million+ LOC codebase are just different contexts that replace each other in the flow.
What problem I want to solve:
You want to write pure functions and you want to control side effects, but you need to pass different contexts.
In practice, you need to pass your environment all over the code.
Reader / IO-like abstractions.
Not explicitly global dependencies like
AppContext, when a function does not need all fields from it, but you still pass it.I suggest treating context as a different language abstraction.
Context is a structural environment of data and capabilities that a function can use, but not only as an argument.
Creating context
You can use
configexplicitly.This is how you initialize context.
ctxis just a lexical scope; it is not state.update_idautomatically uses its variables.Context is not global state.
You create it explicitly.
It has lexical visibility.
It is local to functions.
It has value semantics, not objects.
As you can see, you narrow the context and take only what you need.
You can also expand context if you need it.
As you can see, if you initialize context once or a subcontext, you do not need to pass and initialize it again, and you can use it across the app.
If needed, you can reinitialize context if you have new data.
Contexts are explicit.
Contexts have lexical scope.
Contexts are not global.
Contexts are not mutable; you need to reinitialize context if data changes.
Whether you can call a function or not is decided at compile time.
This is an attempt to take this idea from OOP languages—context—but without objects, mutations, and implicit dependencies.
You can also use context as a value.
Syntax is not final; I am just showing ideas:
you can create context, expand and narrow it, create one context from multiple contexts,
functions can require multiple contexts,
you can treat context syntactically like an object, but it is not an object in the sense that it has no state.
With context, you can also expand behavior infinitely.
You have some domain object/behavior, and with context you can add data needed for infrastructure code—for example, databases or Google Sheets.
In the examples, syntax may be inconsistent, but the main point is the idea of context.
With these ideas, Gleam is not an FP language, but a context-oriented one.
It brings together OOP and FP, but takes the best from both worlds.
Principles
1. Context as the Primary Abstraction
Context is the primary unit of program organization.
Programs are structured around contexts rather than classes or modules.
A context represents a structured environment of data and capabilities in which functions operate.
2. Requirements Instead of Dependencies
Functions declare requirements on their environment rather than accepting dependencies explicitly.
A function specifies what it needs, not how it is passed.
3. Automatic Lexical Binding
Contexts are bound automatically through lexical scope.
If a required context is available in the current lexical environment, it is used without explicit passing.
4. Structural Compatibility
Compatibility is determined structurally, not nominally.
A function can operate in any context that structurally satisfies its requirements, regardless of the context’s name or origin.
5. Minimal Context Principle
A function observes only the minimal subset of the environment it requires.
The effective context is automatically narrowed to the required structure.
6. Context Composition
Complex environments are built by composing simpler contexts.
Composition is preferred over nesting and inheritance.
7. Local Extensibility of the Environment
A context can be locally extended without modifying existing code.
New data and capabilities are introduced by constructing extended contexts rather than mutating existing structures.
8. Context as a Value
A context may be used both implicitly as an environment and explicitly as a value.
Contexts may be:
Both forms are equivalent and interoperable.
9. Explicit Effects
All side effects must be expressed through context.
Effects are not implicit; they are enabled only through explicitly required capabilities.
10. No Hidden Mutation of Context
Contexts do not allow implicit or hidden mutation.
Changes to the environment are expressed by constructing new contexts rather than mutating existing ones.
This gives contexts value semantics rather than object-internal state.
11. Flat Accessibility
Context properties are accessed directly, without navigational indirection.
A context provides locality similar to
this, but without object mutation or encapsulation of behavior.12. Zero-Cost Abstractions
Contexts introduce no runtime overhead.
All context operations are resolved and eliminated at compile time.
13. Separation of Structure and Behavior
Contexts contain only data and capabilities; functions contain only behavior.
Behavior never resides inside data.
14. Determinism
The result of a function is fully determined by its arguments and its context.
The context is part of the function’s contract.
15. Universal Applicability
The same mechanism applies uniformly to:
Paradigm Comparison
Object-Oriented Programming says:
Behavior belongs to the object.
Functional Programming says:
Behavior is a pure function.
Contextual Structural Programming says:
Behavior is a function operating within a structured context.
Contextual Structural Programming is not a mixture of OOP syntax with FP semantics.
It is a structural unification of the core ideas of both paradigms, achieved by replacing their weakest abstractions with a shared one: context.
What Is Taken from Object-Oriented Programming
1. Locality of Data
OOP idea:
Behavior operates in a local data environment (
this).In CSP:
A function operates within a context that provides local data directly.
Context provides object-like locality without objects.
2. Implicit Access to the Environment
OOP idea:
Methods implicitly access object fields.
In CSP:
Functions implicitly access context properties through lexical scope.
This preserves:
while avoiding:
3. Composition over Inheritance
OOP idea (best practice):
Prefer composition over inheritance.
In CSP:
Contexts are composed structurally.
Composition is explicit, static, and structural.
4. Encapsulation of Infrastructure
OOP idea:
Infrastructure details should not pollute domain logic.
In CSP:
Infrastructure lives in contexts, not in domain types.
What Is Taken from Functional Programming
1. Functions as the Primary Unit of Behavior
FP idea:
Behavior is expressed as functions, not methods.
In CSP:
All behavior is implemented as functions.
2. Value Semantics
FP idea:
State evolves by creating new values, not mutating existing ones.
In CSP:
Contexts have value semantics.
This prevents the implicit temporal coupling common in OOP.
3. Explicit Effects
FP idea:
Effects should be explicit in function signatures.
In CSP:
Effects are expressed as required contexts.
This replaces monads with a structural mechanism.
4. Determinism and Reasoning
FP idea:
A function’s behavior is determined by its inputs.
In CSP:
A function’s behavior is determined by:
The context is part of the function’s contract.
What CSP Explicitly Rejects from Both
From OOP:
this-based dependenciesFrom FP:
Why Context Is the Unifying Abstraction
Context absorbs the useful properties of both paradigms while eliminating their pathological cases.
One-Sentence Summary
Object-Oriented Programming attaches behavior to mutable objects.
Functional Programming separates behavior from data.
Contextual Structural Programming keeps behavior functional while restoring object-like locality through explicit, typed contexts.
This is not OOP + FP.
It is a shared abstraction that subsumes both.
Beta Was this translation helpful? Give feedback.
All reactions