Skip to content

Instantly share code, notes, and snippets.

@wircho
Last active October 23, 2023 20:22
Show Gist options
  • Save wircho/bfdfaf3226ebc96cf6c6c59b69ca5101 to your computer and use it in GitHub Desktop.
Save wircho/bfdfaf3226ebc96cf6c6c59b69ca5101 to your computer and use it in GitHub Desktop.
// HERE'S THE PROBLEM I'VE BEEN TRYING TO SOLVE:
// Is it possible (in some type-safe language) to enforce the correctness of a SQL-query-like code joining a transactions table with a conversionRates table (that is, to only allow the amount * rate multiplication if the tables were joined the right way; on both the date and currency columns)?
// SOLUTION ATTEMPT IN SWIFT:
// A "value" represents either a property (cell) of a record, or a record (row) itself
protocol Value {}
// A value can be "refined" (parametrized). This is usually as a result of joining tables on some conditions
protocol RefinerProtocol {}
@dynamicMemberLookup
struct Refined<T: Value, R: RefinerProtocol>: Value {
subscript<U>(dynamicMember keyPath:KeyPath<T, U>) -> Refined<U, R> {
.init()
}
}
// A "condition" is an expression on which two tables are joined
protocol Condition {}
// Some specific refiners
struct MergeRefiner: RefinerProtocol {}
struct ConditionRefiner<C: Condition>: RefinerProtocol {}
extension Value {
typealias MergeRefined = Refined<Self, MergeRefiner>
typealias ConditionRefined<C: Condition> = Refined<Self, ConditionRefiner<C>>
}
// Merged (joined) and conditioned values
struct LeftRight<L: Value, R: Value>: Value {
let left: L
let right: R
}
typealias Merged<L: Value, R: Value> = Refined<LeftRight<L, R>, MergeRefiner>
typealias Conditioned<V: Value, C: Condition> = Refined<V, ConditionRefiner<C>>
// A "table" represents (doesn't really hold in memory though) a collection of records
struct Table<R: Value> {
func join<OtherR: Value, C: Condition>(_ otherTable: Table<OtherR>, on: (R.MergeRefined, OtherR.MergeRefined) -> C) -> Table<Conditioned<Merged<R, OtherR>, C>> {
.init()
}
func crossJoin<OtherR: Value>(_ otherTable: Table<OtherR>) -> Table<Merged<R, OtherR>> {
.init()
}
func select<OtherV: Value>(_ transform: (R) -> OtherV) -> Table<OtherV> {
.init()
}
}
// SPECIFIC USE CASE: TRANSACTIONS & CONVERSION RATES
struct ToUSDConversionRateRecord: Value {
struct FromCurrency: Value {}
struct Date: Value {}
struct Rate: Value {}
let fromCurrency: FromCurrency
let date: Date
let rate: Rate
}
struct TransactionRecord: Value {
struct Currency: Value {}
struct Date: Value {}
struct Amount: Value {}
let currency: Currency
let date: Date
let amount: Amount
}
// Conditions under which it is possible to multiply a transaction amount and a conversion rate
struct CurrencyCondition: Condition {}
struct DateCondition: Condition {}
struct CurrencyAndDateCondition: Condition {}
func ==(lhs: ToUSDConversionRateRecord.FromCurrency.MergeRefined, rhs: TransactionRecord.Currency.MergeRefined) -> CurrencyCondition { .init() }
func ==(lhs: TransactionRecord.Currency.MergeRefined, rhs: ToUSDConversionRateRecord.FromCurrency.MergeRefined) -> CurrencyCondition { .init() }
func ==(lhs: ToUSDConversionRateRecord.Date.MergeRefined, rhs: TransactionRecord.Date.MergeRefined) -> DateCondition { .init() }
func ==(lhs: TransactionRecord.Date.MergeRefined, rhs: ToUSDConversionRateRecord.Date.MergeRefined) -> DateCondition { .init() }
func &&(lhs: CurrencyCondition, rhs: DateCondition) -> CurrencyAndDateCondition { .init() }
func &&(lhs: DateCondition, rhs: CurrencyCondition) -> CurrencyAndDateCondition { .init() }
// Actual multiplication of a transaction amount and a completion rate
struct USDAmount: Value {}
func *(lhs: ToUSDConversionRateRecord.Rate.MergeRefined.ConditionRefined<CurrencyAndDateCondition>, rhs: TransactionRecord.Amount.MergeRefined.ConditionRefined<CurrencyAndDateCondition>) -> USDAmount {
.init()
}
func *(lhs: TransactionRecord.Amount.MergeRefined.ConditionRefined<CurrencyAndDateCondition>, rhs: ToUSDConversionRateRecord.Rate.MergeRefined.ConditionRefined<CurrencyAndDateCondition>) -> USDAmount {
.init()
}
// Join statement that works:
let rates = Table<ToUSDConversionRateRecord>()
let transactions = Table<TransactionRecord>()
let joined = transactions.join(rates) {
(transaction, conversion) in
transaction.currency == conversion.fromCurrency
&& transaction.date == conversion.date
}.select {
record in
record.left.amount * record.right.rate
}
// Join statement that doesn't work:
//let badJoined = transactions.crossJoin(rates).select {
// record in
// record.left.amount * record.right.rate
//}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment