Purity

Levels of purity

Every type in crow has a purity. The purpose is to control what can be shared between different contexts that may be running in parallel. The precise details will be explained in Fibers.

There are 3 levels or purity:

data Immutable values only.
shared All data, plus values that are mutable in a thread-safe way.
mut Allows all types, including ordinary mutable values.

data is stricter than "read-only"; it only allows values that are guaranteed to never change. You can't cast mutable data to immutable data, but you can usually call to.

Declaring purity

Types are assumed data by default. If not, they must be marked shared or mut at the declaration.

main void() () has-data record(x string) has-shared record(x string shared[]) shared has-mutable record(x string mut[]) mut

Benefits of purity

Purity is deep: If a record has a mut field, it must be mut itself.
To put it in reverse, if a record is data, then everything it references, and everything they reference, is data too.

Two other Crow features also enforce kinds of purity:

  • Global state is considered unsafe. (It may be used, but code should behave as if there is no global state.)
  • For non-summon functions, the ability to do I/O must come from a parameter. This will be explained in I/O.

With these combine, purity creates a "jail" that a (non-summon) function can't escape from. Its parameters are the roots of the graph of all data it can access.
If those parameters are data, then the entire graph of objects accessible to that function is data.
It can't modify any state visible to the outside, or do any I/O. It's limited to reading data and producing a return value. The only thing that matters about such a function is its return value.
It can still create mutable objects of its own, but this is fine since they won't affect anything elsewhere.

Even if some parameter is shared or mut, the function can only access whatever state or side effects are exposed through that parameter.