Nitpick Object Orientation & Traits
Nitpick employs a strict, composition-over-inheritance model. It completely rejects classic class-based inheritance in favor of interfaces (Traits) and isolated data (Structs).
1. Traits and Implementation
A trait defines a set of functions that a
type must implement to satisfy a behavioral contract.
trait:TraitName = { ... };defines the interface.impl:TraitName:for:TypeName = { ... };implements the trait on a specific struct.
trait:Serializable = {
func:to_bytes = buffer(Self:self);
};
struct:Message = {
int32:id;
};
impl:Serializable:for:Message = {
func:to_bytes = buffer(Message:self) {
// ... serialize logic ...
pass result;
};
};
2. Default Methods (v0.83.1)
Traits can provide default implementations. Impls may override or keep the default.
trait:Describable = {
func:name = string(Self:self); // required — must be implemented
func:describe = string(Self:self) { // default — optional to override
pass("an object");
};
};
3. Supertraits (v0.83.1)
A trait can require that implementing types also implement another trait:
trait:Ordered = Equatable & {
func:compare = int32(Self:a, Self:b);
};
Impls of Ordered must also implement
Equatable. The compiler enforces this
transitively.
4. Associated Types (v0.83.2)
Traits can declare associated types that impls must bind to concrete types:
trait:Iterator = {
Type:Item;
func:next = Item(Self:self);
};
impl:Iterator:for:Range = {
Type:Item = int32;
func:next = int32(Range:self) { pass(self.current); };
};
Associated types can have defaults:
Type:Error = string;
5. Inherent Impls (v0.83.3)
Methods can be attached directly to a type without any trait:
impl:for:Point = {
func:magnitude = flt64(Point:self) {
pass(flt64_sqrt(flt64(self.x * self.x + self.y * self.y)));
};
};
6. Coherence & Object Safety (v0.83.3)
Coherence: At most one impl of a given trait for a given type. Overlapping impls produce a compile error.
Object safety: A trait is object-safe
(usable as dyn Trait) if: 1. All methods take a
self parameter (no static methods) 2. No method
returns Self (unknown size at dyn time) 3. No
methods have comptime type parameters
7. Derive Macros (v0.83.4)
The @derive attribute auto-generates trait
implementations:
@derive(Default, PartialOrd, ToString, Eq, Hash, Clone, Debug, Ord)
struct:Config = {
int32:priority;
string:name;
};
8. Blanket Impls (v0.83.5)
A blanket impl auto-implements a trait for all types satisfying a bound:
impl:Loggable:for:T:where:Printable = {
func:log_str = string(T:self) {
pass("[LOG]");
};
};
Concrete impls take priority over blanket-generated impls.
9. Dynamic Dispatch
By default, trait implementations are statically resolved
at compile-time (monomorphization) to guarantee performance
and zero-cost abstraction. If runtime polymorphism is
absolutely required, developers must explicitly opt-in using
the dyn keyword to create a “fat pointer” trait
object.
Message:msg = Message{id: 1};
dyn Serializable:obj = msg;
Multi-bound dyn (v0.83.5)
A dyn type can require multiple traits:
dyn Drawable + Serializable:obj = msg;
dyn A + B is assignable to
dyn A (widening). Each trait must be
object-safe.
(Note: Because dynamic dispatch obfuscates the
control flow graph, it triggers warnings under strict
nitpick-safety profiles).
10. Data Hiding
All struct fields are pub (public) or
private by default based on the module visibility rules. To
strictly hide internal representations (e.g. for handles or
FFI pointers), use the opaque keyword to define
a type whose internal layout is entirely unknown to the
consumer.
opaque:DatabaseHandle;