Last active
October 23, 2023 20:22
-
-
Save wircho/bfdfaf3226ebc96cf6c6c59b69ca5101 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// 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