Why verbs and participles

A method that changes in place and the method that returns a changed copy are different words, sort against sorted, because value semantics make one name for both ambiguous at the call site.

sort changes a list in place; sorted returns a new one. Two words for what looks like one idea cost every reader a second name to learn, so this page argues for why the two words earn their keep.

The decision#

A verb changes its receiver in place and is a var fn; its participle reads its receiver and returns a changed copy, under a different name. append/appended, sort/sorted, remove/removed, insert/inserted.

  • The participles are default members of the collection traits, written once in terms of the verb: copy, change the copy, return it. An implementation only has to write the verb.
  • Calling the verb through a const path is a compile error that names the participle, so the fix is one word away.
var numbers = [3, 1, 2]
numbers.sort { _ }
print numbers

const numbers2 = [3, 1, 2]
const ascending = numbers2.sorted({ _ }).toList()
print "{numbers2} {ascending}"

Why#

Because value semantics make one name for both readings ambiguous at the call site, not just in documentation. Under mutable value semantics list.sort() and const sorted = list.sort() would both be legal syntax with one shared name: the first line changes list, the second one only looks like it does. A single overloaded sort cannot tell a reader which happened without them checking whether the result was used - exactly the ambiguity Sort.sort in languages with reference semantics never has, because there a mutating method returning Void already signals "this is the only effect."

Because the wrong one is now a compile error instead of a quiet copy. Calling the verb on a const binding fails immediately and names the participle in the message, so the fix is mechanical:

Does not compile
const numbers = [3, 1, 2]
numbers.sort { _ }
// error: `sort` needs a `var`

Because a name says what happens without the reader tracing the return value. list.sorted() reads as "the sorted list," which is what it returns; list.sort() reads as an instruction, which is what it does. Neither name needs the call site's context (was the result assigned? was it discarded?) to be understood, unlike a single sort that means either depending on how it is used.

Because it costs nothing per implementation. The participles are default members of List, Set and Map, each written once against the verb they pair with. A type that implements append gets appended for free; nobody writes the copy-and-call-through logic twice.

What was rejected#

  • One name plus a copy { ... } block to opt into a copy where needed. Rejected because it moves the ambiguity from the method name to a block that is easy to forget, and the forgetting is silent - the call still compiles and changes the original.
  • Ruby's ! suffix for the mutating form. Rejected because a single punctuation mark is easy to miss in review and reads as an intensity marker rather than a different operation; sort/sorted needs no footnote to explain what the mark means.
  • Scala's symbolic operators for the copy-returning side. Rejected for the same reason bit operators were (see Why the bit operators bind like arithmetic): a symbol needs a legend, a word does not.

Consequences#

Every collection member that could mutate has two names to choose between, and the table is fixed rather than invented per type. append/appended, remove/removed, sort/sorted, insert/inserted - one pair per meaning, spelled the same on every collection, and a participle answers Self so it can be chained. See Verbs and participles for the complete table.

A type built on the collection traits writes the verb once and gets the participle for free, which is what keeps the standard library from doubling in size:

type Point {
  var x: Int
  var y: Int

  var fn translate(deltaX: Int, deltaY: Int) {
    x = x + deltaX
    y = y + deltaY
  }

  fn translated(deltaX: Int, deltaY: Int): Point {
    copy(x: x + deltaX, y: y + deltaY)
  }
}

var point = Point x: 0, y: 0
point.translate deltaX: 1, deltaY: 1
const moved = Point(x: 0, y: 0).translated(deltaX: 1, deltaY: 1)
print "{point} {moved}"

A new verb/participle pair follows the same rule when a type introduces one of its own: the verb is a var fn and changes the receiver, the participle only reads it, is named for what it returns, and is typically written in terms of the verb through copy.

Esc