Skip to content
academia.sh

Lesson 16 / 18

Modules and Dependencies

A dependency's version is most often never written in the source at all; of six requests, three are written in the main module's own source, three are derived from a dependency's own dependency. The module file's rule for filling in an unwritten version — minimal version selection — is modeled and run within the source; no network call is made. The limiting measurement shows that the selected version is not the newest version in a module's catalog, only the largest of the requests actually written.

Published

In this lesson

  • Distinguishes, in a dependency graph of six requests, how many are written in the source and how many are derived
  • Applies the minimal version selection rule by running it on a model, without making a real network call
  • Shows, by measuring it, why the selected version does not have to be the same as the newest version available
Contents

The previous lesson showed that a package’s boundary stops at the package, not at the file. This lesson carries the same question one level up: when a package depends on another package, where in the source is which version of that dependency meant? Most often, nowhere at all. A module file writes the version of a handful of modules it directly depends on; the version of those modules’ own dependencies is never written at all, it is derived from the modules’ own files. This lesson’s question is the same as the course’s general question: when the version is not written, what does the module file put in its place?

A dependency graph has a problem its version resolution has to solve: if the same module is requested through more than one path in the graph, which version that module should be compiled with is not written on any single line of the source. When two separate dependencies request the same third module with different minimum versions, the main module is forced to choose between the two — but this choice does not appear in the main module’s source at all, because the main module never mentions that third module directly. This situation comes up often in dependency graphs by another name: two separate paths arrive at the same third point, and two different requests collide there. The collision is not an error — both requests say “at least this version,” both draw only a lower bound, neither draws an upper bound. The job of the selection is to merge these two lower bounds into a single version and make sure the merged version satisfies both requests. The measurement below builds this situation in its smallest form and reads the result by actually running the selection algorithm.

Six Requests, Two Sources

The measurement below never connects to a real module registry at all; the dependency graph is built by hand within the source, and it is written here explicitly that it is modeled. There are four modules: libraryA, libraryB, libraryC, libraryD. The main module directly requests libraryA, libraryB, and libraryD — all three versions are written in the source. libraryA requests one version of libraryC; libraryB requests another version of the same libraryC. libraryC, for its own part, requests one version of libraryD. These three requests are never written in the main module’s source at all — they are derived, during the main module’s compilation, from the dependency modules’ own files.

// main.go — minimal version selection, the dependency graph is modeled in the source (no network call)
package main

import "fmt"

// Version carries a three-part version number; this lesson does not connect to a real
// module registry, the whole graph below is constructed by hand.
type Version struct {
	Major, Minor, Patch int
}

func (v Version) text() string {
	return fmt.Sprintf("%d.%d.%d", v.Major, v.Minor, v.Patch)
}

// greaterThan reports whether v is a newer version than other.
func (v Version) greaterThan(other Version) bool {
	if v.Major != other.Major {
		return v.Major > other.Major
	}
	if v.Minor != other.Minor {
		return v.Minor > other.Minor
	}
	return v.Patch > other.Patch
}

// Request carries the lowest version a module's dependency requests. If Written is
// true, the request sits directly in the main module's own source; if not, it was
// derived from another dependency's own module file.
type Request struct {
	Module    string
	From      string
	Requested Version
	Written   bool
}

func main() {
	requests := []Request{
		{Module: "libraryA", From: "main module", Requested: Version{1, 2, 0}, Written: true},
		{Module: "libraryB", From: "main module", Requested: Version{1, 1, 0}, Written: true},
		{Module: "libraryD", From: "main module", Requested: Version{2, 1, 0}, Written: true},
		{Module: "libraryC", From: "libraryA", Requested: Version{1, 4, 0}, Written: false},
		{Module: "libraryC", From: "libraryB", Requested: Version{1, 6, 0}, Written: false},
		{Module: "libraryD", From: "libraryC", Requested: Version{2, 0, 0}, Written: false},
	}

	fmt.Println("-- six requests, is the source written or derived --")
	writtenCount, derivedCount := 0, 0
	for _, req := range requests {
		source := "derived"
		if req.Written {
			source = "written"
			writtenCount++
		} else {
			derivedCount++
		}
		fmt.Printf("%-12s <- %-12s %-9s requested=%s\n", req.Module, req.From, source, req.Requested.text())
	}
	fmt.Printf("total: %d written, %d derived\n", writtenCount, derivedCount)

	fmt.Println()
	fmt.Println("-- selected version: the greatest of all requests for each module --")
	orderedModules := []string{"libraryA", "libraryB", "libraryC", "libraryD"}
	selected := map[string]Version{}
	for _, req := range requests {
		current, ok := selected[req.Module]
		if !ok || req.Requested.greaterThan(current) {
			selected[req.Module] = req.Requested
		}
	}
	for _, name := range orderedModules {
		fmt.Printf("%-12s selected=%s\n", name, selected[name].text())
	}

	fmt.Println()
	fmt.Println("-- limiting measurement: selection takes not the newest, but the greatest written --")
	catalogNewestC := Version{1, 9, 0}
	fmt.Printf("newest version in the catalog for libraryC: %s\n", catalogNewestC.text())
	fmt.Printf("selected version: %s\n", selected["libraryC"].text())
	fmt.Printf("is the selected version the same as the catalog's newest: %v\n", selected["libraryC"] == catalogNewestC)
}
-- six requests, is the source written or derived --
libraryA     <- main module  written   requested=1.2.0
libraryB     <- main module  written   requested=1.1.0
libraryD     <- main module  written   requested=2.1.0
libraryC     <- libraryA     derived   requested=1.4.0
libraryC     <- libraryB     derived   requested=1.6.0
libraryD     <- libraryC     derived   requested=2.0.0
total: 3 written, 3 derived

-- selected version: the greatest of all requests for each module --
libraryA     selected=1.2.0
libraryB     selected=1.1.0
libraryC     selected=1.6.0
libraryD     selected=2.1.0

-- limiting measurement: selection takes not the newest, but the greatest written --
newest version in the catalog for libraryC: 1.9.0
selected version: 1.6.0
is the selected version the same as the catalog's newest: false

PT8. Three of the six requests are written in the source, three are derived; which three are written depends on whether that module is a direct dependency of the main module. PT9. libraryC has two separate requests — one from libraryA, one from libraryB — and both come into being without ever appearing in the source at all, purely from these two modules’ own files.

Minimal Version Selection

PT10. A module’s selected version is the greatest of all the requests written for that module; when both 1.4.0 and 1.6.0 are requested for libraryC, the selected version is 1.6.0, and when both 2.0.0 and 2.1.0 are requested for libraryD, the selected version is 2.1.0. The rule’s name is minimal version selection, but the selected number is the greatest of the requests, not the smallest — this seemingly contradictory naming resolves once you look at what the rule actually guarantees. The rule says: let the selected version be the lowest version that violates no request. A dependency requesting 1.4.0 also works with 1.6.0 (versions are assumed backward compatible), but a dependency requesting 1.6.0 gives no guarantee at all that it will work with 1.4.0. This is why the lowest version satisfying all requests is the greatest of the requests — the word “minimal” does not point to a single request, it points to the single point that satisfies every request at once.

This is a computation in which backward compatibility is taken as an assumption; the rule itself never checks this, it only accepts it. The greaterThan comparison above compares three numbers in order and never tests anywhere at all whether two versions are really compatible — compatibility is accepted as a promise attached to the version number itself, and it is never separately verified. The selection algorithm gives the correct result in a world where this promise is kept; when the promise is not kept (when the new version does not offer the same interface as the old one), the algorithm still selects the same number, but whether the selected version actually works is a separate question — the answer to that question is found not in the module file, but in whether the source itself compiles.

This computation also runs, for libraryD, through a two-layer chain: libraryD receives one request directly from the main module (2.1.0, written) and receives another request indirectly through libraryC (2.0.0, derived). Selection makes no distinction at all between the two — requests are compared not by where they come from but by which number they carry, and the greater one (2.1.0) wins. Whether a request is written or derived is not an input to the selection algorithm at all; it is only a separate observation, classifying the request’s source, that this lesson measures.

The importance of this distinction grows as the number of requests grows. In the measurement above there are four modules and six requests; in a real dependency graph this number can be measured in the hundreds, and the same module being requested from dozens of different paths is an ordinary situation. The selection rule does not change at this scale either: for every module, the greatest of the versions requested by all paths leading to that module is taken. The algorithm itself is insensitive to the depth of the graph — whether the request reaching libraryD comes from two steps away through libraryC, or directly from the main module, the comparison consists of nothing more than placing the same two numbers side by side and picking the larger one. The complexity sits in the size of the graph, not in the rule itself.

The Limiting Measurement: Not the Newest, the Greatest Written

The run above also carries one more catalog value: the “newest version available” for libraryC is given as 1.9.0, but the selected version is 1.6.0. PT11. Selection never asks at all about the newest version a module has available; it only looks at requests actually written (or derived), and takes the greatest of those. This is the most misunderstood side of minimal version selection: the rule does not keep a dependency up to date, it only satisfies whatever has been requested somewhere in the source (directly or indirectly). libraryC may have a 1.9.0 version, but as long as no request asks for it, it never enters the selected version at all.

That this is a deliberate choice becomes visible when you look at what it actually guarantees: it is wanted that a dependency graph’s result be independent of who compiles that graph and when. If selection were “always take the newest,” the same source text could compile with a different set of dependencies at different times — even though the source itself never changed at all. Minimal version selection instead looks only at what is written in the source (even if indirectly); the same set of requests selects the same set of versions no matter when or where it is run. The cost is explicit: to benefit from a dependency’s new version, a line requesting that version has to be written somewhere; no request rises on its own.

This has one more consequence: two separate developers, running the same dependency graph at the same time, with no knowledge of each other at all, still arrive at the same set of versions — because selection looks at no outside state at all (the catalog’s current contents, which versions are considered “newest”), only at what is written in the source. Upgrading a dependency therefore requires a separate, deliberate step: either the directly requested version is changed in the source, or an intermediate dependency’s own request somewhere in the chain is raised. Either way, the upgrade happens by writing a new number somewhere; until it is written, selection always returns to the same result, because the computation itself is deterministic.

Written, Derived, Selected

In a module file’s real source, these three concepts fall on three separate lines. The versions the main module directly requests sit explicitly written in the module file’s own declaration block. Modules carrying only a derived version also fall into the same block, but these lines are marked “indirect” — they show modules never imported anywhere in the source, only an input to the selection computation. The selected version emerges from the union of these two sets, through the computation above; because the module file carries these three separately, why a module ends at that version — its own request or another dependency’s request — remains traceable backward from the source.

In a real module file, this declaration block can be read line by line: every line carries a module name and the version selected for that module, and a short note is added at the end of a line for a module not directly requested but only derived. This note itself is not a version piece of information, it is a piece of origin information — it says that module was never imported at all in the main module’s source, it only entered the selection computation through another module. The Written field in the measurement above is exactly the counterpart of this note: for libraryA, libraryB, libraryD the Written field is true (these are imported in the main module’s own source), and false for libraryC‘s two requests (neither is ever mentioned at all in the main module’s source). The declaration block itself is written after the selection computation — first every request is gathered, minimal version selection is run, then only the result (the selected version) falls into the block. The intermediate steps — which request came from which module — never appear at all in the module file itself; this information exists only for a moment while the selection computation is running, exactly as it sits in the requests slice above.

These three distinctions (written, derived, selected) have a practical consequence: someone reading a module file can tell apart, with two different questions, why a dependency stands there. The answer to “did I request this module?” sits in the direct/indirect distinction. The answer to “which version did we get?” sits on the selected version line — but this second question never comes out of any single line of the source alone, because the selected version is the result of comparing more than one request. The module file is therefore less a declaration list and more the final state of a computation: every line carries the winner of a comparison that does not appear in the source.

Reading where a dependency’s version comes from off a declaration line (Modules, Tooling and the Ecosystem) and determining which version a package is published under (Python Projects: Packaging and Testing) were already built in other languages’ own mechanisms; this lesson does not repeat that model, it only measures Go’s own selection rule — taking the greatest of the requests.

Summary

  • Requests in a dependency graph come from two sources: those written in the main module’s own source, and those derived from a dependency’s own module file.
  • Minimal version selection takes the greatest of all requests written for a module; it carries the name “minimal” because it is the lowest common point that violates no request.
  • Whether a request is written or derived does not affect the selection computation; only which number is carried is compared.
  • The limiting measurement: the selected version is not the newest version in a module’s catalog, only the greatest of the ones actually requested — no request rises on its own.
  • This measurement was run without making a real network call at all, by modeling the dependency graph within the source.

Next Step

This lesson measured who selects a module’s version: the choice is determined neither by a person nor by a separate tool, it is determined by the requests themselves — whether written in the source or derived from a dependency, the computation applies the same rule to both. A module’s version therefore sits outside the source, as an output of the selection algorithm. But for the form of the source inside a module and the convention it follows, the answer to the same question sits in a different place entirely: not a person or a rulebook applies them, but Go’s own command family. The next lesson calls these commands from within the program and measures how an unwritten formatting rule changes the source, and which tool catches a pattern the compiler does not reject.

To keep your progress and take notes, Log in

My notes

Log in to take notes.

Start typing to search.

↑↓ ↵ Esc navigate · open · close