`let` all the things!

Bertrand Longevialle
4 min readApr 26, 2018

--

Swift lets us declare either constants (let) or variables (var). That’s a neat and easy way to advertise whether value will, or not, change after affectation.

In a word, Swift is declarative; we should use that to write clear code!

Let this be your code: Sturdy and Crystal Clear | Photo by Hao Zhang on Unsplash

The two main points here are declaration and affectation.

A common misconception is that a declaration using let needs to be immediately followed by an affectation.
Often, this results in the use of var with control flows like if or switch.

var variable: Any
if boolean {
variable = foo
} else {
variable = bar
} // never change variable again 😓
print(variable)
return

But if there’s exactly one affectation per codepath, let can — and should! — be used:

let constant: Any
if boolean {
constant = foo
} else {
constant = bar
} // never change it again ☺️
print(constant)
constant = foobar // compiler error 🤗
return

It’s also valid in a switch:

let constant: Any
switch boolean {
case true: constant = foo
case false: constant = bar
} // never change it again ☺️

Clearer Code

That general idea can be applied in many use-cases, such as:

  • getting rid of optionals;
  • flattening a Pyramid of Doom;
  • calling a closure, for each code path;
  • streamlining loops.

Getting Rid of Optionals

class Less {    var b: B?    init(string: String) {
switch string {
case "foo": b = .foo
case "bar": b = .bar
default: b = nil
}
b?.make()
if let someB = b {
use(someB)
}
}
}

becomes:

class Y {    let b: B    init(string: String) throws {
switch string {
case "foo": b = .foo
case "bar": b = .bar
default: throw error
}
b.make()
use(b) // Good bye optional-chainings and if-lets 👋
}
}

Flattening a Pyramid of Doom

More precisely, let implies a single affectation per codepath where the constant is used. This helps handling throwing functions with do-try-catch:

func function() {
do {
let foo = try compute("foo")
use(foo)
do {
let bar = try compute("bar")
use(bar)
} catch {
print(error)
return
}
} catch {
print(error)
return
}

stuff()
}
func compute(string: String) throws -> C

becomes:

func f() {    let foo: Any
let bar: Any
do {
foo = try compute("foo")
bar = try compute("bar")
} catch {
print(error)
return
}
/// At this point, either foo and bar are set
/// or f() has returned from the catch statement
[foo, bar].forEach { use($0) } stuff()
}

Much simpler to apprehend!

Calling the closure; every time

func gatherAB(completion: (String, String, ComparisonResult) -> ()){    let a = getA()
let b: String
let c: ComparisonResult
if a == "A" {
b = getB()
if b == a {
c = .orderedSame
completion(a,b,c)
return
}
c = compare(a,b)
} else {
b = getOtherB()
c = a.compare(b)
completion(a,b,c)
}
}

gatherAB() is kind of dumb. But it’s easy to imagine any kind of logic that leads to writing similar conditions-bloated code where a call to completion is easily forgotten. Like here in the case where b != a.

Declaring upfront the intent — here getting a and b and how they compare — can help write complete and sturdy code:

func gatherAB(completion: (String, String, ComparisonResult) -> ()) {
let a: String
let b: String
let c: ComparisonResult
defer { completion(a,b,c) }
a = getA() if a == "A" {
b = getB()
if b == a {
c = .orderedSame;
return
}
c = compare(a,b)
} else {
b = getOtherB()
c = a.compare(b)
}
}

Here, it’s probably overkill. But now completion is guaranteed to be called in all code paths and you get a compiler error if you forgot to set a, b, or c.

Maybe it’s not production code, but rather a tool in debugging or a step in refactoring.

Streamlining Loops

If you’re still reading this, maybe it’s because you want to get rid of as many var as you can 🤗
One last place where it can be done is when using loops; typically:

var accumulator = ""
for(s in ["a", "b", "c", "d"]) {
accumulator += s
}
print(accumulator) // never change it again 😥

Here, using let wouldn’t be correct since accumulator is mutated multiple times (one per loop run). But after the loop, accumulator won’t change. As with if and switch examples at the beginning, that loop is the single affectation of the constant.

So, probably a function can be extracted returning the desired value… Hopefully, functional Swift covers this kind of scenario:

// Don't even need a local accumulator anymore 👌
print(["a", "b", "c", "d"].reduce("", +))

Here it’s reduce but there’s obviously a lot of possiblities like map and others.

There you have it: new tools to get rid of var and, more importantly, make your code a little more self-documenting and sturdy!

How do you like it?

--

--