Hi!
I'm working on a DSL for https://bitcoin.sipa.be/miniscript/ and ran into an issue trying to make expressions conform to different result types depending on their inputs.
To abstract the problem I've come up with a basic playground example.
I have an Or
construct that takes 2 boolean expressions and returns another boolean expression. I want to identify when the input boolean expressions are different and mark that as an xor result.
There's another expression I called NeedsXOrResult
which requires xor results, even If they come from a regular or.
The problem is I can't make Or
conditionally conform to ExpXOr
for both when A: ExpTrue
and B: ExpFalse
and viceversa at the same time. I need to choose one, but then I'd be leaving valid results outside of the scope for NeedsXOrResult
which would absolutely break the rules of the DSL.
My solution was to create a second ExpXOr_
protocol and then do a bunch of gymnastics to have NeedsXOrResult
take either of the 2 flavors of xor results.
This is the best I could come up with. I wonder if there's a better solution involving associated types or result builders or something.
Anyway thanks for taking a look. Here's the playground source code:
protocol Exp: CustomStringConvertible {}
protocol ExpBool: Exp {}
protocol ExpTrue: ExpBool {}
protocol ExpFalse: ExpBool {}
struct True: ExpTrue {
let description = "true"
}
struct False: ExpFalse {
let description = "false"
}
struct Or<A: ExpBool, B: ExpBool>: Exp {
let a: A
let b: B
init(_ a: A, _ b: B) {
self.a = a
self.b = b
}
var description: String { "\(a) or \(b)" }
}
// Nastiness begins…
protocol ExpXOr: Exp {}
protocol ExpXOr_: Exp {}
extension Or: ExpXOr where A: ExpTrue, B: ExpFalse { }
extension Or: ExpXOr_ where A: ExpFalse, B: ExpTrue { }
extension Never: @retroactive CustomStringConvertible {}
extension Never: ExpXOr, ExpXOr_ { public var description: String { fatalError() } }
struct NeedsXOrResult<E: ExpXOr, E_: ExpXOr_>: Exp {
// What we would really like is `struct NeedsXOrResult<E: ExpXOr>: Exp { … }`. Just one dependant type.
init(_ a: E, _ dummy: E_? = Never?.none) {
precondition(dummy == nil)
self.a = a
a_ = .none
}
init(_ a_: E_, _ dummy: E? = Never?.none) {
precondition(dummy == nil)
a = .none
self.a_ = a_
}
let a: E?
let a_: E_? // The hack cascades all the way to the implementation of the expression.
var description: String {
if let a {
return "\(a)"
}
guard let a_ else { preconditionFailure() }
return "\(a_)"
}
}
print(NeedsXOrResult(Or(True(), False()))) // true or false
print(NeedsXOrResult(Or(False(), True()))) // false or true
// NeedsXOrResult(Or(True(), True())) // 👍 "No exact matches in call to initializer" 👍