hindsight-core
Copyright(c) 2024
LicenseBSD3
Maintainermaintainer@example.com
Stabilityexperimental
Safe HaskellNone
LanguageGHC2021

Hindsight.Events

Description

This module provides the core API for Hindsight's event system. It enables compile-time event versioning, automatic serialization, and safe event evolution over time.

Overview

Hindsight uses type-level programming to provide compile-time guarantees about event versioning and upgrades. Events are identified by type-level strings (Symbols) and can have multiple payload versions that evolve over time.

Quick Start

To define a single-version event:

type instance MaxVersion "user_created" = 0
type instance Versions "user_created" = '[UserCreated]

instance Event "user_created"
instance MigrateVersion 0 "user_created"  -- No method needed for single version!

Then use mkEvent to create event values:

event = mkEvent "user_created" (UserCreated userId name)

Advanced Usage: Multi-Version Events

For events with multiple versions, define consecutive upgrades using Upcast and the system automatically composes them:

-- V0 → V1 transition
instance Upcast 0 "user_created" where
  upcast v0 = V1 { newField = defaultValue, ... }

-- V1 → V2 transition
instance Upcast 1 "user_created" where
  upcast v1 = V2 { anotherField = defaultValue, ... }

-- Migration instances use automatic composition
instance MigrateVersion 0 "user_created"  -- Automatic: V0 → V1 → V2
instance MigrateVersion 1 "user_created"  -- Automatic: V1 → V2
instance MigrateVersion 2 "user_created"  -- Automatic: V2 → V2 (identity)

For detailed examples, see the tutorial documentation in the hindsight-tutorials package.

Type Aliases for Clarity

This module provides several type aliases to make complex type signatures more readable:

These are particularly useful when reading type errors or writing advanced code.

Synopsis

Event Definition

Core types for defining and working with events.

class EventConstraints event => Event (event :: Symbol) Source #

Core type class for versioned events.

This is the main constraint you'll use when working with events. It automatically includes all necessary constraints via EventConstraints.

To define an event, create instances of MaxVersion, Versions, Event, and MigrateVersion:

type instance MaxVersion "user_created" = 0
type instance Versions "user_created" = '[UserCreated]

instance Event "user_created"
instance MigrateVersion 0 "user_created"  -- Automatic migration (identity for v0)

type EventConstraints (event :: Symbol) = (AssertVersionCountMatches event, KnownSymbol event, Typeable event, ToJSON (CurrentPayloadType event), FullVersionRange event, ReifiablePeanoNat (ToPeanoNat (MaxVersion event))) Source #

Complete set of constraints required for an event type.

This type alias bundles all the low-level constraints needed for event processing:

  • AssertVersionCountMatches - Validate MaxVersion matches Versions list length
  • KnownSymbol - Access event name as runtime value
  • Typeable - Runtime type information
  • ToJSON - Serialize current version payloads
  • FullVersionRange - Compile-time evidence for all version constraints
  • ReifiablePeanoNat - Convert type-level version numbers to runtime values

You don't typically need to use this directly; just use the Event constraint. This is exported for documentation purposes so you can see what constraints are actually required.

data SomeLatestEvent Source #

Existential wrapper for an event at its latest version.

This packages up an event name (as a Proxy) with its current payload, hiding the specific event type. Useful for heterogeneous collections of events.

Create values using mkEvent.

Constructors

Event event => SomeLatestEvent 

mkEvent Source #

Arguments

:: forall (event :: Symbol) -> Event event 
=> CurrentPayloadType event

Event payload at current version

-> SomeLatestEvent

Wrapped event with type information

Smart constructor for creating events using RequiredTypeArguments.

This provides a convenient syntax for creating events:

event = mkEvent "user_created" (UserCreated userId name)

This is equivalent to:

event = SomeLatestEvent (Proxy @"user_created") (UserCreated userId name)

Event Names

getEventName Source #

Arguments

:: forall (event :: Symbol). KnownSymbol event 
=> Proxy event

Proxy for the event type

-> Text

Event name as Text

Get the event name as Text from a Symbol.

This converts a type-level event name to a runtime value, useful for logging, debugging, and serialization.

getEventName (Proxy @"user_created") = "user_created"

Event Versioning

Type families and aliases for declaring event versions.

Use MaxVersion to declare the latest version number, and Versions to specify the payload types for each version.

type family MaxVersion (event :: Symbol) :: Nat Source #

Declare the maximum version number for an event.

Version numbers start at 0. For a single-version event, use MaxVersion = 0. For multi-version events, increment this as you add versions.

type instance MaxVersion "user_created" = 0  -- Single version
type instance MaxVersion "order_placed" = 2  -- Three versions (0, 1, 2)

type family Versions (event :: Symbol) :: [Type] Source #

Declare the payload types for each version of an event.

Use a type-level list to specify all payload versions in order:

-- Single version:
type instance Versions "user_created" = '[UserCreated]

-- Multiple versions:
type instance Versions "order_placed" = '[OrderPlacedV0, OrderPlacedV1, OrderPlacedV2]

The list length must match MaxVersion + 1. This constraint is enforced at compile time through the version evidence system.

type CurrentPayloadType (event :: Symbol) = FinalVersionType (EventVersionVector event) Source #

Get the current (latest) payload type for an event.

This extracts the final type from the version vector:

CurrentPayloadType "user_created" = UserCreated
CurrentPayloadType "order_placed"  = OrderPlacedV2  -- if MaxVersion = 2

Clarity Aliases

These aliases make type signatures more readable.

type EventVersionCount (event :: Symbol) = 'PeanoSucc (ToPeanoNat (MaxVersion event)) Source #

The number of versions for an event (as a Peano number).

This is MaxVersion + 1 in Peano encoding. For an event with MaxVersion = 2, this expands to PeanoSucc (PeanoSucc (PeanoSucc PeanoZero)).

This alias makes type signatures more readable when working with version indices and constraints.

type family EventVersionVector (event :: Symbol) :: EventVersions 'PeanoZero (EventVersionCount event) where ... Source #

The full version vector type for an event.

This type family represents the complete EventVersions structure from version 0 to MaxVersion. This is computed from the Versions list using FromList.

Example expansion:

EventVersionVector "user_created"
  = FromList (Versions "user_created")
  = FromList '[UserCreated]
  = EventVersions 'PeanoZero ('PeanoSucc 'PeanoZero)

Note: This is an internal type family. Users should work with Versions which uses the simpler list syntax.

type FullVersionRange (event :: Symbol) = HasEvidenceList 'PeanoZero (EventVersionCount event) event ValidPayloadForVersion (EventVersionVector event) Source #

Evidence that all version payloads satisfy the required constraints.

This is the main "proof obligation" checked by EventConstraints. It ensures that every payload version can be serialized, deserialized, and upgraded to the latest version.

Using this alias makes EventConstraints much more readable:

type EventConstraints event =
  ( ...
  , FullVersionRange event  -- instead of massive HasEvidenceList expression
  , ...
  )

Internal Version Machinery

Low-level types for the version system.

Most users won't need these directly. The EventVersions GADT and FromList type family are used internally to convert the simple list syntax from Versions into the indexed type structure.

data EventVersions (startsAt :: PeanoNat) (finalCount :: PeanoNat) where Source #

Type-level vector of versioned payload types.

This GADT represents a non-empty list of types, indexed by Peano numbers. The indices track the "start" and "end" positions in the version sequence.

  • startsAt - The version number where this vector begins
  • finalCount - One past the last version (i.e., length when startsAt = 0)

Constructors:

  • Final - A single-element vector (the last/only version)
  • Then - Prepend a type to an existing vector (earlier versions)

Example: A vector of 3 versions (0, 1, 2):

Then PayloadV0 (Then PayloadV1 (Final PayloadV2))
  :: EventVersions PeanoZero (PeanoSucc (PeanoSucc (PeanoSucc PeanoZero)))

Constructors

Final :: forall (startsAt :: PeanoNat). Type -> EventVersions startsAt ('PeanoSucc startsAt)

Final version in the vector

Then :: forall (startsAt :: PeanoNat) (finalCount :: PeanoNat). Type -> EventVersions ('PeanoSucc startsAt) finalCount -> EventVersions startsAt finalCount

Prepend an earlier version

type family FromList (payloadList :: [Type]) :: k where ... Source #

Convert a type-level list to an EventVersions GADT

This has a polymorphic kind to allow recursive usage at different indices. The result kind is constrained at usage sites via EventVersionVector.

Equations

FromList '[a] = 'Final a :: EventVersions startsAt ('PeanoSucc startsAt) 
FromList (a ': rest) = 'Then a (FromList rest :: EventVersions ('PeanoSucc startsAt) finalCount) 

Upgrade System

Types and classes for upgrading old event versions to the latest.

type family FinalVersionType (vec :: EventVersions startsAt finalCount) where ... Source #

Extract the final (most recent) type from a version vector.

This traverses the vector structure to find the Final constructor.

Equations

FinalVersionType ('Final t :: EventVersions startsAt ('PeanoSucc startsAt)) = t 
FinalVersionType ('Then t rest :: EventVersions startsAt finalCount) = FinalVersionType rest 

type PayloadVersion (event :: Symbol) (n :: PeanoNat) = PayloadAtVersion n (EventVersionVector event) Source #

Get the payload type at a specific version number.

This allows you to reference older payload versions in upgrade logic:

instance Upcast 0 "order_placed" where
  upcast :: PayloadVersion "order_placed" 0 -> PayloadVersion "order_placed" 1
  upcast v0 = ... -- upgrade OrderPlacedV0 to OrderPlacedV1

type family PayloadAtVersion (idx :: PeanoNat) (vec :: EventVersions startsAt finalCount) where ... Source #

Extract the type at a specific version index.

Returns a compile error if the index is out of bounds.

Equations

PayloadAtVersion idx ('Final t :: EventVersions startsAt ('PeanoSucc startsAt)) = PeanoEqual idx startsAt t (TypeError ('Text "Version index out of bounds") :: Type) 
PayloadAtVersion idx ('Then t rest :: EventVersions startsAt finalCount) = PeanoEqual idx startsAt t (PayloadAtVersion idx rest) 

Consecutive Upcast API

API for version migrations using consecutive upgrades.

Define one Upcast instance per version transition, and MigrateVersion instances are automatically composed.

class Upcast (ver :: Nat) (event :: Symbol) where Source #

Upgrade a payload from version ver to version ver + 1.

This class represents a single consecutive upgrade step. You define one instance for each version transition:

-- Upgrade from V0 to V1
instance Upcast 0 MyEvent where
  upcast v0 = V1 { newField = defaultValue, ... }

-- Upgrade from V1 to V2
instance Upcast 1 MyEvent where
  upcast v1 = V2 { anotherField = defaultValue, ... }

These consecutive upgrades are automatically composed to provide migrations from any old version to the latest via MigrateVersion.

Methods

upcast :: PayloadAtVersion (ToPeanoNat ver) (EventVersionVector event) -> PayloadAtVersion (ToPeanoNat (ver + 1)) (EventVersionVector event) Source #

Upgrade from version ver to version ver + 1

Instances

Instances details
(TypeError (((((((((('Text "Missing Upcast instance for version " ':<>: 'ShowType ver) ':<>: 'Text " of event \"") ':<>: 'Text event) ':<>: 'Text "\"") ':$$: 'Text "") ':$$: 'Text "You need to define:") ':$$: (((('Text " instance Upcast " ':<>: 'ShowType ver) ':<>: 'Text " \"") ':<>: 'Text event) ':<>: 'Text "\" where")) ':$$: (('Text " upcast v" ':<>: 'ShowType ver) ':<>: 'Text " = ...")) ':$$: 'Text "") ':$$: ((('Text "This upgrades from version " ':<>: 'ShowType ver) ':<>: 'Text " to version ") ':<>: 'ShowType (ver + 1))) :: Constraint) => Upcast ver event Source #

Provide a helpful error message when an Upcast instance is missing.

This overlappable instance ensures that if you forget to define an Upcast instance for a version, you get a clear error message instead of cryptic constraint errors.

Instance details

Defined in Hindsight.Events

class MigrateVersion (ver :: Nat) (event :: Symbol) where Source #

Migrate a payload from any version to the latest version.

You must declare an instance for each version, but the method body is optional (uses automatic consecutive composition by default):

-- Automatic consecutive composition (V0 → V1 → V2)
instance MigrateVersion 0 MyEvent

-- Also automatic (V1 → V2)
instance MigrateVersion 1 MyEvent

-- Latest version (identity)
instance MigrateVersion 2 MyEvent

-- Override for direct upgrade (if needed)
instance MigrateVersion 0 MyEvent where
  migrateVersion v0 = V2 { ... }  -- Skip V1 if it loses information

Minimal complete definition

Nothing

Methods

migrateVersion :: PayloadAtVersion (ToPeanoNat ver) (EventVersionVector event) -> CurrentPayloadType event Source #

Migrate from version ver to the latest version

default migrateVersion :: ConsecutiveUpcast (IsLatest (ToPeanoNat ver) event) (ToPeanoNat ver) event => PayloadAtVersion (ToPeanoNat ver) (EventVersionVector event) -> CurrentPayloadType event Source #

Default implementation: automatically compose consecutive upgrades

This delegates to ConsecutiveUpcast which handles both the latest version (identity) and non-latest versions (composition) cases.

type family MaxVersionPeano (event :: Symbol) :: PeanoNat where ... Source #

Convert MaxVersion to Peano representation for type-level computation.

This is used internally by the consecutive upcast machinery to determine when a version is the latest.

Equations

MaxVersionPeano event = ToPeanoNat (MaxVersion event) 

Serialization

Type constraints for JSON serialization.

type Serializable a = (Show a, Eq a, FromJSON a, ToJSON a) Source #

Basic type constraints required for event payloads.

All payload types must be serializable to JSON for storage and transmission.

Parsing Utilities

Functions typically used by store implementations for deserializing events from storage.

parseMap Source #

Arguments

:: forall (event :: Symbol). Event event 
=> Map Int (Value -> Parser (CurrentPayloadType event))

Map from version to parser

Build a version-aware parser map for an event.

Creates a map from version numbers to parsers that can deserialize event payloads at any version and automatically upgrade them to the latest.

This is used internally by store implementations when reading events from storage.

parseMap @"order_placed"
  = Map.fromList
      [ (0, parser that reads OrderPlacedV0 and upgrades to V2)
      , (1, parser that reads OrderPlacedV1 and upgrades to V2)
      , (2, parser that reads OrderPlacedV2 directly)
      ]

parseMapFromProxy Source #

Arguments

:: forall (event :: Symbol). Event event 
=> Proxy event

Proxy for the event type

-> Map Int (Value -> Parser (CurrentPayloadType event))

Map from version to parser

Convenience wrapper around parseMap that accepts a proxy argument.

Some contexts require explicit type application or proxy arguments. This function provides compatibility with such APIs.

getMaxVersion Source #

Arguments

:: forall (event :: Symbol). Event event 
=> Proxy event

Proxy for the event type

-> Integer

Maximum version number

Get the maximum version number for an event as a runtime integer.

Useful for debugging, logging, or runtime version checks:

getMaxVersion (Proxy @"order_placed") = 2  -- if MaxVersion "order_placed" = 2

Advanced Type-Level Utilities

These are primarily for internal use or advanced scenarios.

Most users won't need to reference these directly. They're exported for documentation purposes and for library implementors.

Peano Numbers

data PeanoNat Source #

Type-level Peano natural numbers.

These provide an alternative representation to GHC's Nat that's more suitable for structural recursion and pattern matching at the type level.

0 = PeanoZero
1 = PeanoSucc PeanoZero
2 = PeanoSucc (PeanoSucc PeanoZero)
...

class ReifiablePeanoNat (n :: PeanoNat) where Source #

Convert type-level Peano numbers to runtime integers.

This class allows us to "reify" type-level numbers into runtime values, which is necessary for parsing version numbers from JSON and other runtime operations.

Methods

reifyPeanoNat :: Integer Source #

Get the runtime integer value of a Peano number

type family ToPeanoNat (n :: Nat) :: PeanoNat where ... Source #

Convert GHC's Nat to Peano representation.

This is needed because type families and data families use Nat, but we need PeanoNat for structural pattern matching.

Equations

ToPeanoNat 0 = 'PeanoZero 
ToPeanoNat n = 'PeanoSucc (ToPeanoNat (n - 1)) 

type family FromPeanoNat (n :: PeanoNat) :: Nat where ... Source #

Convert Peano representation back to GHC's Nat.

This is the inverse of ToPeanoNat and is needed for some constraint manipulations where GHC requires Nat.

Constraint Evidence

data Dict c where Source #

Dictionary carrying constraint evidence.

This allows us to package up constraints and manipulate them at runtime. It's particularly useful for passing around evidence that certain type-level properties hold.

Example use: storing evidence that a payload satisfies serialization constraints so we can retrieve it later when needed.

Constructors

Dict :: forall c. c => Dict c 

data VersionConstraints (ts :: EventVersions startsAt finalCount) (c :: PeanoNat -> Type -> Constraint) where Source #

Evidence that each version in a vector satisfies some constraint.

This GADT packages up constraint evidence for all elements in an EventVersions vector. It's structurally similar to the vector itself:

The constraint c is indexed by version number and payload type:

c :: PeanoNat -> Type -> Constraint

Example: Prove all versions are serializable:

class (ToJSON payload, FromJSON payload) => Serializable (idx :: PeanoNat) payload
...
evidence :: VersionConstraints myVersions Serializable

Constructors

VersionConstraintsLast :: forall (c :: PeanoNat -> Type -> Constraint) (startsAt :: PeanoNat) t. c startsAt t => (Proxy startsAt, Proxy t) -> VersionConstraints ('Final t :: EventVersions startsAt ('PeanoSucc startsAt)) c

Evidence for a single-element vector

VersionConstraintsCons :: forall (c :: PeanoNat -> Type -> Constraint) (startsAt :: PeanoNat) t (finalCount :: PeanoNat) (ts' :: EventVersions ('PeanoSucc startsAt) finalCount). c startsAt t => (Proxy startsAt, Proxy t) -> VersionConstraints ts' c -> VersionConstraints ('Then t ts') c

Evidence for head + inductive evidence for tail

class VersionPayloadRequirements event idx payload => ValidPayloadForVersion (event :: Symbol) (idx :: PeanoNat) payload where Source #

Evidence that a type is a valid payload for a version.

This class packages up VersionPayloadRequirements into a Dict that can be passed around at runtime. Used internally by the parsing machinery.

Instances

Instances details
VersionPayloadRequirements event idx payload => ValidPayloadForVersion event idx payload Source # 
Instance details

Defined in Hindsight.Events

class HasEvidenceList (startsAt :: PeanoNat) (finalCount :: PeanoNat) (event :: k) (c :: k -> PeanoNat -> Type -> Constraint) (vec :: EventVersions startsAt finalCount) Source #

Build constraint evidence for all elements in a version vector.

This class provides a way to automatically derive VersionConstraints evidence given:

  1. Evidence that each individual version satisfies the constraint c
  2. The structure of the version vector

The instances mirror the structure of EventVersions:

  • Base case: single-element vector (Final)
  • Inductive case: multi-element vector (Then)

Usage:

getEvidenceList :: VersionConstraints myVersions MyConstraint

Minimal complete definition

getEvidenceList

Instances

Instances details
(c event startsAt payload, HasEvidenceList ('PeanoSucc startsAt) finalCount event c ts) => HasEvidenceList startsAt finalCount (event :: k) (c :: k -> PeanoNat -> Type -> Constraint) ('Then payload ts :: EventVersions startsAt finalCount) Source #

Inductive case: Evidence for multi-version vector

Instance details

Defined in Hindsight.Events.Internal.Versioning

Methods

getEvidenceList :: VersionConstraints ('Then payload ts) (c event) Source #

c event startsAt payload => HasEvidenceList startsAt ('PeanoSucc startsAt) (event :: k) (c :: k -> PeanoNat -> Type -> Constraint) ('Final payload :: EventVersions startsAt ('PeanoSucc startsAt)) Source #

Base case: Evidence for a single-version vector

Instance details

Defined in Hindsight.Events.Internal.Versioning

Methods

getEvidenceList :: VersionConstraints ('Final payload :: EventVersions startsAt ('PeanoSucc startsAt)) (c event) Source #

type HasFullEvidenceList (event :: Symbol) (c :: Symbol -> PeanoNat -> Type -> Constraint) = HasEvidenceList 'PeanoZero (EventVersionCount event) event c (EventVersionVector event) Source #

Convenience alias for full version range evidence.

This is used in EventConstraints via the FullVersionRange alias.

getPayloadEvidence :: forall (event :: Symbol) (c :: Symbol -> PeanoNat -> Type -> Constraint). HasFullEvidenceList event c => VersionConstraints (EventVersionVector event) (c event) Source #

Extract constraint evidence for all versions of an event.

Used internally by parsing and serialization machinery.

Type-Level Utilities

type VersionPayloadRequirements (event :: Symbol) (idx :: PeanoNat) payload = (Serializable payload, MigrateVersion (FromPeanoNat idx) event, PayloadVersion event (ToPeanoNat (FromPeanoNat idx)) ~ PayloadVersion event idx, KnownSymbol event, ReifiablePeanoNat idx, Typeable payload, Typeable event, Typeable idx, payload ~ PayloadVersion event idx) Source #

Core constraints required for event payloads at a specific version.

This bundles together all the requirements for a payload type at a given version index. You typically won't use this directly.

type family PeanoEqual (a :: PeanoNat) (b :: PeanoNat) (thenResult :: result) (elseResult :: result) :: result where ... Source #

Type-level equality check with conditional results.

Returns thenResult if a and b are equal, elseResult otherwise.

This is used for version number matching.

PeanoEqual PeanoZero PeanoZero "yes" "no" = "yes"
PeanoEqual (PeanoSucc PeanoZero) PeanoZero "yes" "no" = "no"

Equations

PeanoEqual n n (thenResult :: result) (_1 :: result) = thenResult 
PeanoEqual n m (_1 :: result) (elseResult :: result) = elseResult