A Swift library for uncertainty-aware programming, which is especially useful for making reliable decisions with noisy, error-prone, or incomplete data.
This library implements the approach described in:
James Bornholt, Todd Mytkowicz, and Kathryn S. McKinley
"Uncertain: A First-Order Type for Uncertain Data"
Architectural Support for Programming Languages and Operating Systems (ASPLOS), March 2014.
Programs often treat uncertain data as exact values, leading to unreliable decisions:
import CoreLocation
let speedLimit: CLLocationSpeed = 25 // 25 m/s ≈ 60 mph or 90 km/h
// ❌ WRONG: Treats GPS reading as exact truth
if location.speed > speedLimit {
issueSpeedingTicket() // Oops! False positive due to GPS uncertainty
}
Uncertain<T>
uses evidence-based conditionals that account for uncertainty:
import Uncertain
import CoreLocation
let speedLimit: CLLocationSpeed = 25 // 25 m/s ≈ 60 mph or 90 km/h
// ✅ CORRECT: Ask about evidence, not facts
let uncertainLocation = Uncertain<CLLocation>.from(location)
let uncertainSpeed = uncertainLocation.speed
if (uncertainSpeed > speedLimit).probability(exceeds: 0.95) {
issueCitation() // Only if 95% confident
}
Note
Check out this blog post for more information about probabilistic programming in Swift.
- Swift 6.0+
dependencies: [
.package(url: "https://github.com/mattt/Uncertain.git", from: "1.0.0")
]
import Uncertain
let temperatureSensor = Uncertain<Double>.normal(mean: 23.5, standardDeviation: 1.2)
let humiditySensor = Uncertain<Double>.normal(mean: 45.0, standardDeviation: 5.0)
// Combine sensor readings
let comfortIndex = temperatureSensor.map { temp in
humiditySensor.map { humidity in
// Heat index calculation
-42.379 + 2.04901523 * temp + 10.14333127 * humidity
}
}.flatMap { $0 }
let comfortable = (comfortIndex >= 68.0) && (comfortIndex <= 78.0)
if comfortable.probability(exceeds: 0.8) {
print("Environment is comfortable")
}
For platforms that support the Core Location framework, this library provides built-in convenience methods for working with GPS data:
import Uncertain
import CoreLocation
let userLocation = Uncertain<CLLocationCoordinate2D>.coordinate(
CLLocationCoordinate2D(latitude: 37.7749, longitude: -122.4194),
accuracy: 10.0 // ±10 meters
)
let destination = Uncertain<CLLocationCoordinate2D>.coordinate(
CLLocationCoordinate2D(latitude: 37.7849, longitude: -122.4094),
accuracy: 5.0 // ±5 meters
)
let distance = userLocation.distance(to: destination)
if (distance < 100.0).probability(exceeds: 0.9) {
print("User is likely at the destination")
} else {
let bearing = userLocation.bearing(to: destination)
if (bearing >= 45.0 && bearing <= 135.0).probability(exceeds: 0.8) {
print("User should head generally eastward")
}
}
// Create uncertain locations from CLLocation objects
let gpsReading = CLLocation(latitude: 37.7749, longitude: -122.4194)
let uncertainGPS = Uncertain<CLLocation>.from(gpsReading) // Uses built-in accuracy
// Or override accuracy if the GPS reading seems too optimistic
let conservativeGPS = Uncertain<CLLocation>.from(gpsReading, horizontalAccuracy: 20.0)
Use Sequential Probability Ratio Test (SPRT) for efficient evidence evaluation:
let sensor = Uncertain<Double>.normal(mean: 25.0, standardDeviation: 3.0)
let evidence = sensor > 30.0
// Basic probability test
let isLikely = evidence.probability(exceeds: 0.95)
// Advanced hypothesis testing with full control
let result = evidence.evaluateHypothesis(
threshold: 0.8,
confidenceLevel: 0.99,
maxSamples: 5000,
epsilon: 0.05,
alpha: 0.01,
beta: 0.01,
batchSize: 20
)
print("Decision: \(result.decision)")
print("Observed probability: \(result.probability)")
print("Samples used: \(result.samplesUsed)")
Tip
The alpha
and beta
parameters correspond to the
Type I error (false positive rate) and
Type II error (false negative rate), respectively.
Adjust these to control the strictness of your hypothesis test.
The SPRT approach automatically determines sample sizes based on statistical significance, making hypothesis testing both efficient and reliable.
Operations build computation graphs rather than computing immediate values, so sampling only occurs when you evaluate evidence.
let x = Uncertain<Double>.normal(mean: 10.0, standardDeviation: 2.0)
let y = Uncertain<Double>.normal(mean: 5.0, standardDeviation: 1.0)
// Expressions are lazily evaluated
let result = (x + y) * 2.0 - 3.0 // Uncertain<Double>
let evidence = result > 15.0 // Uncertain<Bool>
// Now we evaluate
let confident = evidence.probability(exceeds: 0.8) // Bool
All standard arithmetic operations are supported and build computation graphs:
let a = Uncertain<Double>.normal(mean: 10.0, standardDeviation: 2.0)
let b = Uncertain<Double>.normal(mean: 5.0, standardDeviation: 1.0)
// Arithmetic with other uncertain values
let sum = a + b
let difference = a - b
let product = a * b
let quotient = a / b
// Arithmetic with constants
let scaled = a * 2.0
let shifted = a + 5.0
let normalized = (a - 10.0) / 2.0
Comparisons return uncertain Uncertain<Bool>
, not Bool
:
let speed = Uncertain<Double>.normal(mean: 55.0, standardDeviation: 5.0)
// All comparisons return Uncertain<Bool>
let tooFast = speed > 60.0
let tooSlow = speed < 45.0
let exactMatch = speed == 55.0
let withinRange = (speed >= 50.0) && (speed <= 60.0)
Boolean operations work with Uncertain<Bool>
values:
let condition1 = speed > 50.0
let condition2 = speed < 70.0
let bothTrue = condition1 && condition2
let eitherTrue = condition1 || condition2
let notTrue = !condition1
Transform uncertain values with map
/ flatMap
/ filter
:
let temperature = Uncertain<Double>.normal(mean: 20.0, standardDeviation: 3.0)
// Map: transform each sample
let fahrenheit = temperature.map { $0 * 9/5 + 32 }
// FlatMap: chain uncertain computations
let adjusted = temperature.flatMap { temp in
Uncertain<Double>.normal(mean: temp, standardDeviation: 1.0)
}
// Filter: [rejection sampling][rejection-sampling]
let positive = temperature.filter { $0 > 0 }
This library comes with built-in constructors for various probability distributions.
Tip
Check out this companion project, which has interactive visualizations to help you build up an intuition about these probability distributions.
let normal = Uncertain<Double>.normal(mean: 0.0, standardDeviation: 1.0)
Best for modeling measurement errors, natural phenomena, and central limit theorem applications.
let uniform = Uncertain<Double>.uniform(min: 0.0, max: 1.0)
All values within the range are equally likely. Useful for random sampling and Monte Carlo simulations.
let exponential = Uncertain<Double>.exponential(rate: 1.0)
Models time between events in a Poisson process. Common for modeling waiting times and lifetimes.
let kumaraswamy = Uncertain<Double>.kumaraswamy(a: 2.0, b: 3.0)
A continuous distribution on [0,1] with flexible shapes. Similar to Beta distribution but with simpler mathematical forms.
let rayleigh = Uncertain<Double>.rayleigh(scale: 1.0)
Models the magnitude of a 2D vector whose components are normally distributed. Commonly used for modeling distances from a center point, such as GPS uncertainty.
let binomial = Uncertain<Int>.binomial(trials: 100, probability: 0.3)
Models the number of successes in a fixed number of independent trials with constant success probability.
let poisson = Uncertain<Int>.poisson(lambda: 3.5)
Models the number of events occurring in a fixed time interval when events occur independently at a constant rate.
let bernoulli = Uncertain<Bool>.bernoulli(probability: 0.7)
Models a single trial with two possible outcomes (success/failure).
let categorical = Uncertain<String>.categorical([
"red": 0.3,
"blue": 0.5,
"green": 0.2
])
Models discrete outcomes with specified probabilities.
let observedData = [1.2, 3.4, 2.1, 4.5, 1.8, 2.9]
let empirical = Uncertain<Double>.empirical(observedData)
Uses observed data to create a distribution by randomly sampling from the provided values.
let mixture = Uncertain<Double>.mixture(
of: [normal1, normal2, uniform],
weights: [0.5, 0.3, 0.2]
)
Combines multiple distributions with optional weights.
let certain = Uncertain<Double>.point(42.0)
Represents a known, certain value within the uncertain framework.
Or, you can always initialize with your own sampler:
let custom = Uncertain<Double> {
// Your sampling logic here
return myRandomValue()
}
The provided closure is called each time a sample is needed.
Uncertain<T>
provides convenience functions for statistical analysis:
Expected Value (Mean)
let normal = Uncertain<Double>.normal(mean: 50.0, standardDeviation: 10.0)
let mean = normal.expectedValue(sampleCount: 1000) // ≈ 50.0
let std = normal.standardDeviation(sampleCount: 1000) // ≈ 10.0
let (lower, upper) = normal.confidenceInterval(0.95, sampleCount: 1000)
// 95% of values fall between lower and upper bounds
let skew = normal.skewness(sampleCount: 1000) // ≈ 0 for normal distribution
let kurt = normal.kurtosis(sampleCount: 1000) // ≈ 0 for normal distribution
let probability = normal.cdf(at: 60.0, sampleCount: 1000)
// Probability that a sample is ≤ 60.0
Mode (Most Frequent Value)
let mode = categorical.mode(sampleCount: 1000)
// Most frequently occurring value
let frequencies = categorical.histogram(sampleCount: 1000)
// Dictionary mapping values to occurrence counts
let entropy = categorical.entropy(sampleCount: 1000)
// Information entropy in bits
let logLikelihood = normal.logLikelihood(45.0, sampleCount: 1000, bandwidth: 1.0)
// Estimated log-likelihood using kernel density estimation
This project is available under the MIT license. See the LICENSE file for more info.