Builtin Interfaces
Pebble 0.3.1 · all symbols on this page are stable.
Pebble's type-class machinery is exposed via interfaces — sets of methods a type can implement. Two are built into the prelude and auto-derived for every type that supports them. User-defined interfaces use the same dispatch mechanism but require explicit implements blocks.
type Foo implements Show {
show( self ): bytes {
return "Foo".toBytes();
}
}
The compiler resolves interface methods at monomorphisation time — when a generic function constrained on T: SomeInterface is called with a concrete type, the compiler looks up that type's impl (either user-defined or built-in factory) and inlines the right method IR. There is no on-chain dictionary-passing overhead.
ToData
Defines the canonical conversion of a value to Plutus data. Every concrete Pebble type that can be encoded as data automatically has a ToData impl — primitives, List<T>, Array<T>, LinearMap<K, V> (when K and V are themselves ToData), Optional<T>, structs and enums, and the native Value.
interface ToData {
toData( self ): data;
}
The auto-derivation is provided by the compiler's built-in factory in populateBuiltinInterfaces.ts. For each concrete type, the factory returns the IR closure that performs the right encoding:
| Type | Lowering |
|---|---|
int | iData |
bytes | bData |
bool | Constr(0/1, []) |
List<T> | listData(map(toData<T>, xs)) |
LinearMap<K,V> | mapData with mkPairData + toData<K>/toData<V> |
Optional<T> | Constr(0, [toData<T>(x)]) for Some, Constr(1, []) for None |
| struct | Constr(0, [toData(field_1), toData(field_2), ...]) |
| enum / SoP | Constr(variantIndex, [toData(field_1), ...]) |
native Value | the valueData builtin |
Calling it
Every value has a .toData() method:
const n: int = 42;
const d: data = n.toData(); // iData(42)
const xs: List<int> = [1, 2, 3];
const dl: data = xs.toData(); // listData([iData(1), iData(2), iData(3)])
Implementing it on your own type
Most user types don't need a hand-rolled ToData — the auto-derivation covers them. Reach for an explicit impl only when you need a non-canonical encoding (e.g. a legacy on-chain shape from a previous protocol version).
struct LegacyRecord {
a: int,
b: bytes,
}
type LegacyRecord implements ToData {
toData( self ): data {
// Hand-rolled to match an older protocol's wire format.
return std.builtins.constrData(7, [
self.b.toData(), // legacy order: b first, then a
self.a.toData(),
]);
}
}
Using it as a constraint
Stdlib generics that need to encode T use a T: ToData constraint. std.linearMap.prepend is the canonical example:
// signature: prepend<K: ToData, V: ToData>(k: K, v: V, m: LinearMap<K, V>): LinearMap<K, V>
const m: LinearMap<bytes, int> = {};
const m2 = std.linearMap.prepend(#01, 100, m);
Show
Defines a human-readable byte representation. Built-in for primitives; user-overridable.
interface Show {
show( self ): bytes;
}
| Type | Default impl |
|---|---|
int | Decimal digits (42 → #3432) |
bytes | Hex digits, lower-case, no 0x prefix (#deadbeef → #6465616462656566) |
bool | #74727565 ("true") or #66616c7365 ("false") |
string | UTF-8 bytes (essentially .toBytes()) |
Calling it
const n: int = 42;
trace(n.show()); // emits "42" to the trace log
Implementing it on your own type
Useful for adding debug-friendly trace output without writing a hand-rolled formatter every time:
struct Account {
name: bytes,
balance: int,
}
type Account implements Show {
show( self ): bytes {
return self.name.concat(": ").toBytes()
.concat(self.balance.show());
}
}
const a = Account{ name: "alice".toBytes(), balance: 100 };
trace(a.show()); // "alice: 100"
Using it as a constraint
If you write a generic helper that traces its argument:
function debugIdentity<T: Show>( x: T ): T {
trace(x.show());
return x;
}
Implementation note
Both interfaces are registered in the prelude via the same populateBuiltinInterfaces entry point. At call time, dispatch goes through resolveInterfaceImpl(concreteType, interfaceName) which:
- First checks the type's
methodsNamesPtr/methodNamesPtrfor a user-supplied impl. - Then falls back to the built-in factory map (
program.builtinInterfaceImpls). - Otherwise raises a
"type X does not implement Y"compile error.
This means user impls always win over the built-in derivation. If you write a type int implements ToData (which would be unusual but legal), your version replaces the default iData lowering for every call site that monomorphises against int.
See also
data— the typeToDataproducesstd.builtins— the underlying data constructors (constrData,iData,bData, …)- Datum & Redeemer Flow — when and where
ToDatais invoked at the on-chain ↔ off-chain boundary