Migrate your code
You have a std list. You want the type check.
The change is small and boring.
- You find and replace, five times.
- There is no design to think about.
- The list itself does not change: the same std type, the same calls.
Before and after
The std Node moves into a TypedNode.
The TypedNode holds the Node and the type id.
before after
Message Message
+--------------------+ +---------------------------+
| text | | text |
| node <-- the list | | tnode: DoublyTypedNode |
+--------------------+ | +-----------------------+ |
| | node <-- the list | |
| | type id | |
| +-----------------------+ |
+---------------------------+
NAQ: Why not keep the type id in the std Node?
The std Node belongs to std. Paternitas does not change std.
So the TypedNode holds the std Node, and the type id next to it.
Your list still links plain std Nodes.
Before, with the plain std list:
const Message: type = struct {
text: []const u8,
node: std.DoublyLinkedList.Node = .{},
};
const Job: type = struct {
id: u32,
node: std.DoublyLinkedList.Node = .{},
};
var message: Message = .{ .text = "hello" };
var job: Job = .{ .id = 42 };
var list: std.DoublyLinkedList = .{};
list.append(&message.node);
list.append(&job.node);
const first: *std.DoublyLinkedList.Node = list.popFirst() orelse return error.ListEmpty;
const m: *Message = @fieldParentPtr("node", first);
std.log.info("message: {s}", .{m.*.text});
const second: *std.DoublyLinkedList.Node = list.popFirst() orelse return error.ListEmpty;
const j: *Job = @fieldParentPtr("node", second);
std.log.info("job: {d}", .{j.*.id});
After, with Paternitas:
const Message: type = struct {
text: []const u8,
tnode: paternitas.DoublyTypedNode = .{},
};
const TypedMessage: type = paternitas.Typed(Message);
var list: std.DoublyLinkedList = .{};
list.append(TypedMessage.node(&message));
const node: *std.DoublyLinkedList.Node = list.popFirst() orelse return error.ListEmpty;
const recovered: *Message = TypedMessage.parentFromNode(node) orelse return error.WrongParent;
The full program: One struct, one list.
The five steps
Do them for each struct that goes into the list.
| step | find | replace with |
|---|---|---|
| 1 | std.DoublyLinkedList.Node |
paternitas.DoublyTypedNode |
| 1 | std.SinglyLinkedList.Node |
paternitas.SinglyTypedNode |
| 2 | the end of the struct, }; |
};, then const TypedMessage = paternitas.Typed(Message); |
| 3 | after var message: Message = .{ ... }; |
add TypedMessage.setTypeId(&message); |
| 4 | &message.node |
TypedMessage.node(&message) |
| 5 | @fieldParentPtr("node", n) |
TypedMessage.parentFromNode(n), or TypedMessage.mustParentFromNode(n) |
Job gets the same five steps, with TypedJob.
Step 1 keeps the field name, or changes it. Both are fine.
Step 3 has one more place.
- Your code may write the whole struct somewhere, as a reset.
- Add
setTypeIdright after that write too. - The setTypeId rule says when to call it.
What the compiler finds
The compiler stops at each place you missed in steps 4 and 5.
&message.nodeno longer exists. The field is a TypedNode now.@fieldParentPtr("node", n)no longer matches the field.
It cannot find a missed step 3.
parentFromNodegives null for that struct.mustParentFromNodepanics, in every build mode.- The panic says what it found:
<no type>.
That panic comes from this program:
//! mustParentFromNode panics in every build mode when setTypeId was never
//! called on the Parent.
const Msg = struct { tnode: p.SinglyTypedNode = .{} };
pub fn main() void {
var m: Msg = .{};
_ = p.Typed(Msg).mustParentFromNode(p.Typed(Msg).node(&m));
}
const p = @import("paternitas");
When you see <no type>, add setTypeId right after that struct is created.
The four calls
You use these most of the time.
| call | what it does |
|---|---|
paternitas.Typed(Message) |
makes the helper for Message |
TypedMessage.setTypeId(&message) |
sets the type id of Message |
TypedMessage.node(&message) |
gives the Node to the std list |
TypedMessage.parentFromNode(node) |
checks the Node and gives you *Message, or null |
NAQ: What does a check cost?
One pointer compare.
There is a fifth call for one case.
TypedMessage.mustParentFromNode(node)gives you*Message, or panics.- Use it when another type would be a bug.
- It panics in every build mode.
- The panic message shows both type names.
NAQ: Does it check in ReleaseFast?
Yes.
The type check works in every build mode.
The must calls panic in every build mode too.
Several types in one list? Use parentFromNode, and ask each type in turn.
Several types in one list shows how.
Singly or doubly linked?
Use the typed Node that matches your list.
| your list | the field in your struct |
|---|---|
std.SinglyLinkedList |
tnode: paternitas.SinglyTypedNode = .{}, |
std.DoublyLinkedList |
tnode: paternitas.DoublyTypedNode = .{}, |
STNode and DTNode are short names for the same two types.
The field name does not matter.
node,tnode,link. All are fine.- The field can be anywhere in the struct.
- Paternitas finds the field by its type.
Do not touch the fields inside the TypedNode. Use the calls.
One struct has one TypedNode.
A second one is a compile error:
//! Two TypedNodes in one struct do not compile.
const TwoTypedNodes = struct {
a: p.SinglyTypedNode = .{},
b: p.DoublyTypedNode = .{},
};
comptime {
_ = p.Typed(TwoTypedNodes).typeId();
}
const p = @import("paternitas");
What Typed(P) does for you
P is your struct. Typed(P) is its helper.
You declare it once, right after the struct.
It does the housekeeping:
- It finds the TypedNode field in your struct, by its type.
- It does the
@fieldParentPtrwork. - It writes your struct's type id, in
setTypeId. - It checks the type id when the Node comes back.
You never write @fieldParentPtr or the field's name again.