Skip to content

Instantly share code, notes, and snippets.

@griotspeak
Last active February 17, 2017 18:37
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save griotspeak/31445ddcdba44bb8de599be6c9a93bd1 to your computer and use it in GitHub Desktop.
Save griotspeak/31445ddcdba44bb8de599be6c9a93bd1 to your computer and use it in GitHub Desktop.
Pure Functions

Pure Functions

  • Proposal: SE-NNNN
  • Author(s): TJ Usiyan
  • Status: Awaiting review
  • Review manager: TBD

Introduction

Some functions are, essentially, only meant to be transformations of their input and–as such–do not and should not reference any variables other than those passed in. These same functions are not meant to have any effects other than the aforementioned transformation of input. Currently, Swift cannot assist the developer and confirm that any given function is one of these 'pure' functions. To facilitate this, this proposal adds syntax to signal that a function is 'pure'.

'pure', in this context, means:

  1. The function must explicitly express return value. (No implied return of Void)
  2. This function can only call other pure functions
  3. This function cannot access/modify global or static variables.

Motivation

Example 1 - Inner functions

Consider the following example where _computeNullability(of:) is meant to create its output solely based on the provided recognizer.

class Recognizer {
	var nullabilityMemo: Bool?
	var isNullable: Bool {
		func _computeNullability(of recognizer: Recognizer) -> Bool {…}
		if let back = nullabilityMemo {
			return back		
		} else {
			let back =  _computeNullability(of: self)
			nullabilityMemo = back
			return back
		}
	}
}

if _computeNullability(of:) is recursive at all, there exists a real potential to accidentally reference self in its body and the mistake, depending on circumstance, can be terribly subtle. Converting _computeNullability(of:) to a static function is an option but obfuscates the fact that it is only to be called within isNullable.

Example 2

The difference between… func update(_ m: Instance) -> Instance and… func update(_ m: Instance) => Instance

… is that the caller can expect the second function to leave m unchanged where the caller could reasonably expect m is updated and then returned by the first version.

Proposed solution

Given the ability to indicate that _computeNullability(of:) is a 'pure' function, the developer gains assurance from the tooling that it doesn't reference anything or cause any side effects.

class Recognizer {
	var nullabilityMemo: Bool?
	var isNullable: Bool {
		func _computeNullability(of recognizer: Recognizer) => Bool {…}
		if let back = nullabilityMemo {
			return back		
		} else {
			let back =  _computeNullability(of: self)
			nullabilityMemo = back
			return back
		}
	}
}

Detailed design

This proposal introduces a new annotation =>, which is to be accepted everywhere -> currently is. Members created using this kewyord must follow the rules listed in the introduction.

Types with Reference Semantics as parameters.

Passing references into pure functions will be allowed but, in keeping with rule 2 above, only pure methods will be invokable.

inout parameters

Passing inout parameters will be allowed. The requirement of an explicitly stated return value permits func lowpass(input: inout Double) => Void

Impact on existing code

This is an additive feature unless alternative 2 is chosen and, as such, should not require an effect on existing code. It could be used to annotate closures accepted by methods in the standard library such as map, filter, and reduce. While this would fit well with their typical use, such a change is not necessarily part of this proposal.

Alternatives considered

It should be noted that neither of these alternatives can remain consistent for inline closures.

  1. keyword pfunc (pronounciation: pifəŋk) for 'pure' functions.
  2. proc keyword for 'impure' functions and 'func' for 'pure' functions. This would be a massively source breaking change and, as such, is unlikely to have any feasibility. It is, however, the most clean semantically, in my opinion.
@rnapier
Copy link

rnapier commented Feb 17, 2017

You say "This function cannot access/modify global or static variables." But this seems too loose a requirement. Later you indicate that modifying self would be impure, but self is not a global or static variable. I assume you really mean "access/modify variables outside its scope."

It would be helpful to include a simple bug in example 1 to demonstrate the motivating example. It's not obvious what subtle bug you're referring to.

Example 2 introduces new syntax (=>) in a way that is not clear. That said, example 2 isn't a very strong argument for this approach. Whether m is mutated or not is expressed by a glyph (-> vs =>) that does not exist at the call site, and is quite subtle in the definition. The fact that the author believes they have expressed something important (by using =>) is undercut by the fact that the caller doesn't see it. Modifying the signature from => to -> causes no compiler errors in any caller, but creates spooky action at a distance. (This would create a new example of my existing complaint about value/reference types and how changing a struct to a class creates spooky action.) This is not disqualifying at all; let vs var is also not clear at the call site. But I'd argue that Example 2 is not a strong reason. (What would be more useful would be common patterns of practice and naming, and of course avoiding reference types :D)

You don't include adding a pure keyword (parallel to override, public, required, etc.) as an alternative considered. I suspect I know why (you don't want to make non-pure the default), but it seems the most obvious and Swift-like implementation, so it definitely needs to be addressed.

I would think that annotating existing stdlib methods with => would be source breaking. For example, the following map is not pure but is currently legal. It may be an abomination, but it is legal, and I would not be surprised to find it in live code.

var greatestElement: Int = 0
let doubles: [Int] = [1,2,3,4,3,2,1].map {
    if $0 > greatestElement { greatestElement = $0 }
    return $0 * 2
}
greatestElement

The following horrible use of filter is likely even common, and is impure.

var previous: Int? = nil
let noRepeats = [1,1,2,3,2,2,4].filter {
    if let p = previous {
        previous = $0
        return p != $0
    } else {
        previous = $0
        return true
    }
}
noRepeats

How does this impact types? Is (Int) => Bool a type? Is it a different type than (Int) -> Bool? If so, is there an ISA relationship (in the same way that I can use a non-throwing closure where a throwing closure is expected)? Can a pure method be used to conform to a non-pure protocol requirement? (I assume the answer to all of this is "yes" and basically follows the pattern of throws.)

Can a pure function accept or return a non-pure function as a parameter? I assume the answer is yes, but it cannot call it.

Can a method overload on =>? Is it legal to have both func f(n: Int) -> Int and func f(n: Int) => Int in the same scope?

Can static/global constants be accessed? Since global constants are lazy, reading them may have side-effects. Does this violate purity? Consider:

// Bob.swift
struct Bob {
    let x: String
}
var testing = false
let globalBob = testing ? Bob(x: "test") : Bob(x: "notTest") // global let, so this will be lazily evaluated

// main.swift
func soPure() => String {
    return globalBob.x // Seems pretty pure. Everything is `let`.
}

testing = true
print(soPure()) // depends not only on `testing`, but whether `globalBob` has been accessed previously.

If this is disallowed, are you forbidden using Float.pi in a pure function? Do we need a way to mark globalBob itself pure (independent of being let)?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment