Argus.Purity.Effects (Panoptes v0.13.0)

Copy Markdown View Source

The effect model: which calls are observable effects, and which are known to be free of them.

A purity claim is a claim about every execution, so the analysis cannot simply assume that a call it knows nothing about is harmless. Every real purity checker ships an effect model of its standard library — Haskell encodes it in types, effect-typed languages in rows; on the BEAM it has to be a table, because the runtime offers no way to ask.

So calls land in one of three buckets:

  • impure — a known observable effect. Names the category, so the report can say what the effect is rather than just that there is one.
  • pure — known to compute a value and nothing else.
  • unknown — neither. The analysis reports these as unprovable rather than guessing, which is the difference between a verification and an opinion.

That third bucket is the honest part. Assuming unknown calls are pure would make the checker report success far more often and mean nothing; assuming they are impure would flag every real program. Saying "I cannot see past this" is the only answer that keeps a "verified" worth having.

Two dimensions, not one

Every impure call also carries a mode: :read or :write.

Purity cares about neither — reading a clock or the process dictionary already breaks referential transparency, so both modes are equally disqualifying. Other contracts care a great deal. Inside a database transaction, Application.get_env/2 is harmless and HTTPoison.post/2 is an incident: one has nothing to undo, the other has already left the machine. A model with only "is this an effect" cannot tell them apart and would report every config read as a transaction hazard.

Mode defaults to :write, so a call nobody has classified is assumed to change something. That is the safe direction: a false "this is irreversible" costs a look, a false "this is harmless" costs the bug.

Scope

Effects here are observable: something outside the function can tell they happened. Allocation, arithmetic, matching and exceptions are not effects — a function that raises still computes a value or fails and leaves nothing behind.

Summary

Types

Whether an effect changes anything a rollback or a retry would care about.

Functions

Every impure category, for exhaustiveness checks and reporting.

Classify a remote call.

Whether a call changes anything, or merely observes it.

Modules whose calls dispatch to an open set of implementations.

Modules treated as free of observable effects.

Types

category()

@type category() ::
  :io
  | :process
  | :process_dict
  | :ets
  | :port
  | :node
  | :time
  | :random
  | :network
  | :code_loading
  | :logging

mode()

@type mode() :: :read | :write

Whether an effect changes anything a rollback or a retry would care about.

Defaults to :write for anything unlisted, because assuming an unknown effect is harmless is the expensive mistake.

verdict()

@type verdict() ::
  {:impure, category(), mode()}
  | :pure
  | {:opaque, :protocol | :dot_dispatch}
  | :unknown

Functions

categories()

@spec categories() :: [category()]

Every impure category, for exhaustiveness checks and reporting.

classify(module, function)

@spec classify(String.t(), String.t()) :: verdict()

Classify a remote call.

iex> Argus.Purity.Effects.classify("IO", "puts")
{:impure, :io, :write}

iex> Argus.Purity.Effects.classify("Application", "get_env")
{:impure, :process, :read}

iex> Argus.Purity.Effects.classify(":erlang", "+")
:pure

iex> Argus.Purity.Effects.classify(":erlang", "put")
{:impure, :process_dict, :write}

iex> Argus.Purity.Effects.classify("String.Chars", "to_string")
{:opaque, :protocol}

iex> Argus.Purity.Effects.classify("MyApp.Repo", "all")
:unknown

mode(module, function)

@spec mode(String.t(), String.t()) :: mode()

Whether a call changes anything, or merely observes it.

iex> Argus.Purity.Effects.mode("File", "read")
:read

iex> Argus.Purity.Effects.mode("File", "write")
:write

protocol_modules()

@spec protocol_modules() :: [String.t()]

Modules whose calls dispatch to an open set of implementations.

pure_modules()

@spec pure_modules() :: [String.t()]

Modules treated as free of observable effects.