Bi-directional Type Inference

TL;DR: It`s bi-directional: provide the necessary type information for generics wherever you want to, as long as it is not ambiguous.

One of Swift`s best features is well hidden.

Actually, the less code you write, the more it takes control.
What? Yes!
Let`s talk about type inference.
Let`s talk about bi-directional type inference.

Strongly typed meant to be a lot of typing ⌨️

When you take a look at the Tiobe Index the PL ranking 1st is Java (April 2016). And I guess quite a lot of people are already familiar with the Java way of generics and types.

First of all, pretty much everything you get from Java generics should work in Swift too. But Swift generics are way more powerful and allow concepts that are impossible to do in a Java type system.

Example? Here comes Java:

// JAVA
// you have to type a lot ...
// we define a variable `numbers` which is of type
// List of Integer and instantiate it with a new object 
// of type ArrayList of Integer.
List<Integer> numbers = new ArrayList<Integer>();
// have a look at numbers.getClass();
// Not possible - type inference:
// var numbers = new ArrayList<Integer>();
// #semicolonsFTW

To be honest - it`s not fun to write the Java examples as there is too much stuff to type. And the worst part - after providing all the information - at the end the generic types will be erased. E.g. if you look at numbers.getClass();it is of type class java.util.ArrayList. You can run the code online in the Groovy console.

For the next example I switch to Groovy which is like Java, but friendlier and more concise.

// GROOVY
// you can run this snippet also in the Groovy console
// Groovy literals for creating Lists!
List<Integer> ints = [1,2,3,4]
List<Double> doubles = [1,2,3,4]
// Interesting... whats the result?
// *.class will call getClass() on every list item
println("ints?")
println(ints*.class) 
// [class java.lang.Integer, class java.lang.Integer, class java.lang.Integer, class java.lang.Integer]
println("doubles?")
println(doubles*.class)
// [class java.lang.Integer, class java.lang.Integer, class java.lang.Integer, class java.lang.Integer]

Damn. I mean… What?

Exactly - this is the way type inference works in many mainstream languages.

  • The type information flows only from the leaf of the expression to the root. In that case [1,2,3,4] provides the type information (List of Integers) to List<Integer> ints.
  • Flowing in the opposite direction is not possible. The root List<Double> doubles cannot be inferred to the leaf [1,2,3,4].

Next Stop: Leaf of expression 🍀

Back to Swift. To demonstrate the so-called bi-directional type inference we begin with an easy example.

  • defining a list of Int (or is it a list of Double?)
// infer the type based on literal [1, 2, 3, 4] to [Int]
// Type information:
// ROOT <- LEAF 
let ints = [1, 2, 3, 4]
// infer the type based on [Double] to literal
// Type information:
// ROOT -> LEAF 
let doubles: [Double] = [1, 2, 3, 4]
// check the type information:
print("ints:", ints.dynamicType)
// - Array<Int>
print("doubles:", doubles.dynamicType)
// - Array<Double>

Awesome! The Type Information flows to the leaf of your expression.

Beware: Type information can not be inferred across multiple expressions!

We can even try to build a more complex expression:

let tupples1 = [(1, 0)]
print(tupples1.dynamicType)
// -> Array<(Int, Int)>
let tupples2: [(Double?, CGFloat?)?]? = [(1, 0)]
print(tupples2.dynamicType)
// -> Optional<Array<Optional<(Optional<Double>, Optional<CGFloat>)>>>

Why is it working?

  • The compiler infers 1 as Optional<Int> or Int depending on context.
  • Also 1 and 0 are FloatLiteralConvertibles and can initialize a Float

So far we only applied inference to built-in generic types like Array, Optional and the FloatLiteralConvertible protocol. But knowing that type information flows in an expression let us build flexible constructs ourselves.

BEWARE: There is a thin line between solving generics and warnings like: Expression was too complex ....

Write once, infer anywhere 🍵

In the next step we write a generic function and make use of type inference to solve the generics.

Goal: We write a generic function, which takes a value and returns a function to check if an optional value of the same type is less than the provided value.

Go:

// in order to call < we need to ensure that T is Comparable
func lessThan<T: Comparable>(tx: T) -> T? -> Bool {
  return { t in t.map { $0 < tx } ?? false }
}

Let`s invoke it:

Version 1:

// based on `10` the types of `lessThan` will be inferred 
let lessThan10 = lessThan(10)
print(lessThan10.dynamicType)
// -> Optional<Int> -> Bool

Version 2:

// based on the left hand side `CGFloat? -> Bool` the types will be inferred
let lessThan10X: CGFloat? -> Bool = lessThan(10)
print(lessThan10X.dynamicType)
// -> Optional<CGFloat> -> Bool

Version 3:

typealias DoublePredicate = (Double?) -> Bool
// based on the left hand side typealias the types will be inferred
let lessThan10XX: DoublePredicate = lessThan(10)
// -> Optional<Double> -> Bool

All you need to take care of is that the provided type identifiers work nicely together. We deconstruct Version 3:

  • 10 can represent Int, Float, Double, CGFloat
  • DoublePredicate represents a function of type (Double?) -> Bool
  • lessThan<T: Comparable>(tx: T) -> T? -> Bool wants that tx and thave the same Type (and tis optional)

When we apply these constraints to the func we get something like:

PSEUDOCODE

///
/// func lessThan<T: Comparable>(tx: T) -> T? -> Bool
///
/// let lessThan10XX: DoublePredicate = lessThan(10)
///
/// 1. DoublePredicate return type: 
/// -> `T? -> Bool` must be `Double? -> Bool`
/// 2. lessThan(10)
/// -> `T` can be  `Int`, `Float`, `Double`, `CGFloat` or even optional of it
/// 3. func lessThan<T: Comparable>(tx: T) -> T? -> Bool 
/// -> `T?` is  `Double?`
/// 
/// next:  
/// `T`is `Double`
///  next:
///  -> `10` -> Double
let lessThan10XX: Double? -> Bool = func lessThan(tx: Double) -> Double? -> Bool {
  return { t in t.map { $0 < tx } ?? false }
}

And now? 💭

With these concepts in mind, what can we build up … ?

As an example I want to take a look at Palau, a framework we published on github. It provides easy typesafe NSUserDefaults access. The whole concept of Palau works only because of bi-directional type inference.

Be sure to check out the README and the Tests

The framework is based on generics and protocols with associated types. Only by defining the return type of a computed property we infer the types for the actual implementation throughout the framework.

Pseudocode not working in playground

/// Defaults are defined like:
extension PalauDefaults {
  public static var name: PalauDefaultsEntry<String> {
    get { return value("name") }
    set { }
  }
}
/// getting a value from NSUserDefaults looks like
let value = PalauDefaults.name.value
///
/// Let solve the generics:
///
/// 1. PalauDefaultsEntry is generic
public struct PalauDefaultsEntry<T: PalauDefaultable> {}
///
/// 2. String conforms to PalauDefaultable
extension String: PalauDefaultable {
  public typealias ValueType = String
}
/// 3. PalauDefaultsEntry has a computed property value 
 public var value: T.ValueType? {
    get { return ensure(T.get(key, from: defaults)) }
}

/// 4. String gets the PalauDefaultable methods from an extension of the protocol
PalauDefaults.name.value
/// turns into
String.get("name", from: defaults)
/// Looks reasonable

Generics IN your NSUserDefaults 🍢

As we kept Palau generic to the bone, you can even go one step further and create NSUserDefaults of type FloatLiteralConvertible. But we have no generic constants yet - so we need a function.

import Palau
import CoreGraphics
///
///  Swift 2.2 does not yet allow generic constants
///  so we take a function instead.
/// We define a new `NSUserDefaults` value with the key `'floatConvertible'` and
/// can get its `PalauDefaultsEntry` with the `func floatConvertible`.
extension PalauDefaults {
  public static func floatConvertible<T: FloatLiteralConvertible>() -> PalauDefaultsEntry<T> {
    return value("floatConvertible")
  }
}
/// Conform CGFloat to PalauDefaultable for storing
/// Float and Double are supported out of the box
extension CGFloat: PalauDefaultable {
  public typealias ValueType = CGFloat
}

And now the generic usage:

/// will infer PalauDefaultsEntry<Double>
var doubleEntry = PalauDefaults.floatConvertible()
let double = doubleEntry.value ?? 0
let doubleX = double + 1
// -> Double
doubleEntry.value = doubleX
/// 
/// will infer PalauDefaultsEntry<Float>
var floatEntry: PalauDefaultsEntry<Float> = PalauDefaults.floatConvertible()
let float = floatEntry.value ?? 0
let floatX = float + 1
// -> Float
floatEntry.value = floatX
///
/// will infer PalauDefaultsEntry<CGFloat>
var cgfEntry: PalauDefaultsEntry<CGFloat> = PalauDefaults.floatConvertible()
let cgf = cgfEntry.value ?? 0
let cgfX = cgf + 1.4
cgfEntry.value = cgfX
// -> CGFloat

The same NSUserDefaults value can be treated as Float, Double or CGFloat. This is kinda cool.

Types FTW 🏁

Understanding type inference is an important part in order to build reusable code parts that increase productivity and type safety. So type inference and the idea that a type can provide all information in order to be stored in the defaults was the idea behind Palau.

My suggestion:

  • In the first time, try to get a feeling when it`s appropriate to provide no type identifier at all.
  • When you start with generics, try type inference from the root.
  • Then start to mix all kind of type inference and start to smile when reading “Expression was to complex…”.
  • If you want to: work through the Palau Sourcode and see it in action.

The gist of it 👻

👻 Feel free to hit me on twitter. @elmkretzer