Repository: gonum/gonum
Pull request #1689 (2021-06-23 11:33:13)

graph/{iterator,multi}: use map iterators for lines and weighted lines
Please take a look.

<!--
Checklist:

- API changes have been discussed
- code is goformated correctly (goimports)
- packages with generated code have had code generation run
- tests pass locally
- linked to relevant issues

Please make sure your commit message summary line and pull request
title match the Go convention; a one-line summary of the change,
prefixed by the primary affected package that should complete the
sentence, "This change modifies Gonum to _____."
-->


codecov-commenter: # [Codecov](https://codecov.io/gh/gonum/gonum/pull/1689?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) Report
> Merging [#1689](https://codecov.io/gh/gonum/gonum/pull/1689?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) (b8a9027) into [master](https://codecov.io/gh/gonum/gonum/commit/4895e9b3fc41544b07a41a714316e6ad016fc503?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) (4895e9b) will **decrease** coverage by `0.16%`.
> The diff coverage is `79.26%`.

[![Impacted file tree graph](https://codecov.io/gh/gonum/gonum/pull/1689/graphs/tree.svg?width=650&height=150&src=pr&token=B7qHnqSPNL&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum)](https://codecov.io/gh/gonum/gonum/pull/1689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum)

```diff
@@            Coverage Diff             @@
##           master    #1689      +/-   ##
==========================================
- Coverage   73.25%   73.08%   -0.17%     
==========================================
  Files         508      493      -15     
  Lines       60020    59538     -482     
==========================================
- Hits        43966    43512     -454     
+ Misses      13570    13554      -16     
+ Partials     2484     2472      -12     
```


| [Impacted Files](https://codecov.io/gh/gonum/gonum/pull/1689?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) | Coverage Δ | |
|---|---|---|
| [graph/iterator/map.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvaXRlcmF0b3IvbWFwLmdv) | `48.57% <33.33%> (-7.96%)` | :arrow_down: |
| [graph/multi/undirected.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvbXVsdGkvdW5kaXJlY3RlZC5nbw==) | `89.55% <50.00%> (-0.16%)` | :arrow_down: |
| [graph/multi/weighted\_undirected.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvbXVsdGkvd2VpZ2h0ZWRfdW5kaXJlY3RlZC5nbw==) | `90.36% <50.00%> (-0.23%)` | :arrow_down: |
| [graph/multi/directed.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvbXVsdGkvZGlyZWN0ZWQuZ28=) | `90.69% <71.42%> (-1.62%)` | :arrow_down: |
| [graph/multi/weighted\_directed.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvbXVsdGkvd2VpZ2h0ZWRfZGlyZWN0ZWQuZ28=) | `92.35% <75.00%> (-2.62%)` | :arrow_down: |
| [graph/iterator/lines\_map.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvaXRlcmF0b3IvbGluZXNfbWFwLmdv) | `100.00% <100.00%> (ø)` | |
| [graph/iterator/nodes\_map.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvaXRlcmF0b3Ivbm9kZXNfbWFwLmdv) | `90.24% <100.00%> (ø)` | |
| [internal/asm/c64/conj.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-aW50ZXJuYWwvYXNtL2M2NC9jb25qLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [graph/iterator/nodes\_map\_safe.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-Z3JhcGgvaXRlcmF0b3Ivbm9kZXNfbWFwX3NhZmUuZ28=) | | |
| [internal/asm/f64/dot.go](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum#diff-aW50ZXJuYWwvYXNtL2Y2NC9kb3QuZ28=) | | |
| ... and [15 more](https://codecov.io/gh/gonum/gonum/pull/1689/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/gonum/gonum/pull/1689?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/gonum/gonum/pull/1689?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum). Last update [4895e9b...b8a9027](https://codecov.io/gh/gonum/gonum/pull/1689?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=gonum).


Patch:
diff --git a/graph/iterator/lines_map.go b/graph/iterator/lines_map.go
new file mode 100644
index 00000000..95959b3a
--- /dev/null
+++ b/graph/iterator/lines_map.go
@@ -0,0 +1,140 @@
+// Copyright ©2021 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build !safe
+// +build !safe
+
+package iterator
+
+import "gonum.org/v1/gonum/graph"
+
+// Lines implements the graph.Lines interfaces.
+// The iteration order of Lines is randomized.
+type Lines struct {
+	lines int
+	iter  *mapIter
+	pos   int
+	curr  graph.Line
+}
+
+// NewLines returns a Lines initialized with the provided lines, a
+// map of line IDs to graph.Lines. No check is made that the keys
+// match the graph.Line IDs, and the map keys are not used.
+//
+// Behavior of the Lines is unspecified if lines is mutated after
+// the call to NewLines.
+func NewLines(lines map[int64]graph.Line) *Lines {
+	return &Lines{lines: len(lines), iter: newMapIterLines(lines)}
+}
+
+// Len returns the remaining number of lines to be iterated over.
+func (l *Lines) Len() int {
+	return l.lines - l.pos
+}
+
+// Next returns whether the next call of Line will return a valid line.
+func (l *Lines) Next() bool {
+	if l.pos >= l.lines {
+		return false
+	}
+	ok := l.iter.next()
+	if ok {
+		l.pos++
+		l.curr = l.iter.line()
+	}
+	return ok
+}
+
+// Line returns the current line of the iterator. Next must have been
+// called prior to a call to Line.
+func (l *Lines) Line() graph.Line {
+	return l.curr
+}
+
+// Reset returns the iterator to its initial state.
+func (l *Lines) Reset() {
+	l.curr = nil
+	l.pos = 0
+	l.iter.it = nil
+}
+
+// LineSlice returns all the remaining lines in the iterator and advances
+// the iterator. The order of lines within the returned slice is not
+// specified.
+func (l *Lines) LineSlice() []graph.Line {
+	if l.Len() == 0 {
+		return nil
+	}
+	lines := make([]graph.Line, 0, l.Len())
+	for l.iter.next() {
+		lines = append(lines, l.iter.line())
+	}
+	l.pos = l.lines
+	return lines
+}
+
+// WeightedLines implements the graph.WeightedLines interfaces.
+// The iteration order of WeightedLines is randomized.
+type WeightedLines struct {
+	lines int
+	iter  *mapIter
+	pos   int
+	curr  graph.WeightedLine
+}
+
+// NewWeightedLines returns a WeightedLines initialized with the provided lines, a
+// map of line IDs to graph.WeightedLines. No check is made that the keys
+// match the graph.WeightedLine IDs, and the map keys are not used.
+//
+// Behavior of the WeightedLines is unspecified if lines is mutated after
+// the call to NewWeightedLines.
+func NewWeightedLines(lines map[int64]graph.WeightedLine) *WeightedLines {
+	return &WeightedLines{lines: len(lines), iter: newMapIterWeightedLines(lines)}
+}
+
+// Len returns the remaining number of lines to be iterated over.
+func (l *WeightedLines) Len() int {
+	return l.lines - l.pos
+}
+
+// Next returns whether the next call of Line will return a valid line.
+func (l *WeightedLines) Next() bool {
+	if l.pos >= l.lines {
+		return false
+	}
+	ok := l.iter.next()
+	if ok {
+		l.pos++
+		l.curr = l.iter.weightedLine()
+	}
+	return ok
+}
+
+// WeightedLine returns the current line of the iterator. Next must have been
+// called prior to a call to WeightedLine.
+func (l *WeightedLines) WeightedLine() graph.WeightedLine {
+	return l.curr
+}
+
+// Reset returns the iterator to its initial state.
+func (l *WeightedLines) Reset() {
+	l.curr = nil
+	l.pos = 0
+	l.iter.it = nil
+}
+
+// WeightedLineSlice returns all the remaining lines in the iterator and advances
+// the iterator. The order of lines within the returned slice is not
+// specified.
+func (l *WeightedLines) WeightedLineSlice() []graph.WeightedLine {
+	if l.Len() == 0 {
+		return nil
+	}
+	lines := make([]graph.WeightedLine, 0, l.Len())
+	for l.iter.next() {
+		lines = append(lines, l.iter.weightedLine())
+	}
+	l.pos = l.lines
+	return lines
+}
diff --git a/graph/iterator/lines_map_safe.go b/graph/iterator/lines_map_safe.go
new file mode 100644
index 00000000..0a7414f7
--- /dev/null
+++ b/graph/iterator/lines_map_safe.go
@@ -0,0 +1,146 @@
+// Copyright ©2021 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build safe
+// +build safe
+
+package iterator
+
+import (
+	"reflect"
+
+	"gonum.org/v1/gonum/graph"
+)
+
+// Lines implements the graph.Lines interfaces.
+// The iteration order of Lines is randomized.
+type Lines struct {
+	lines reflect.Value
+	iter  *reflect.MapIter
+	pos   int
+	curr  graph.Line
+}
+
+// NewLines returns a Lines initialized with the provided lines, a
+// map of line IDs to graph.Lines. No check is made that the keys
+// match the graph.Line IDs, and the map keys are not used.
+//
+// Behavior of the Lines is unspecified if lines is mutated after
+// the call to NewLines.
+func NewLines(lines map[int64]graph.Line) *Lines {
+	rv := reflect.ValueOf(lines)
+	return &Lines{lines: rv, iter: rv.MapRange()}
+}
+
+// Len returns the remaining number of lines to be iterated over.
+func (l *Lines) Len() int {
+	return l.lines.Len() - l.pos
+}
+
+// Next returns whether the next call of Line will return a valid line.
+func (l *Lines) Next() bool {
+	if l.pos >= l.lines.Len() {
+		return false
+	}
+	ok := l.iter.Next()
+	if ok {
+		l.pos++
+		l.curr = l.iter.Value().Interface().(graph.Line)
+	}
+	return ok
+}
+
+// Line returns the current line of the iterator. Next must have been
+// called prior to a call to Line.
+func (l *Lines) Line() graph.Line {
+	return l.curr
+}
+
+// Reset returns the iterator to its initial state.
+func (l *Lines) Reset() {
+	l.curr = nil
+	l.pos = 0
+	l.iter = l.lines.MapRange()
+}
+
+// LineSlice returns all the remaining lines in the iterator and advances
+// the iterator. The order of lines within the returned slice is not
+// specified.
+func (l *Lines) LineSlice() []graph.Line {
+	if l.Len() == 0 {
+		return nil
+	}
+	lines := make([]graph.Line, 0, l.Len())
+	for l.iter.Next() {
+		lines = append(lines, l.iter.Value().Interface().(graph.Line))
+	}
+	l.pos = l.lines.Len()
+	return lines
+}
+
+// WeightedLines implements the graph.WeightedLines interfaces.
+// The iteration order of WeightedLines is randomized.
+type WeightedLines struct {
+	lines reflect.Value
+	iter  *reflect.MapIter
+	pos   int
+	curr  graph.WeightedLine
+}
+
+// NewWeightedLines returns a WeightedLines initialized with the provided lines, a
+// map of line IDs to graph.WeightedLines. No check is made that the keys
+// match the graph.WeightedLine IDs, and the map keys are not used.
+//
+// Behavior of the WeightedLines is unspecified if lines is mutated after
+// the call to NewWeightedLines.
+func NewWeightedLines(lines map[int64]graph.WeightedLine) *WeightedLines {
+	rv := reflect.ValueOf(lines)
+	return &WeightedLines{lines: rv, iter: rv.MapRange()}
+}
+
+// Len returns the remaining number of lines to be iterated over.
+func (l *WeightedLines) Len() int {
+	return l.lines.Len() - l.pos
+}
+
+// Next returns whether the next call of WeightedLine will return a valid line.
+func (l *WeightedLines) Next() bool {
+	if l.pos >= l.lines.Len() {
+		return false
+	}
+	ok := l.iter.Next()
+	if ok {
+		l.pos++
+		l.curr = l.iter.Value().Interface().(graph.WeightedLine)
+	}
+	return ok
+}
+
+// WeightedLine returns the current line of the iterator. Next must have been
+// called prior to a call to WeightedLine.
+func (l *WeightedLines) WeightedLine() graph.WeightedLine {
+	return l.curr
+}
+
+// Reset returns the iterator to its initial state.
+func (l *WeightedLines) Reset() {
+	l.curr = nil
+	l.pos = 0
+	l.iter = l.lines.MapRange()
+}
+
+// WeightedLineSlice returns all the remaining lines in the iterator and advances
+// the iterator. The order of lines within the returned slice is not
+// specified.
+func (l *WeightedLines) WeightedLineSlice() []graph.WeightedLine {
+	if l.Len() == 0 {
+		return nil
+	}
+	lines := make([]graph.WeightedLine, 0, l.Len())
+	for l.iter.Next() {
+		lines = append(lines, l.iter.Value().Interface().(graph.WeightedLine))
+	}
+	l.pos = l.lines.Len()
+	return lines
+}
diff --git a/graph/iterator/map.go b/graph/iterator/map.go
index bca015c5..613a614d 100644
--- a/graph/iterator/map.go
+++ b/graph/iterator/map.go
@@ -28,31 +28,44 @@ type emptyInterface struct {
 }
 
 // newMapIterNodes returns a range iterator for a map of nodes.
+// The returned mapIter must not have its line or weightedLine methods called.
 func newMapIterNodes(m map[int64]graph.Node) *mapIter {
 	return &mapIter{m: eface(m)}
 }
 
 // newMapIterEdges returns a range iterator for a map of edges.
-// The returned mapIter must not have its node method called.
+// The returned mapIter must not have its node, line or weightedLine methods called.
 func newMapIterEdges(m map[int64]graph.Edge) *mapIter {
 	return &mapIter{m: eface(m)}
 }
 
-// newMapIterWeightedEdges returns a range iterator for a map of edges.
-// The returned mapIter must not have its node method called.
-func newMapIterWeightedEdges(m map[int64]graph.WeightedEdge) *mapIter {
+// newMapIterLines returns a range iterator for a map of line.
+// The returned mapIter must not have its node or weightedLine method called.
+func newMapIterLines(m map[int64]graph.Line) *mapIter {
 	return &mapIter{m: eface(m)}
 }
 
-// newMapIterLines returns a range iterator for a map of edges.
-// The returned mapIter must not have its node method called.
-func newMapIterLines(m map[int64]map[int64]graph.Line) *mapIter {
+// newMapIterWeightedLines returns a range iterator for a map of line.
+// The returned mapIter must not have its node, line or weightedLine methods called.
+func newMapIterWeightedLines(m map[int64]graph.WeightedLine) *mapIter {
 	return &mapIter{m: eface(m)}
 }
 
-// newMapIterWeightedLines returns a range iterator for a map of edges.
-// The returned mapIter must not have its node method called.
-func newMapIterWeightedLines(m map[int64]map[int64]graph.WeightedLine) *mapIter {
+// newMapIterByWeightedEdges returns a range iterator for a map of edges.
+// The returned mapIter must not have its node, line or weightedLine methods called.
+func newMapIterByWeightedEdges(m map[int64]graph.WeightedEdge) *mapIter {
+	return &mapIter{m: eface(m)}
+}
+
+// newMapIterByLines returns a range iterator for a map of edges.
+// The returned mapIter must not have its node, line or weightedLine methods called.
+func newMapIterByLines(m map[int64]map[int64]graph.Line) *mapIter {
+	return &mapIter{m: eface(m)}
+}
+
+// newMapIterByWeightedLines returns a range iterator for a map of edges.
+// The returned mapIter must not have its node, line or weightedLine methods called.
+func newMapIterByWeightedLines(m map[int64]map[int64]graph.WeightedLine) *mapIter {
 	return &mapIter{m: eface(m)}
 }
 
@@ -82,6 +95,28 @@ func (it *mapIter) node() graph.Node {
 	return *(*graph.Node)(mapiterelem(it.it))
 }
 
+// line returns the value of the iterator's current map entry.
+func (it *mapIter) line() graph.Line {
+	if it.it == nil {
+		panic("mapIter.line called before next")
+	}
+	if mapiterkey(it.it) == nil {
+		panic("mapIter.line called on exhausted iterator")
+	}
+	return *(*graph.Line)(mapiterelem(it.it))
+}
+
+// weightedLine returns the value of the iterator's current map entry.
+func (it *mapIter) weightedLine() graph.WeightedLine {
+	if it.it == nil {
+		panic("mapIter.weightedLine called before next")
+	}
+	if mapiterkey(it.it) == nil {
+		panic("mapIter.weightedLine called on exhausted iterator")
+	}
+	return *(*graph.WeightedLine)(mapiterelem(it.it))
+}
+
 // next advances the map iterator and reports whether there is another
 // entry. It returns false when the iterator is exhausted; subsequent
 // calls to Key, Value, or next will panic.
diff --git a/graph/iterator/nodes_map.go b/graph/iterator/nodes_map.go
index c919b136..029d59dd 100644
--- a/graph/iterator/nodes_map.go
+++ b/graph/iterator/nodes_map.go
@@ -23,7 +23,7 @@ type Nodes struct {
 // match the graph.Node IDs, and the map keys are not used.
 //
 // Behavior of the Nodes is unspecified if nodes is mutated after
-// the call the NewNodes.
+// the call to NewNodes.
 func NewNodes(nodes map[int64]graph.Node) *Nodes {
 	return &Nodes{nodes: len(nodes), iter: newMapIterNodes(nodes)}
 }
@@ -92,7 +92,7 @@ type NodesByEdge struct {
 // and the map keys are not used.
 //
 // Behavior of the NodesByEdge is unspecified if nodes or edges
-// is mutated after the call the NewNodes.
+// is mutated after the call to NewNodes.
 func NewNodesByEdge(nodes map[int64]graph.Node, edges map[int64]graph.Edge) *NodesByEdge {
 	return &NodesByEdge{nodes: nodes, edges: len(edges), iter: newMapIterEdges(edges)}
 }
@@ -105,9 +105,9 @@ func NewNodesByEdge(nodes map[int64]graph.Node, edges map[int64]graph.Edge) *Nod
 // and the map keys are not used.
 //
 // Behavior of the NodesByEdge is unspecified if nodes or edges
-// is mutated after the call the NewNodes.
+// is mutated after the call to NewNodes.
 func NewNodesByWeightedEdge(nodes map[int64]graph.Node, edges map[int64]graph.WeightedEdge) *NodesByEdge {
-	return &NodesByEdge{nodes: nodes, edges: len(edges), iter: newMapIterWeightedEdges(edges)}
+	return &NodesByEdge{nodes: nodes, edges: len(edges), iter: newMapIterByWeightedEdges(edges)}
 }
 
 // NewNodesByLines returns a NodesByEdge initialized with the
@@ -118,9 +118,9 @@ func NewNodesByWeightedEdge(nodes map[int64]graph.Node, edges map[int64]graph.We
 // and the map keys are not used.
 //
 // Behavior of the NodesByEdge is unspecified if nodes or lines
-// is mutated after the call the NewNodes.
+// is mutated after the call to NewNodes.
 func NewNodesByLines(nodes map[int64]graph.Node, lines map[int64]map[int64]graph.Line) *NodesByEdge {
-	return &NodesByEdge{nodes: nodes, edges: len(lines), iter: newMapIterLines(lines)}
+	return &NodesByEdge{nodes: nodes, edges: len(lines), iter: newMapIterByLines(lines)}
 }
 
 // NewNodesByWeightedLines returns a NodesByEdge initialized with the
@@ -131,9 +131,9 @@ func NewNodesByLines(nodes map[int64]graph.Node, lines map[int64]map[int64]graph
 // and the map keys are not used.
 //
 // Behavior of the NodesByEdge is unspecified if nodes or lines
-// is mutated after the call the NewNodes.
+// is mutated after the call to NewNodes.
 func NewNodesByWeightedLines(nodes map[int64]graph.Node, lines map[int64]map[int64]graph.WeightedLine) *NodesByEdge {
-	return &NodesByEdge{nodes: nodes, edges: len(lines), iter: newMapIterWeightedLines(lines)}
+	return &NodesByEdge{nodes: nodes, edges: len(lines), iter: newMapIterByWeightedLines(lines)}
 }
 
 // Len returns the remaining number of nodes to be iterated over.
diff --git a/graph/iterator/nodes_map_safe.go b/graph/iterator/nodes_map_safe.go
index d097dc2e..c3b551c0 100644
--- a/graph/iterator/nodes_map_safe.go
+++ b/graph/iterator/nodes_map_safe.go
@@ -27,7 +27,7 @@ type Nodes struct {
 // match the graph.Node IDs, and the map keys are not used.
 //
 // Behavior of the Nodes is unspecified if nodes is mutated after
-// the call the NewNodes.
+// the call to NewNodes.
 func NewNodes(nodes map[int64]graph.Node) *Nodes {
 	rv := reflect.ValueOf(nodes)
 	return &Nodes{nodes: rv, iter: rv.MapRange()}
@@ -97,7 +97,7 @@ type NodesByEdge struct {
 // and the map keys are not used.
 //
 // Behavior of the NodesByEdge is unspecified if nodes or edges
-// is mutated after the call the NewNodes.
+// is mutated after the call to NewNodes.
 func NewNodesByEdge(nodes map[int64]graph.Node, edges map[int64]graph.Edge) *NodesByEdge {
 	rv := reflect.ValueOf(edges)
 	return &NodesByEdge{nodes: nodes, edges: rv, iter: rv.MapRange()}
@@ -111,7 +111,7 @@ func NewNodesByEdge(nodes map[int64]graph.Node, edges map[int64]graph.Edge) *Nod
 // and the map keys are not used.
 //
 // Behavior of the NodesByEdge is unspecified if nodes or edges
-// is mutated after the call the NewNodes.
+// is mutated after the call to NewNodes.
 func NewNodesByWeightedEdge(nodes map[int64]graph.Node, edges map[int64]graph.WeightedEdge) *NodesByEdge {
 	rv := reflect.ValueOf(edges)
 	return &NodesByEdge{nodes: nodes, edges: rv, iter: rv.MapRange()}
@@ -125,7 +125,7 @@ func NewNodesByWeightedEdge(nodes map[int64]graph.Node, edges map[int64]graph.We
 // and the map keys are not used.
 //
 // Behavior of the NodesByEdge is unspecified if nodes or lines
-// is mutated after the call the NewNodes.
+// is mutated after the call to NewNodes.
 func NewNodesByLines(nodes map[int64]graph.Node, lines map[int64]map[int64]graph.Line) *NodesByEdge {
 	rv := reflect.ValueOf(lines)
 	return &NodesByEdge{nodes: nodes, edges: rv, iter: rv.MapRange()}
@@ -139,7 +139,7 @@ func NewNodesByLines(nodes map[int64]graph.Node, lines map[int64]map[int64]graph
 // and the map keys are not used.
 //
 // Behavior of the NodesByEdge is unspecified if nodes or lines
-// is mutated after the call the NewNodes.
+// is mutated after the call to NewNodes.
 func NewNodesByWeightedLines(nodes map[int64]graph.Node, lines map[int64]map[int64]graph.WeightedLine) *NodesByEdge {
 	rv := reflect.ValueOf(lines)
 	return &NodesByEdge{nodes: nodes, edges: rv, iter: rv.MapRange()}
diff --git a/graph/multi/directed.go b/graph/multi/directed.go
index a1caed94..ef9ab1d4 100644
--- a/graph/multi/directed.go
+++ b/graph/multi/directed.go
@@ -69,24 +69,24 @@ func (g *DirectedGraph) Edge(uid, vid int64) graph.Edge {
 
 // Edges returns all the edges in the graph. Each edge in the returned slice
 // is a multi.Edge.
+//
+// The returned graph.Edges is only valid until the next mutation of
+// the receiver.
 func (g *DirectedGraph) Edges() graph.Edges {
 	if len(g.nodes) == 0 {
 		return graph.Empty
 	}
 	var edges []graph.Edge
-	for _, u := range g.nodes {
-		for _, e := range g.from[u.ID()] {
-			var lines []graph.Line
-			for _, l := range e {
-				lines = append(lines, l)
-			}
-			if len(lines) != 0 {
-				edges = append(edges, Edge{
-					F:     g.Node(u.ID()),
-					T:     g.Node(lines[0].To().ID()),
-					Lines: iterator.NewOrderedLines(lines),
-				})
+	for uid, u := range g.nodes {
+		for vid, lines := range g.from[u.ID()] {
+			if len(lines) == 0 {
+				continue
 			}
+			edges = append(edges, Edge{
+				F:     g.Node(uid),
+				T:     g.Node(vid),
+				Lines: iterator.NewLines(lines),
+			})
 		}
 	}
 	if len(edges) == 0 {
diff --git a/graph/multi/undirected.go b/graph/multi/undirected.go
index 781143cd..a0b0a627 100644
--- a/graph/multi/undirected.go
+++ b/graph/multi/undirected.go
@@ -72,31 +72,29 @@ func (g *UndirectedGraph) EdgeBetween(xid, yid int64) graph.Edge {
 
 // Edges returns all the edges in the graph. Each edge in the returned slice
 // is a multi.Edge.
+//
+// The returned graph.Edges is only valid until the next mutation of
+// the receiver.
 func (g *UndirectedGraph) Edges() graph.Edges {
 	if len(g.lines) == 0 {
 		return graph.Empty
 	}
 	var edges []graph.Edge
 	for xid, u := range g.lines {
-		for yid, e := range u {
+		for yid, lines := range u {
 			if yid < xid {
 				// Do not consider lines when the To node ID is
 				// before the From node ID. Both orientations
 				// are stored.
 				continue
 			}
-			// TODO(kortschak): Add iterator.Lines and use that here.
-			if len(e) == 0 {
+			if len(lines) == 0 {
 				continue
 			}
-			lines := make([]graph.Line, 0, len(e))
-			for _, l := range e {
-				lines = append(lines, l)
-			}
 			edges = append(edges, Edge{
 				F:     g.Node(xid),
 				T:     g.Node(yid),
-				Lines: iterator.NewOrderedLines(lines),
+				Lines: iterator.NewLines(lines),
 			})
 		}
 	}
diff --git a/graph/multi/weighted_directed.go b/graph/multi/weighted_directed.go
index 348b2058..f4786ca3 100644
--- a/graph/multi/weighted_directed.go
+++ b/graph/multi/weighted_directed.go
@@ -74,25 +74,25 @@ func (g *WeightedDirectedGraph) Edge(uid, vid int64) graph.Edge {
 
 // Edges returns all the edges in the graph. Each edge in the returned slice
 // is a multi.WeightedEdge.
+//
+// The returned graph.Edges is only valid until the next mutation of
+// the receiver.
 func (g *WeightedDirectedGraph) Edges() graph.Edges {
 	if len(g.nodes) == 0 {
 		return graph.Empty
 	}
 	var edges []graph.Edge
-	for _, u := range g.nodes {
-		for _, e := range g.from[u.ID()] {
-			var lines []graph.WeightedLine
-			for _, l := range e {
-				lines = append(lines, l)
-			}
-			if len(lines) != 0 {
-				edges = append(edges, WeightedEdge{
-					F:             g.Node(u.ID()),
-					T:             g.Node(lines[0].To().ID()),
-					WeightedLines: iterator.NewOrderedWeightedLines(lines),
-					WeightFunc:    g.EdgeWeightFunc,
-				})
+	for uid, u := range g.nodes {
+		for vid, lines := range g.from[u.ID()] {
+			if len(lines) == 0 {
+				continue
 			}
+			edges = append(edges, WeightedEdge{
+				F:             g.Node(uid),
+				T:             g.Node(vid),
+				WeightedLines: iterator.NewWeightedLines(lines),
+				WeightFunc:    g.EdgeWeightFunc,
+			})
 		}
 	}
 	if len(edges) == 0 {
@@ -334,25 +334,25 @@ func (g *WeightedDirectedGraph) WeightedEdge(uid, vid int64) graph.WeightedEdge
 
 // WeightedEdges returns all the edges in the graph. Each edge in the returned slice
 // is a multi.WeightedEdge.
+//
+// The returned graph.WeightedEdges is only valid until the next mutation of
+// the receiver.
 func (g *WeightedDirectedGraph) WeightedEdges() graph.WeightedEdges {
 	if len(g.nodes) == 0 {
 		return graph.Empty
 	}
 	var edges []graph.WeightedEdge
-	for _, u := range g.nodes {
-		for _, e := range g.from[u.ID()] {
-			var lines []graph.WeightedLine
-			for _, l := range e {
-				lines = append(lines, l)
-			}
-			if len(lines) != 0 {
-				edges = append(edges, WeightedEdge{
-					F:             g.Node(u.ID()),
-					T:             g.Node(lines[0].To().ID()),
-					WeightedLines: iterator.NewOrderedWeightedLines(lines),
-					WeightFunc:    g.EdgeWeightFunc,
-				})
+	for uid, u := range g.nodes {
+		for vid, lines := range g.from[u.ID()] {
+			if len(lines) == 0 {
+				continue
 			}
+			edges = append(edges, WeightedEdge{
+				F:             g.Node(uid),
+				T:             g.Node(vid),
+				WeightedLines: iterator.NewWeightedLines(lines),
+				WeightFunc:    g.EdgeWeightFunc,
+			})
 		}
 	}
 	if len(edges) == 0 {
diff --git a/graph/multi/weighted_undirected.go b/graph/multi/weighted_undirected.go
index c52ba383..f2998b53 100644
--- a/graph/multi/weighted_undirected.go
+++ b/graph/multi/weighted_undirected.go
@@ -76,32 +76,30 @@ func (g *WeightedUndirectedGraph) EdgeBetween(xid, yid int64) graph.Edge {
 }
 
 // Edges returns all the edges in the graph. Each edge in the returned slice
-// is a multi.Edge.
+// is a multi.WeightedEdge.
+//
+// The returned graph.Edges is only valid until the next mutation of
+// the receiver.
 func (g *WeightedUndirectedGraph) Edges() graph.Edges {
 	if len(g.lines) == 0 {
 		return graph.Empty
 	}
 	var edges []graph.Edge
 	for xid, u := range g.lines {
-		for yid, e := range u {
+		for yid, lines := range u {
 			if yid < xid {
 				// Do not consider lines when the To node ID is
 				// before the From node ID. Both orientations
 				// are stored.
 				continue
 			}
-			// TODO(kortschak): Add iterator.WeightedLines and use that here.
-			if len(e) == 0 {
+			if len(lines) == 0 {
 				continue
 			}
-			lines := make([]graph.WeightedLine, 0, len(e))
-			for _, l := range e {
-				lines = append(lines, l)
-			}
 			edges = append(edges, WeightedEdge{
 				F:             g.Node(xid),
 				T:             g.Node(yid),
-				WeightedLines: iterator.NewOrderedWeightedLines(lines),
+				WeightedLines: iterator.NewWeightedLines(lines),
 				WeightFunc:    g.EdgeWeightFunc,
 			})
 		}
@@ -341,32 +339,30 @@ func (g *WeightedUndirectedGraph) WeightedEdgeBetween(xid, yid int64) graph.Weig
 }
 
 // WeightedEdges returns all the edges in the graph. Each edge in the returned slice
-// is a multi.Edge.
+// is a multi.WeightedEdge.
+//
+// The returned graph.WeightedEdges is only valid until the next mutation of
+// the receiver.
 func (g *WeightedUndirectedGraph) WeightedEdges() graph.WeightedEdges {
 	if len(g.lines) == 0 {
 		return graph.Empty
 	}
 	var edges []graph.WeightedEdge
 	for xid, u := range g.lines {
-		for yid, e := range u {
+		for yid, lines := range u {
 			if yid < xid {
 				// Do not consider lines when the To node ID is
 				// before the From node ID. Both orientations
 				// are stored.
 				continue
 			}
-			// TODO(kortschak): Add iterator.WeightedLines and use that here.
-			if len(e) == 0 {
+			if len(lines) == 0 {
 				continue
 			}
-			lines := make([]graph.WeightedLine, 0, len(e))
-			for _, l := range e {
-				lines = append(lines, l)
-			}
 			edges = append(edges, WeightedEdge{
 				F:             g.Node(xid),
 				T:             g.Node(yid),
-				WeightedLines: iterator.NewOrderedWeightedLines(lines),
+				WeightedLines: iterator.NewWeightedLines(lines),
 				WeightFunc:    g.EdgeWeightFunc,
 			})
 		}
diff --git a/spatial/r3/vector.go b/spatial/r3/vector.go
index 46d64922..96cdbbf5 100644
--- a/spatial/r3/vector.go
+++ b/spatial/r3/vector.go
@@ -7,6 +7,7 @@ package r3
 import (
 	"math"
 
+	"gonum.org/v1/gonum/blas/blas64"
 	"gonum.org/v1/gonum/mat"
 	"gonum.org/v1/gonum/num/quat"
 )
@@ -155,9 +156,32 @@ func (r Rotation) Matrix() mat.Matrix {
 	ijm := im * jm
 	jkm := jm * km
 	kim := km * im
-	return mat.NewDense(3, 3, []float64{
+	return &matrix{
 		1 - 2*(jm2+km2), 2 * (ijm - rkm), 2 * (kim + rjm),
 		2 * (ijm + rkm), 1 - 2*(im2+km2), 2 * (jkm - rim),
 		2 * (kim - rjm), 2 * (jkm + rim), 1 - 2*(im2+jm2),
-	})
+	}
+}
+
+// matrix is a 3×3 rotation matrix.
+type matrix [9]float64
+
+var (
+	_ mat.Matrix      = (*matrix)(nil)
+	_ mat.RawMatrixer = (*matrix)(nil)
+)
+
+func (m *matrix) At(i, j int) float64 {
+	if uint(i) >= 3 {
+		panic(mat.ErrRowAccess)
+	}
+	if uint(j) >= 3 {
+		panic(mat.ErrColAccess)
+	}
+	return m[i*3+j]
+}
+func (m *matrix) Dims() (r, c int) { return 3, 3 }
+func (m *matrix) T() mat.Matrix    { return mat.Transpose{Matrix: m} }
+func (m *matrix) RawMatrix() blas64.General {
+	return blas64.General{Rows: 3, Cols: 3, Data: m[:], Stride: 3}
 }


Test patch:
diff --git a/graph/iterator/lines_test.go b/graph/iterator/lines_test.go
index 7d982588..ce0ba406 100644
--- a/graph/iterator/lines_test.go
+++ b/graph/iterator/lines_test.go
@@ -6,6 +6,7 @@ package iterator_test
 
 import (
 	"reflect"
+	"sort"
 	"testing"
 
 	"gonum.org/v1/gonum/graph"
@@ -13,12 +14,63 @@ import (
 	"gonum.org/v1/gonum/graph/simple"
 )
 
-type line struct{ f, t int }
+type line struct{ f, t, id int64 }
 
 func (l line) From() graph.Node         { return simple.Node(l.f) }
 func (l line) To() graph.Node           { return simple.Node(l.t) }
-func (l line) ReversedLine() graph.Line { return line{f: l.t, t: l.f} }
-func (l line) ID() int64                { return 1 }
+func (l line) ReversedLine() graph.Line { l.f, l.t = l.t, l.f; return l }
+func (l line) ID() int64                { return l.id }
+
+var linesTests = []struct {
+	lines map[int64]graph.Line
+}{
+	{lines: nil},
+	{lines: map[int64]graph.Line{1: line{f: 1, t: 2, id: 1}}},
+	{lines: map[int64]graph.Line{1: line{f: 1, t: 2, id: 1}, 2: line{f: 2, t: 3, id: 2}, 3: line{f: 3, t: 4, id: 3}, 4: line{f: 4, t: 5, id: 4}}},
+	{lines: map[int64]graph.Line{4: line{f: 5, t: 4, id: 4}, 3: line{f: 4, t: 3, id: 3}, 2: line{f: 3, t: 2, id: 2}, 1: line{f: 2, t: 1, id: 1}}},
+}
+
+func TestLinesIterate(t *testing.T) {
+	for _, test := range linesTests {
+		it := iterator.NewLines(test.lines)
+		for i := 0; i < 2; i++ {
+			if it.Len() != len(test.lines) {
+				t.Errorf("unexpected iterator length for round %d: got:%d want:%d", i, it.Len(), len(test.lines))
+			}
+			var got map[int64]graph.Line
+			if it.Len() != 0 {
+				got = make(map[int64]graph.Line)
+			}
+			for it.Next() {
+				got[it.Line().ID()] = it.Line()
+			}
+			want := test.lines
+			if !reflect.DeepEqual(got, want) {
+				t.Errorf("unexpected iterator output for round %d: got:%#v want:%#v", i, got, want)
+			}
+			it.Reset()
+		}
+	}
+}
+
+func TestLinesSlice(t *testing.T) {
+	for _, test := range linesTests {
+		it := iterator.NewLines(test.lines)
+		for i := 0; i < 2; i++ {
+			got := it.LineSlice()
+			var want []graph.Line
+			for _, l := range test.lines {
+				want = append(want, l)
+			}
+			sort.Slice(got, func(i, j int) bool { return got[i].ID() < got[j].ID() })
+			sort.Slice(want, func(i, j int) bool { return want[i].ID() < want[j].ID() })
+			if !reflect.DeepEqual(got, want) {
+				t.Errorf("unexpected iterator output for round %d: got:%#v want:%#v", i, got, want)
+			}
+			it.Reset()
+		}
+	}
+}
 
 var orderedLinesTests = []struct {
 	lines []graph.Line
@@ -64,15 +116,66 @@ func TestOrderedLinesSlice(t *testing.T) {
 }
 
 type weightedLine struct {
-	f, t int
-	w    float64
+	f, t, id int64
+	w        float64
 }
 
 func (l weightedLine) From() graph.Node         { return simple.Node(l.f) }
 func (l weightedLine) To() graph.Node           { return simple.Node(l.t) }
 func (l weightedLine) ReversedLine() graph.Line { l.f, l.t = l.t, l.f; return l }
 func (l weightedLine) Weight() float64          { return l.w }
-func (l weightedLine) ID() int64                { return 1 }
+func (l weightedLine) ID() int64                { return l.id }
+
+var weightedLinesTests = []struct {
+	lines map[int64]graph.WeightedLine
+}{
+	{lines: nil},
+	{lines: map[int64]graph.WeightedLine{2: weightedLine{f: 1, t: 2, w: 1, id: 2}}},
+	{lines: map[int64]graph.WeightedLine{2: weightedLine{f: 1, t: 2, w: 1, id: 2}, 4: weightedLine{f: 2, t: 3, w: 2, id: 4}, 6: weightedLine{f: 3, t: 4, w: 3, id: 6}, 8: weightedLine{f: 4, t: 5, w: 4, id: 8}}},
+	{lines: map[int64]graph.WeightedLine{8: weightedLine{f: 5, t: 4, w: 4, id: 8}, 6: weightedLine{f: 4, t: 3, w: 3, id: 6}, 4: weightedLine{f: 3, t: 2, w: 2, id: 4}, 2: weightedLine{f: 2, t: 1, w: 1, id: 2}}},
+}
+
+func TestWeightedLinesIterate(t *testing.T) {
+	for _, test := range weightedLinesTests {
+		it := iterator.NewWeightedLines(test.lines)
+		for i := 0; i < 2; i++ {
+			if it.Len() != len(test.lines) {
+				t.Errorf("unexpected iterator length for round %d: got:%d want:%d", i, it.Len(), len(test.lines))
+			}
+			var got map[int64]graph.WeightedLine
+			if it.Len() != 0 {
+				got = make(map[int64]graph.WeightedLine)
+			}
+			for it.Next() {
+				got[it.WeightedLine().ID()] = it.WeightedLine()
+			}
+			want := test.lines
+			if !reflect.DeepEqual(got, want) {
+				t.Errorf("unexpected iterator output for round %d: got:%#v want:%#v", i, got, want)
+			}
+			it.Reset()
+		}
+	}
+}
+
+func TestWeightedLinesSlice(t *testing.T) {
+	for _, test := range weightedLinesTests {
+		it := iterator.NewWeightedLines(test.lines)
+		for i := 0; i < 2; i++ {
+			got := it.WeightedLineSlice()
+			var want []graph.WeightedLine
+			for _, l := range test.lines {
+				want = append(want, l)
+			}
+			sort.Slice(got, func(i, j int) bool { return got[i].ID() < got[j].ID() })
+			sort.Slice(want, func(i, j int) bool { return want[i].ID() < want[j].ID() })
+			if !reflect.DeepEqual(got, want) {
+				t.Errorf("unexpected iterator output for round %d: got:%#v want:%#v", i, got, want)
+			}
+			it.Reset()
+		}
+	}
+}
 
 var orderedWeightedLinesTests = []struct {
 	lines []graph.WeightedLine
diff --git a/spatial/r3/euler_example_test.go b/spatial/r3/euler_example_test.go
new file mode 100644
index 00000000..fc69aaa0
--- /dev/null
+++ b/spatial/r3/euler_example_test.go
@@ -0,0 +1,77 @@
+// Copyright ©2021 The Gonum Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package r3_test
+
+import (
+	"fmt"
+	"math"
+
+	"gonum.org/v1/gonum/num/quat"
+	"gonum.org/v1/gonum/spatial/r3"
+)
+
+// euler returns an r3.Rotation that corresponds to the Euler
+// angles alpha, beta and gamma which are rotations around the x,
+// y and z axes respectively. The order of rotations is x, y, z;
+// there are many conventions for this ordering.
+func euler(alpha, beta, gamma float64) r3.Rotation {
+	// Note that this function can be algebraically simplified
+	// to reduce floating point operations, but is left in this
+	// form for clarity.
+	var rot1, rot2, rot3 quat.Number
+	rot1.Imag, rot1.Real = math.Sincos(alpha / 2) // x-axis rotation
+	rot2.Jmag, rot2.Real = math.Sincos(beta / 2)  // y-axis rotation
+	rot3.Kmag, rot3.Real = math.Sincos(gamma / 2) // z-axis rotation
+
+	return r3.Rotation(quat.Mul(rot3, quat.Mul(rot2, rot1))) // order of rotations
+}
+
+func ExampleRotation_eulerAngles() {
+	// It is possible to interconvert between the quaternion representation
+	// of a rotation and Euler angles, but this leads to problems.
+	//
+	// The first of these is that there are a variety of conventions for
+	// application of the rotations.
+	//
+	// The more serious consequence of using Euler angles is that it is
+	// possible to put the rotation system into a singularity which results
+	// in loss of degrees of freedom and so causes gimbal lock. This happens
+	// when the second axis to be rotated around is rotated to 𝝿/2.
+	//
+	// See https://en.wikipedia.org/wiki/Euler_angles for more details.
+
+	pt := r3.Vec{1, 0, 0}
+
+	// For the Euler conversion function in this example, the second rotation
+	// is around the y-axis.
+	const singularY = math.Pi / 2
+
+	arb := math.Pi / 4
+
+	fmt.Printf("rotate around x-axis: %.2f\n", euler(arb, 0, 0).Rotate(pt))
+	fmt.Printf("rotate around y-axis: %.2f\n", euler(0, arb, 0).Rotate(pt))
+	fmt.Printf("rotate around z-axis: %.2f\n", euler(0, 0, arb).Rotate(pt))
+	fmt.Printf("rotate around x+y-axes: %.2f\n", euler(arb, arb, 0).Rotate(pt))
+	fmt.Printf("rotate around x+z-axes: %.2f\n", euler(arb, 0, arb).Rotate(pt))
+	fmt.Printf("rotate around y+z-axes: %.2f\n", euler(0, arb, arb).Rotate(pt))
+
+	fmt.Printf("rotate around y-axis to singularity: %.2f\n", euler(0, singularY, 0).Rotate(pt))
+	fmt.Printf("rotate around x+y-axes with singularity → gimbal lock: %.2f\n", euler(arb, singularY, 0).Rotate(pt))
+	fmt.Printf("rotate around z+y-axes with singularity → gimbal lock: %.2f\n", euler(0, singularY, arb).Rotate(pt))
+	fmt.Printf("rotate around all-axes with singularity → gimbal lock: %.2f\n", euler(arb, singularY, arb).Rotate(pt))
+
+	// Output:
+	//
+	// rotate around x-axis: {1.00 0.00 0.00}
+	// rotate around y-axis: {0.71 0.00 -0.71}
+	// rotate around z-axis: {0.71 0.71 0.00}
+	// rotate around x+y-axes: {0.71 0.00 -0.71}
+	// rotate around x+z-axes: {0.71 0.71 0.00}
+	// rotate around y+z-axes: {0.50 0.50 -0.71}
+	// rotate around y-axis to singularity: {0.00 0.00 -1.00}
+	// rotate around x+y-axes with singularity → gimbal lock: {0.00 0.00 -1.00}
+	// rotate around z+y-axes with singularity → gimbal lock: {0.00 0.00 -1.00}
+	// rotate around all-axes with singularity → gimbal lock: {0.00 0.00 -1.00}
+}