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:
- The function must explicitly express return value. (No implied return of
Void
) - This function can only call other pure functions
- This function cannot access/modify global or static variables.
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
.
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.
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
}
}
}
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.
Passing references into pure functions will be allowed but, in keeping with rule 2 above, only pure methods will be invokable.
Passing inout
parameters will be allowed. The requirement of an explicitly stated return value permits
func lowpass(input: inout Double) => Void
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.
It should be noted that neither of these alternatives can remain consistent for inline closures.
- keyword
pfunc
(pronounciation: pifəŋk) for 'pure' functions. 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.
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, butself
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. Whetherm
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
vsvar
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 tooverride
,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 followingmap
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.The following horrible use of
filter
is likely even common, and is impure.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 ofthrows
.)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 bothfunc f(n: Int) -> Int
andfunc 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:
If this is disallowed, are you forbidden using
Float.pi
in a pure function? Do we need a way to markglobalBob
itself pure (independent of beinglet
)?