Elfie

Documentation / Traits

Traits

A trait packages members, criteria, and rules so that many entities can share them. Traits are how Elfie composes behavior, and how targets, frameworks, and team conventions reach your code.

Declaring and applying

def/traits.lfy
trait timestamped: `Records when it was made and last changed` {
  $createdAt: `When it was first saved` = string;
  $updatedAt: `When it was last saved` = string;
  where (`It is saved for the first time`) -> `[[timestamped.createdAt]] and [[timestamped.updatedAt]] are the same moment`;
}

d Note is timestamped: `A short note a user writes` {
  $text: `What the note says` = string;
}

Applying a trait with is runs its body against the entity:

  • members declared with $ join the entity's scope, so Note now has createdAt and updatedAt
  • value setters such as .provides = [] become the entity's values
  • criteria and where rules join the entity's criteria, with the trait recorded as their contributor

Apply several traits with commas: d Note is audited, owned(User).

Parameters

def/traits.lfy
/// owner: the data that owns it
trait owned(owner: Data): `Belongs to exactly one [[&owner]]` {
  $ownerId: `The id of its owner` = string;
  where (`Its owner is deleted`) -> `It is deleted with it`;
}

Arguments are evaluated when the trait is applied. Use parameters in members, setters, where rules, or criteria. A parameter that appears only in a description is a defect, because nothing checks it. [[&owner]] refers to the entity passed in, so owned(User) reads Belongs to exactly one User.

Passing too many or too few arguments is reported at the application.

Inheritance

def/traits.lfy
trait persisted: `Kept in the store and read back unchanged` {
  where (`An entity carrying it is written`) -> `Reading it back gives an equal value`;
}

trait audited extends persisted, timestamped: `Every change to it is recorded` {
  where (`It changes`) -> `One audit record names {{@identifier}} and what changed`;
}

Applying audited applies persisted and timestamped first, up the whole chain. A trait reached more than once for one entity, through several paths with equal arguments, is applied once.

Rules for other entities

A with block inside a trait attaches its criteria to another entity, once for each entity the trait is applied to:

def/site.lfy
trait slugged: `Reachable at a URL friendly slug` {
  with Page {
    where (`A [[Page]] is built for {{@identifier}}`) -> `Its path ends in a slug of the title`;
  }
}

Applying from outside

apply puts a trait on an entity, a list of entities, the current file, or the whole program.

Elfie
timestamped.apply(Draft);
timestamped.apply([Draft, Note]);
persisted.apply(@);
logged.apply(global);

At the top level of a file, @ is the file itself, so persisted.apply(@) applies the trait to every entity the file declares. global stands for every entity of every file. See Global properties.

Looking traits up

ExpressionGives
persisted@entitiesEverything the trait was applied to, directly or by inheritance.
persisted@extendersEvery trait that extends it.
Note@traitsEvery trait applied to Note, in application order.

Traits as types

(is trait) is the type of any value whose type carries the trait.

def/cards.lfy
/// content: what the card shows
fn Card(content: (is persisted)): `Shows one stored thing` => string {
  @acceptanceCriteria.add({ behavior = `Shows every member of [[content]]` });
}

This site uses the same pattern: fn Button(content: (is component)) takes any React component.

Traits from packages

Packages connect your code to a target through traits. A framework in your target says how each one is built.

TraitPackageMakes an fn into
page(path)httpA page at a URL.
endpoint(method, path)httpOne operation of a JSON API.
componentreactA React component.
command(name)cliA subcommand, or the program itself when name is empty.
def/documentation/main.lfy
fn Documentation() is page('/docs'), documentationPage {}

Naming

DoDon't
Name traits so X is trait reads as a property: persisted, owned(User), triedBefore(other).Name traits as verbs or commands: persists, binds, uses.
Make marker traits parameterless.Add a parameter that every application passes the same value to.
Apply a trait once per entity.Apply the same trait twice to one entity.

Was this guide clear?

Share a suggestion