What you're asking for is a type system for expressions that classify not only the value of the expression, but also how it's calculated — in particular, you want to have a class of expressions that is guaranteed not to have side effects when evaluated. The general framework for that is an effect system. There are two other popular ways to model purity: monads as in Haskell and linear types as in Clean. I think both can be modeled as effect systems, although not in a very convenient way. In this answer, I'll focus on effect systems, which are easiest to retrofit to an existing language.
If the following presentation seems a bit dry to you, I invite you to read a much longer answer I wrote on a similar topic.
The general idea of an effect system is that for each expression, we want the type checker to keep track of its possible side effects, in the same way that the type checker keeps track of the possible values through the expression's type (in the classical sense). To a first approximation, a value type designates a set of potential values, and an effect type designates a set of potential side effects. For example, read_int(f) + 1
is an expression that returns an integer and whose effect is to read from the file f
. print_int(f, read_int(f) + 1)
is an expression that returns nothing and whose effect is to read and write to the file f
.
Classically, the type of a function indicates the type of its parameters and the type of its return value. With an effect system, the type of a function indicates the possible side effects when the function is evaluated. That is, if $f : A \to_{e} B$ ($f$ takes a parameter of type $A$, returns a value of type $B$, and has the effect $e$ when applied), then $f(x) :_{e} B$ (the expression $f(x)$ has the type $B$ and the effect $e$, assuming that $x$ has the type $A$). In this manner, the effect type system becomes embedded in the value type system.
Just like with data types, effect types can be more or less precise. For example, you can have a data type system that keeps track of every single potential value that an expression can have, but that very quickly becomes undecidable, so typical programming languages have a type integer
but not a type ${0, 1, 2, 4, 8}$. Likewise, in an effect system, you're going to need to make a compromise between precision and decidability, and this compromise is going to depend on how much you expect programmers to annotate their program.
A very simple effect system is to have just two effect types: pure or impure. Any side effect makes an expression impure, and almost anything built from an impure expression is impure. A notable exception to that last statement is that a function abstraction is pure: $(\lambda x. M)$ itself is a pure expression, the effects of $M$ are “hidden” until the function is applied. An example of this in a mainstream language is constexpr
in C++, which is an annotation indicating that a piece of code is pure. constexpr
on a function means that the function's body is pure.
Another example of an effect system in a mainstream language is the annotations on Java methods that indicate which expressions the method's body can throw. A catch
on an exception removes that exception from the expression's potential effects.
If you want to be able to do things like modify a local variable in a function, but have the function still be considered pure, you're going to need a fancier effect system. For an assignment $v := E$, you need to keep track of which variable $v$ is modified, so that the effect “access the mutable variable $v$” can be limited to the scope of $v$. This can get hairy really quickly, since you need to keep track of aliasing, indirect references, etc. The Haskell approach that forces the programmer to explicitly use a monad for any state, even internal state, makes this tracking considerably easier.
IO
monad. You can do that. Or are you asking for, given anIO
action, a way to verify whether it's pure? $\endgroup$IO
, there are also monads for internal side effects likeST
.) That's one way to achieve exactly what you're asking for. $\endgroup$