Hedronite Lesson · Polyglot-Dev / V · Wed 2026-10-07 · V session 3 of 5

Structs, methods, interfaces, sum types, modules · Fence the Fields · Let the Method Set Decide · Exhaust the Sum · One Folder, One Module

Fence the fields. Let the method set decide. Exhaust the sum. One folder, one module.

Lesson Class: Asr (V Lang bridge · learn-first · watch-not-adopt)
Focus: Fence the Fields · Let the Method Set Decide · Exhaust the Sum · One Folder, One Module
Done-criteria: struct with a mut: field and a method; implicit interface satisfaction; sum type exhausted with match; one module split out and imported
Grounding: vlang/v 0.5.2 (7647ce1) docs.md + vlib/v/builder/c_error_report.v + help/build/build-c.txt + checker.v
Note: The lab Mac (macOS arm64) · V 0.5.2 7647ce1 by absolute path · 17 programs + 4 module projects · V_C_ERROR_BUG_REPORT_DISABLED=1
Fence the Fields
Private and immutable by default. mut: and pub: open sections. mut receiver needs a mut binding. Embedding is not a cast.
Let the Method Set Decide
Fields plus methods satisfy an interface; implements moves the error to the declaration. The mut: section was not enforced at 0.5.2.
Exhaust the Sum
match must name every variant or carry else. as panics on the wrong variant. One folder is one module, one prefix deep.
Fence the fields. Let the method set decide. Exhaust the sum. Close the editor.

Fence the fields. Let the method set decide. Exhaust the sum. One folder, one module.

§I. Frame

Asr V session 3 of 5, Wed 2026-10-07. Learn-first, contributor-aware, watch-not-adopt. Cites are vlang/v at tag 0.5.2 (7647ce1) only: doc/docs.md and source files at the tag. The lab ran on the lab Mac with the compiler Duha #6 built (~/src/vlang-duha6/v/v, called by absolute path). Every message quoted below is copied from lab/lab.log.

Today's row has no compare-page claim to test. The claims under test are the docs' own sentences, and three of them did not match what the 0.5.2 compiler did. Those three are the contributor note in §VIII.

§II. Four techniques

  1. Fence the Fields. Struct fields are private and immutable unless a mut: or pub: section says otherwise. Methods are functions with a receiver, and a mut receiver needs a mut binding at the call.
  2. Let the Method Set Decide. A type satisfies an interface by having the fields and methods. No declaration is needed; implements is an optional check.
  3. Exhaust the Sum. A sum type is a closed list of variants. match must name every one or carry else.
  4. One Folder, One Module. A directory is a module, the prefix is one segment deep, and nothing leaves it without pub.

§III. Fence the Fields

docs.md, Access modifiers: fields "are private and immutable by default (making structs immutable as well)", with five sections: none, mut:, pub:, pub mut:, and __global:. 01_struct_mut_field.v declares name in the default section, hits under mut:, and limit int = 3 under pub: with a default value. A fn (mut c Counter) hit() method raised hits to 3 and full() went from false to true.

Three refusals:

@[required] on a field (05) turns an empty literal into error: field `Config.host` must be initialized. Without it, docs.md says every field is zeroed.

Embedding (06): Button embeds Size and gets its fields and its area() method. b.area() and b.Size.area() both gave 6. docs.md says you "cannot type cast between structs and embedded structs" and calls embedding a mixin, "NOT base classes". 07 passes a Button to a function that takes Size. The V checker let it through. Clang rejected the generated C: passing 'main__Button' ... to parameter of incompatible type 'main__Size', followed by V's own text, "This can be caused by invalid C interop code, C compiler flags, or a V compiler bug." The rule holds, but the check that caught it was the C compiler's.

That first run also printed Sent C compiler bug report to https://bugs.vlang.io/bug-report. and a report id. vlib/v/builder/c_error_report.v at 0.5.2 uploads a report on every C compile error unless V_C_ERROR_BUG_REPORT_DISABLED=1 is set. Its comments say the upload carries up to 40 V source lines on each side of the failing line, "deliberately not the whole file". v help build-c documents the variable; docs.md does not. The lab script now exports it, and the shipped lab.log is the second run, which sent nothing.

§IV. Let the Method Set Decide

08_interface_implicit.v follows docs.md: Dog and Cat each have a breed field and speak(), and neither names Speaker. Both went into a []Speaker. An interface method fn (s Speaker) intro() ran for both. docs.md says such methods are not default implementations; the type's own method wins only after a smart cast. Inside if item is Dog, typeof(item).name printed &Dog. The cast hands you a reference, which docs.md notes under Casting an interface.

A type missing a method (09): error: `Rock` doesn't implement method `speak` of interface `Speaker`, then a second error refusing the cast. With struct Rock implements Speaker (11) the same message lands on the struct line, before any use. That is the job of the optional keyword: the error moves to the declaration.

The mut: section of an interface did not hold. docs.md: "Implementing types will need to have a mut receiver, for methods declared in the mut: section", and its example says passing a value-receiver MyStruct to fn2(s Bar) is a compile error. 10b is that example with the commented line restored. It compiled, printed fn2 got Bar, and exited 0. 10 casts through a mut binding and also ran.

Generated C for 20_c_shapes.v shows the cost. main__Namer is a union of object pointers plus u32 _typ and void* _methods. describe(n) compiles to a call through _main__Namer_interface_methods->_method_name(n._object): one table lookup per call, built by the compiler.

§V. Exhaust the Sum

type Shape = Circle | Rect | Square (12). area() uses match s with one arm per variant, and inside each arm s is already the variant: s.r, s.w, s.side. The loop printed Circle area=3.0, Rect area=6.0, Square area=16.0 from type_name(). A recursive type Tree = Empty | Node summed to 14.

Drop the Square arm (13): error: match must be exhaustive (add match branches for: `Square` or `else {}` at the end). That error is the reason to choose a sum type over an interface for a closed set.

as is the unchecked cast. 14 holds a Moon and asks for Mars: V panic: as cast: cannot cast `main.Moon` to `main.Mars`, exit 1. docs.md says to prefer a smart cast.

docs.md also says a mutable variable keeps its original type in if w is Mars unless you write if mut w is Mars. 15 leaves the mut off, calls w.dust_storm() inside the branch, and printed true, exit 0. With mut (end of 12) it also worked.

In C, main__Shape is a union of variant pointers plus u32 _typ. A sum value points to its payload, and _typ records which variant it holds.

§VI. One Folder, One Module

16_modproj/ has v.mod, main.v, and geom/shapes.v plus geom/more.v, both starting module geom. main.v called geom.new_rect(2, 3) and printed area=6 tag=2x3. more.v called the private secret_tag() from shapes.v. docs.md: items are usable "between the files of a module regardless of whether or not they are prefaced with the pub keyword".

The first draft wrote import geom and then import geom { square }. 0.5.2 refused the second line: A module `geom` was already imported on line 3`. docs.md's note says a selective import "will import the module as well", so the selective line alone is enough. With only that line, geom.new_rect and bare square(4) both resolved.

From another module (17): function `geom.secret_tag` is private, field `geom.Rect.tag` is not public, and field `w` of struct `geom.Rect` is immutable for a pub: field. Readonly from outside, as documented.

pub mut: did not hold. docs.md says it means "public, but mutable only in parent module". In 16, module main wrote r.label = 'door' and printed label=door, exit 0. The struct-field branch of vlib/v/checker/checker.v that we read tests is_mut and nothing about the module.

Two more rules. 18 puts module shapes in geom/shapes.v; docs.md says the name "must match the name of their directory". It built and ran, so 0.5.2 did not enforce that in this layout. 19 gives modules a and b each other's import. The builder stopped with import cycle detected between the following modules: and three chains, starting main -> a -> b -> a.

§VII. Lab

Safety callout: export V_C_ERROR_BUG_REPORT_DISABLED=1 before any v command. V 0.5.2 uploads a report to bugs.vlang.io on every C compile error by default. Each report carries up to 40 V source lines on each side of the failing line, the C context around the error, and the build options. v help build-c documents the switch; docs.md does not. Set the variable to 1 in the lab script's first lines, and every V lab script does the same from here on.

  1. Predict the exit codes of 02, 07, 10b, 13, and 16, then run them by absolute path with V_C_ERROR_BUG_REPORT_DISABLED=1 exported.
  2. Emit C for 20_c_shapes.v and find struct main__Shape, struct main__Namer, and the methods table.
  3. Move square() into a new geom/extra/ folder, import it as geom.extra, and call it with a one-segment prefix.

Evidence: lab/lab.sh, lab/lab.log, and the .v files and project folders beside them.

§VIII. Contributor note

Four gaps a learner could draft into an issue or docs change. Each comes with a two-file reproduction already in lab/.

  1. Interface mut: section. docs.md says a value receiver does not satisfy a mut: method; 0.5.2 accepted it (10, 10b). The example's fn2 line is commented out, so v check-md never tests it. A failcompile block would.
  2. pub mut: outside the parent module. Documented as parent-only; 16 mutated it from main.
  3. Smart cast without mut. Documented as keeping the original type; 15 used the variant method.
  4. Embedded struct passed as its embed. The checker accepts it and the C compiler rejects it (07). That is a checker bug, not a docs gap. Search open issues before filing.

Unrelated to the language, docs.md never mentions the automatic C-error upload. A one-line pointer to V_C_ERROR_BUG_REPORT_DISABLED under Debugging is a small docs change. Duha #8 owns the draft-PR shape. Nothing goes upstream from a lesson.

§IX. Common mistakes

  1. Marking the binding mut and expecting a default-section field to become writable.
  2. Writing import m and import m { f } together.
  3. Using as where if x is T or match would do.
  4. Adding else {} to a sum-type match by habit. It turns off the exhaustiveness error that 13 shows.
  5. Trusting the access table and the interface mut: section as written. Test them on the pinned compiler.
  6. Running V labs without V_C_ERROR_BUG_REPORT_DISABLED=1.

§X. V next to Go and Rust, session 3

Next to Go, interfaces are implicit in both, and V's can also require fields. V adds closed sum types with exhaustive match, which Go lacks. Next to Rust, type Shape = A | B | C with match maps onto enum with match, and both refuse a missing arm. Rust's trait impls are explicit and checked by rustc. V's checker let three documented rules through today and left one to the C compiler. Verdict: the shapes are familiar and pleasant, and the docs promise more checking than 0.5.2 does. Learn it, adopt nothing. Asr #4 takes memory and concurrency tomorrow.

Related:

Fence the fields. Let the method set decide. Exhaust the sum. Close the editor.