Skip to content

CodaFi/functional-swift-style-guide

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 

Repository files navigation

#Functional Swift - A Style Guide

##Background

Functional Swift is a proper subset of Swift and its features that encourages immutability, recursion, and higher types instead of mutability, loops, and subtyping respectively. The end goal is to approach the cleanliness and readability of Declarative Programming proper while still maintaining the simplicity and semantics of Swift.

Note This document is not a sample of best practices, nor will adhering to it necessarily produce the most efficient or practical code. It merely serves as an exploration of pure declarative programming in an imperative language.

##Restrictions

For, do, and while loops are strictly forbidden. What you cannot express recursively, you should not express at all.

For Example

public func foldl<A, B>(f: (B, A) -> B) -> B -> [A] -> B {
	return { z in { l in
		switch destruct(l) {
			case .Empty:
				return z
			case .Destructure(let x, let xs):
				return foldl(f)(f(z, x))(xs)
		}
	} }
}

Assignment is restricted to let bindings and monadic extraction (prefix !, reappropriated from strictness bangs) in do_ blocks.

For Example

public func finally<A, B>(a : IO<A>)(then : IO<B>) -> IO<A> {
	return mask({ (let restore : IO<A> -> IO<A>) -> IO<A> in
		return do_ { () -> A in
			let r = !onException(restore(a))(what: then)
			let b = !then
			return r
		}
	})
}

All classes should derive a kind class (K0-K10), then be declared final so as to prevent further subtyping.

For Example

public final class Either<A, B> : K2<A, B> {

All higher-kinded classes that can, should provide a destructuring function named destruct() that returns an enum so as to facilitate switch-case usage.

For Example

public enum EitherD<A, B> {
	case Left(Box<A>)
	case Right(Box<B>)
}

//...

public func destruct() -> EitherD<A, B> {
	if lVal != nil {
		return .Left(Box(lVal!))
	}
	return .Right(Box(rVal!))
}

Leave the interface inside classes as minimal as possible. Prefer functions that take that class as their first parameter (CoreFoundation style) to encourage composition.

For Example

public class Chan<A> : K1<A> {
	init(read : MVar<MVar<ChItem<A>>>, write: MVar<MVar<ChItem<A>>>) {}
}

public func newChan<A>() -> IO<Chan<A>> {}
public func writeChan<A>(c : Chan<A>)(x : A) -> IO<()> {}
public func readChan<A>(c : Chan<A>) -> IO<A> {}
public func dupChan<A>(c : Chan<A>) -> IO<Chan<A>> {}

Never return the same object from a function without performing some kind of evaluation (notable exception: id).

##Whitespace

Swift code shall use tabs. The only exception is diagrams in comments, which must use spaces to guarantee correct formatting. Each opening brace introduces another tab-level of indentation. Each closing brace removes one tab-level of indentation. Braces appear on the same line as the statements they accompany.

For Example

/// Functors map the functions and objects in one set to a different set of functions and objects.
/// This is represented by any structure parametrized by some set A, and a function `fmap` that 
/// takes objects in A to objects in B and wraps the result back in a functor.  `fmap` respects the
/// following commutative diagram:
///
///               F(A)
///                 •
///                / \
///               /   \
///              /     \
///     fmap f  /       \  fmap (f • g)
///            /         \
///           •-----------•
///       F(B)              F(C)
///               fmap g
///
/// Formally, a Functor is a mapping between Categories, but we have to restrict ourselves to the
/// Category of Swift Types (S), so in practice a Functor is just an Endofunctor.  Functors are
/// often described as "Things that can be mapped over".
public protocol Functor {
	/// Type of Source Objects
	typealias A
	/// Type of Target Objects
	typealias B
	
	/// Type of our Functor
	typealias FA = K1<A>
	
	/// Type of a target Functor
	typealias FB = K1<B>
	
	/// Map "inside" a Functor.
	///
	/// F on our diagram.
	class func fmap(A -> B) -> FA -> FB
	func <%>(A -> B, FA) -> FB

	/// Constant Replace | Replaces all values in the target Functor with a singular constant value
	/// from the source Functor.
	///
	/// Default definition: 
	///		`curry(<%>) • const`
	func <%(A, FB) -> FA
}

Longer array and dictionary literals may appear on multiple lines. If statements should have a single space between the if and the case, and the case and the opening brace. Else statements appear on the same line as the closing brace of the if statement they accompany.

For Example

public func destruct<T>(l : [T]) -> ArrayD<T> {
	if l.count == 0 {
		return .Empty
	} else if l.count == 1 {
		return .Destructure(l[0], [])
	}
	let hd = l[0]
	let tl = Array<T>(l[1..<l.count])
	return .Destructure(hd, tl)
}

##Functions

Function names should describe exactly what a function does, yet still be concise. Abbreviations for common words (l for left, m for monad, etc.) are encouraged, as long as they are used consistently.

A function should always be written in curried form and without argument labels if possible. Higher order parameters of arity 3 mean there should be a separate overloading for that function to allows operators (which are uncurried by default) to be passed in. Strive for point-free code.

For Example

public func foldr<A, B>(k: A -> B -> B) -> B -> [A] -> B {
	return { z in { l in
		switch destruct(l) {
			case .Empty:
				return z
			case .Destructure(let x, let xs):
				return k(x)(foldr(k)(z)(xs))
		}
	} }
}

Operators should be accompanied by functions that are fully curried and perform the same operations. Operators are, in general, the infix form of functions. They are not to be defined on a whim. Operators should be easily reachable on a Mac's keyboard.

For Example

/// A type-restricted version of const.  In cases of typing ambiguity, using this function forces
/// its first argument to resolve to the type of the second argument.
public func asTypeOf<A>(x : A) -> A -> A {
	return const(x)
}

/// "As Type Of" | A type-restricted version of const.  In cases of typing ambiguity, using this 
/// function forces its first argument to resolve to the type of the second argument.  
///
/// Composed because it is the face one makes when having to tell the typechecker how to do its job.
public func >=<<A>(x : A, y : A) -> A {
	return asTypeOf(x)(y)
}

##Data Types

Data types should be classes that derive a kind class, then should be final. Recursive datatypes may require Box<T>s or @autoclosures. A datatype's members are always private, and let-bound whenever possible. Common operators that act on these private variables should remain in the same file as the data type.

For Example

public final class Version : K0 {
	public let versionBranch : [Int]
	public let versionTags : [String]

	public init(_ versionBranch : [Int], _ versionTags : [String]) {
		self.versionBranch = versionBranch
		self.versionTags = versionTags
	}
}

##Protocols

Protocols are weakened typeclasses. Provide all functions that return higher-kinded types with the appropriate typealiases to properly restrict implementations. Default implementations are not possible, but whenever it is appropriate, provide a comment showing the default implementation, or provide a method that returns the default definition for that function. Infix operators should always call their normal form counterparts, not the other way around.

For Example

public protocol Functor {
	/// Type of Source Objects
	typealias A
	/// Type of Target Objects
	typealias B

	/// Type of our Functor
	typealias FA = K1<A>

	/// Type of a target Functor
	typealias FB = K1<B>

	/// Map "inside" a Functor.
	///
	/// F on our diagram.
	class func fmap(A -> B) -> FA -> FB
	func <%>(A -> B, FA) -> FB

	/// Constant Replace | Replaces all values in the target Functor with a singular constant value
	/// from the source Functor.
	func <^(A, FB) -> FA
}

/// Eases writing a definition for Constant Replace.  Hand it an fmap, and x in B, and a source.
public func defaultReplace<A, B, FA : Functor, FB : Functor>(fmap : (A -> B) -> FA -> FB)(x : B)(f : FA) -> FB {
	return (fmap • const)(x)(f)
}

##Bodies

Function bodies begin one line after and one tab-level from their surrounding function or closure brace. Function bodies may only contain let bindings, returns, switches, or simple if-else statements. Functions must return a value. For functions that perform side effects, that value shall be wrapped in an IO monad. For functions that perform only side effects, a result of type IO<()> shall always be returned.

For Example

/// Exits with error code 0 (success).
public func exitSuccess<A>() -> IO<A> {
	return exitWith(.ExitSuccess)
}

##Other

Type annotations include a space between the parameter name and the colon, and the colon and the type. Function applications do not include a space between the parameter name and the colon, but do include it between the colon and the type.

For Example

public func until<A>(p : A -> Bool)(f : A -> A)(x : A) -> A {
	if p(x) {
		return x
	}
	return until(p)(f: f)(x: f(x))
}

Generic typealiases are forbidden by the language, so typealiases not required to implement protocols are forbidden. Functions should never expose typealias'd types, and should instead expose the fully expanded type to the user.

Non-total functions are allowed, but discouraged and should always be documented as such. Generally, prefer types to enforce invariants.

About

Style and Conventions for Functional Swift Projects

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published