diff --git a/README.md b/README.md index edda768..bcc0912 100644 --- a/README.md +++ b/README.md @@ -41,6 +41,7 @@ contribute. - [2.16. Public functions SHOULD have an explicit return type](sections/2-language-rules.md#216-public-functions-should-have-an-explicit-return-type) - [2.17. SHOULD NOT define case classes nested in other classes](sections/2-language-rules.md#217-should-not-define-case-classes-nested-in-other-classes) - [2.18. MUST NOT include classes, traits and objects inside package objects](sections/2-language-rules.md#218-must-not-include-classes-traits-and-objects-inside-package-objects) + - [2.19. SHOULD use head/tail and init/last decomposition only if they can be done in constant time and memory](sections/2-language-rules.md#219-should-use-head-tail-and-init-last-decomposition-only-if-they-can-be-done-in-constant-time-and-memory) - [3. Application Architecture](sections/3-architecture.md) - [3.1. SHOULD NOT use the Cake pattern](sections/3-architecture.md#31-should-not-use-the-cake-pattern) diff --git a/sections/2-language-rules.md b/sections/2-language-rules.md index 56eb25f..9cbf1e4 100644 --- a/sections/2-language-rules.md +++ b/sections/2-language-rules.md @@ -645,3 +645,63 @@ package object dsl { } ``` + +### 2.19 SHOULD use head/tail and init/last decomposition only if they can be done in constant time and memory + +Example of head/tail decomposition: + +```scala +def recursiveSumList(numbers: List[Int], accumulator: Int): Int = + numbers match { + case Nil => + accumulator + + case head :: tail => + recursiveSumList(tail, accumulator + head) + } +``` + +`List` has a special head/tail extractor `::` because `List`s are **made** by always appending an element to the front of the list: + +```scala +val numbers = 1 :: 2 :: 3 :: Nil +``` + +This is the same as: + +```scala +val numbers = Nil.::(3).::(2).::(1) +``` + +For this reason, both `head` and `tail` on a list need only constant time and memory! These operations are `O(1)`. + +There is another head/tail extractor called `+:` that works on any `Seq`: + +```scala +def recursiveSumSeq(numbers: Seq[Int], accumulator: Int): Int = + numbers match { + case Nil => + accumulator + + case head +: tail => + recursiveSumSeq(tail, accumulator + head) + } +``` + +You can find the implementation of `+:` [here](https://github.com/scala/scala/blob/v2.12.4/src/library/scala/collection/SeqExtractors.scala). The problem is that other collections than `List` do not necessarily head/tail-decompose in constant time and memory, e.g. an `Array`: + +```scala +val numbers = Array.range(0, 10000000) + +recursiveSumSeq(numbers, 0) +``` + +This is highly inefficient: each `tail` on an `Array` takes `O(n)` time and memory, because every time a new array needs to be created! + +Unfortunately, the Scala collections library permits these kinds of inefficient operations. We have to keep an eye out for them. + +--- + +An example for an efficient init/last decomposition is `scala.collection.immutable.Queue`. It is backed by two `List`s and the efficiency of `head`, `tail`, `init` and `last` is *amortized constant* time and memory, as explained in the [Scala collection performance characteristics](http://docs.scala-lang.org/overviews/collections/performance-characteristics.html). + +I don't think that init/last decomposition is all that common. In general, it is analogue to head/tail decomposition. The init/last deconstructor for any `Seq` is `:+`.