Morphity provides a trait-based polymorphism system for JavaScript using slots, traits, and automatic dependency resolution.
A container holds the runtime context for your traits and slots:
import { createContainer } from 'jsr:@justbyitself/morphity'
const container = createContainer()Slots are polymorphic functions that can have different implementations for different types:
import { addSlot } from 'jsr:@justbyitself/morphity'
const draw = addSlot(container)
const onClick = addSlot(container)Traits define implementations for slots. Use defineTrait to create traits declaratively:
import { defineTrait } from 'jsr:@justbyitself/morphity'
// Predicate-based trait (matches on value type)
defineTrait({
requires: value => Array.isArray(value),
provides: [[draw, proxy => `Drawing array with ${proxy.length} items`]]
})(container)
// Slot-based trait (requires other slots)
defineTrait({
requires: [toIterable], // needs toIterable slot
provides: [[map, proxy => fn => {
const result = []
for (const item of toIterable(proxy)) result.push(fn(item))
return result
}]]
})(container)const container = createContainer()
const greet = addSlot(container)
// Define trait for strings
defineTrait({
requires: value => typeof value === 'string',
provides: [[greet, proxy => `Hello, ${proxy}!`]]
})(container)
// Define trait for objects
defineTrait({
requires: value => typeof value === 'object' && value.name,
provides: [[greet, proxy => `Hello, ${proxy.name}!`]]
})(container)
// Auto-applies correct trait
greet("Alice") // "Hello, Alice!"
greet({ name: "Bob" }) // "Hello, Bob!"Build complex behaviors by composing traits:
const container = createContainer()
const toIterable = addSlot(container)
const map = addSlot(container)
const filter = addSlot(container)
// Base trait: arrays are iterable
defineTrait({
requires: value => Array.isArray(value),
provides: [[toIterable, proxy => proxy]]
})(container)
// Enumerable trait: requires toIterable, provides map and filter
defineTrait({
requires: [toIterable],
provides: [
[map, proxy => fn => {
const result = []
for (const item of toIterable(proxy)) result.push(fn(item))
return result
}],
[filter, proxy => pred => {
const result = []
for (const item of toIterable(proxy)) {
if (pred(item)) result.push(item)
}
return result
}]
]
})(container)
// Use composed behavior
const doubled = map([1, 2, 3])(x => x * 2) // [2, 4, 6]
const evens = filter([1, 2, 3, 4])(x => x % 2 === 0) // [2, 4]Traits automatically apply based on:
- Predicate matching: When a value matches a predicate (e.g.,
Array.isArray) - Slot dependencies: When required slots become available
// When you call map([1,2,3]), morphity:
// 1. Detects [1,2,3] is an array
// 2. Applies arrayIterableTrait (provides toIterable)
// 3. Detects toIterable is now available
// 4. Applies enumerableTrait (provides map)
// 5. Executes mapThis happens lazily - traits only apply when you use a slot.
Morphity slots are data-first by nature. For data-last pipelines, wrap the slot manually:
const map = addSlot(container)
defineTrait({
requires: value => Array.isArray(value),
provides: [[map, proxy => fn => proxy.map(fn)]]
})(container)
// Data-last wrapper
const mapL = fn => data => map(data)(fn)
mapL(x => x * 2)([1, 2, 3]) // [2, 4, 6]Traits can require multiple slots:
const toIterable = addSlot(container)
const compare = addSlot(container)
const sort = addSlot(container)
// Provides toIterable
defineTrait({
requires: value => Array.isArray(value),
provides: [[toIterable, proxy => proxy]]
})(container)
// Provides compare
defineTrait({
requires: value => Array.isArray(value),
provides: [[compare, proxy => (a, b) => a - b]]
})(container)
// Requires BOTH toIterable and compare
defineTrait({
requires: [toIterable, compare],
provides: [[sort, proxy => [...toIterable(proxy)].sort(compare(proxy))]]
})(container)
sort([3, 1, 2]) // [1, 2, 3]defineTrait accepts two formats:
Object format (recommended):
defineTrait({
requires: value => Array.isArray(value),
provides: [[slot1, impl1], [slot2, impl2]]
})(container)Array format (concise):
defineTrait([
value => Array.isArray(value),
[[slot1, impl1], [slot2, impl2]]
])(container)When a trait requires a slot, actually use it:
// ✅ GOOD - uses toIterable
defineTrait({
requires: [toIterable],
provides: [[map, proxy => fn => {
const result = []
for (const item of toIterable(proxy)) result.push(fn(item))
return result
}]]
})(container)
// ❌ BAD - declares toIterable but doesn't use it
defineTrait({
requires: [toIterable],
provides: [[map, proxy => fn => proxy.map(fn)]] // uses .map() directly
})(container)Keep traits focused and composable:
// ✅ GOOD - separate base and derived traits
defineTrait({
requires: value => Array.isArray(value),
provides: [[toIterable, proxy => proxy]]
})(container)
defineTrait({
requires: [toIterable],
provides: [[map, /* ... */], [filter, /* ... */]]
})(container)
// ❌ BAD - mixing concerns
defineTrait({
requires: value => Array.isArray(value),
provides: [[toIterable, /* ... */], [map, /* ... */]] // mixed levels
})(container)Use data-last for pipeline operations, data-first for simple accessors:
// Pipeline operations → wrap manually for data-last
const map = addSlot(container)
const mapL = fn => data => map(data)(fn)
// Simple accessors → data-first is fine
const length = addSlot(container)
const first = addSlot(container)If no trait provides a slot, a SlotNotImplementedError is thrown:
import { SlotNotImplementedError } from 'jsr:@justbyitself/morphity'
const mySlot = addSlot(container)
try {
mySlot([1, 2, 3])
} catch (e) {
if (e instanceof SlotNotImplementedError) {
console.log(e.message) // short: slot name and value
console.log(e.detail) // verbose: available slots and trait resolution info
}
}The error exposes:
message— short description, suitable for logs and stack tracesdetail— extended context: which slots were available and whether any traits matchedslotId— the Symbol of the slot that faileditem— the internal item object
Always define traits for the types you'll use.
Morphity detects circular dependencies:
defineTrait({
requires: [slotB],
provides: [[slotA, /* ... */]]
})(container)
defineTrait({
requires: [slotA], // Creates cycle: A needs B, B needs A
provides: [[slotB, /* ... */]]
})(container) // Error: Circular dependency detectedMorphity exposes its internal state through a set of reflection functions, useful for understanding trait resolution and debugging unexpected behavior.
import { addSlotWithDescription, description, slots } from 'jsr:@justbyitself/morphity'
const map = addSlotWithDescription('map')(container)
description(map) // 'map'
slots(container) // [map, filter, ...] — all slots registered in this container
hasSlot(map)(container) // trueimport { defineTraitWithDescription, traits, provides, requires, traitDescription } from 'jsr:@justbyitself/morphity'
const trait = defineTraitWithDescription('arrayMap')({
requires: [toIterable],
provides: [[map, proxy => fn => [...toIterable(proxy)].map(fn)]]
})(container)
traitDescription(trait) // 'arrayMap'
provides(trait) // [map]
requires(trait) // [toIterable]
traits(container) // all traits registered in this containerimport { paths } from 'jsr:@justbyitself/morphity'
// Returns all trait paths morphity would use to resolve a slot for a value
paths(map)(container)([1, 2, 3])
// [[arrayIterableTrait, enumerableTrait]]Each path is an ordered array of traits that would be applied in sequence to provide the slot.
For a human-readable summary of the resolution process, use explain:
import { explain } from 'jsr:@justbyitself/morphity'
console.log(explain(map)(container)([1, 2, 3]))
// Resolving slot "map" for value: [1,2,3]
// Path 1:
// 1. arrayIterable → provides: [toIterable]
// 2. arrayMap → provides: [map]If no traits match, explain tells you clearly:
console.log(explain(map)(container)(42))
// Resolving slot "map" for value: 42
// No paths found.explain is particularly useful when a SlotNotImplementedError is thrown and you want to understand why no path was found.