Redefining Everything with the Swift REPL

Our first entry on the REPL covered just the basics, showing how to use the REPL to experiment with Swift as you learn the language. This post explores one way that the REPL bends normal coding rules to give you new powers when developing.

Redefining Identifiers

The Swift compiler automatically protects against a wide range of programming mistakes, including unintentional ambiguity arising from defining the same identifier twice:

swiftc -
var x = "The Answer"
var x = 42
^D
error: invalid redeclaration of 'x'

This makes sense when coding in a non-interactive editor, but in the REPL interactive environment it’s useful to be able to easily make changes. The REPL was specifically designed with this kind of convenience in mind:

  1> var x = "The Answer"
x: String = "The Answer"
  2> var x = 42
x: Int = 42
  3> x + 10
$R0: Int = 52

The newer definition replaces the existing definition for all subsequent references. As illustrated above, even the type of the definition can be changed in the process. This allows a wide range of experiments through iterative refinement. For example, you can start out with a recursive implementation of a function:

  4> func fib(index: Int) -> Int {
  5. 	if index <= 1 {
  6. 		return 1
  7. 	}
  8. 	return fib(index - 1) + fib(index - 2)
  9. }
 10> fib(40)
$R1: Int = 165580141

This is just one way to write this function. You can experiment with your code, trying out different algorithms and APIs. The REPL makes it easy to define a new and improved implementation:

 11> func fib(index: Int) -> Int {
 12. 	var lastValue = 1
 13. 	var currentValue = 1
 14. 	for var iteration = 2; iteration <= index; ++iteration {
 15. 		let newValue = lastValue + currentValue
 16. 		lastValue = currentValue
 17. 		currentValue = newValue
 18. 	}
 19.  	return currentValue
 20. }
 21> fib(40)
$R2: Int = 165580141

Typing the same expression in the REPL now executes the new implementation. This is a simple example, but it illustrates the iterative experimentation that the REPL was designed to facilitate.

Redefinition or Overload?

Redefining constants, variables, and types all work intuitively, and, as we can see above, it is also possible to redefine functions. This raises an obvious question: how does this interact with function overloading? The REPL only replaces an existing definition when it has the same name and signature as shown in the Fibonacci example above. If a function with the same name but a distinct signature already exists, it just defines a new overload. Keep in mind that Swift allows function overloading even when two signatures differ only in their return type. For example:

 22> func foo() {
 23. 	println("Foo!")
 24. }
 25> func foo() -> String {
 26. 	return "Foo!"
 27. }
 28> foo()
error: ambiguous use of 'foo'

The above declarations define two distinct functions that must be called in a manner where only one of the available overloads can be inferred as returning a compatible type:

 28> var foo: String = foo()
foo: String = "Foo!"
 29> foo() as Void
Foo!

Capturing Definitions

The ability to redefine an identifier is powerful, but it only applies to subsequent uses of the identifier. Any line of code that has already been compiled by the REPL retains its reference to the previous definition. It’s as if the new definition obscures the old one but doesn’t eliminate it entirely. The following illustrates how this works in practice:

 30> var message = "Hello, World!"
message: String = "Hello, World!"
 31> func printMessage() {
 32. 	println(message)
 33. }
 34> printMessage()
Hello, World!
 35> message = "Goodbye"
 36> printMessage()
Goodbye
 37> var message = "New Message"
 38> printMessage()
Goodbye
 39> println(message)
New Message

To understand what’s happening here it helps to walk though the example one statement at a time. Line 30 declares a variable named message with a greeting. Lines 31-33 declare a function named printMessage() that prints the contents of the variable declared on line 30. Line 34 calls the method and produces the expected result. So far it’s extremely straightforward.

The subtle distinctions start on line 35 which assigns a new value to the variable declared in line 30, and line 36 which prints this new value as expected. On the other hand, line 37 declares a new variable with the same name. This effectively hides the original variable from all subsequent code, but the call on line 38 invokes a function that was compiled before the redefinition. The function retains its original meaning and prints the value of the original variable, not the newly declared variable. Line 39 shows that the newly defined variable can be referenced, as expected, by new code.

All redefinitions work in this manner, whether they’re redefining a function, a variable, or a type. The REPL grants the freedom to redefine an identifier without restrictions, whereas prior references were compiled with strong semantic checks in place. What would happen if the message identifier in the example above were redefined as a type instead of a variable? The printMessage() function would no longer compile. Rather than ask developers to sort through endless potential edge cases like this, the REPL adheres to a world view that is always self-consistent.

All Blog Posts