The M Paradigm
Why M is a functional language, what immutability means in practice, and how this shapes the way you write queries.
Power Query M is a functional language — closer in design to F# or Haskell than to VBA, Python, or SQL. Understanding its paradigm explains why the language works the way it does and prevents entire categories of bugs.
Functional, Not Imperative
In imperative languages (VBA, Python, C#), you write step-by-step instructions that modify state:
// Pseudocode (imperative)
x = 5
x = x + 1 // x is now 6In M, this is impossible. Variables are immutable — once a value is assigned, it cannot change:
let
x = 5,
y = x + 1 // y is 6, x is still 5
in
yYou do not mutate x. Instead, you create a new variable y that holds the new value. Every transformation in M works this way: you produce new values rather than modifying existing ones.
Every Expression Returns a Value
In M, everything is an expression that returns a value. There are no statements, loops, or void functions. Even if...then...else returns a value, and every step in a let block is an expression that evaluates to something.
This is why let steps are connected by commas rather than semicolons — they are a list of value bindings, not a sequence of actions.
No Loops
M has no for, while, or do loops. Iteration is achieved through functions that operate on collections:
// Transform every item in a list (replaces a for loop)
List.Transform({1, 2, 3}, each _ * 10)
// Result: {10, 20, 30}
// Accumulate a running total (replaces a while loop)
List.Accumulate({1, 2, 3, 4}, 0, (state, current) => state + current)
// Result: 10For more complex iteration, List.Generate creates sequences by repeatedly applying a function until a condition is met:
List.Generate(
() => 1, // initial value
each _ <= 10, // keep going while true
each _ * 2 // next value
)
// Result: {1, 2, 4, 8}Pure Functions
Most M functions are pure: given the same inputs, they always produce the same output, and they have no side effects. This purity is what allows the engine to optimize your queries through lazy evaluation and query folding.
The main exception is functions that read external data (Web.Contents, Sql.Database, File.Contents), which depend on external state.
Lazy Evaluation
Because M is functional and (mostly) pure, the engine can evaluate expressions in any order. It chooses to use lazy evaluation — expressions are not computed until their results are actually needed. This is covered in detail on the Lazy Evaluation concept page.
The practical implication: step order in let does not determine execution order. The engine follows the dependency graph, not the textual position of your steps.
Why This Matters
Understanding M's paradigm helps you:
- Stop looking for loops. Use
List.Transform,List.Accumulate,List.Generate, andTable.TransformColumnsinstead. - Stop trying to reassign variables. Create new names for new values.
- Understand why step order is flexible. The engine evaluates on demand, not top-to-bottom.
- Write foldable queries. Pure, declarative transformations are easier for the engine to push to the data source.
- Debug effectively. Since variables are immutable, you can inspect any step without worrying that a later step changed it.
Best Practices
- Embrace transformation, not mutation. Instead of modifying a table in place, think of each step as producing a new table derived from the previous one.
- Use collection functions (
List.Transform,Table.TransformColumns) instead of trying to write loops. - Use
List.Accumulatewhen you need to build up a result by processing items one at a time — it is M's equivalent of a reduce/fold operation. - Keep functions pure wherever possible. Avoid depending on external state inside transformation logic.