Putting aside the type checker costs, and only thinking about the inherent value when trying to write maintainable software, I can say that I'm conflicted about this concept of implicit conversion. The ancient reptilian part of my brain suggests that this is not a good idea: the possibility of implicitly converting from one type to another, that is, in a place where I need type A
I can pass type B
, can be the source of all sorts of sneaky bugs.
But in my personal experience I can say that I've found several cases where this wouldn't have been a problem at all, and in fact it would have helped with maintainable code.
Here's a couple of examples.
Suppose that I defined a type to express the relationship between 2 layout constraints, some like:
struct ConstraintRelation {
case atLeast(Int)
case exactly(Int)
case atMost(Int)
}
(ignore for a second that this is better expressed by a struct
with 2 properties, one of which an enum
)
Where needed, I can write relation: .atLeast(42)
or relation: .exactly(43)
.
Now, to make things simpler and more direct, I can leverage the ExpressibleByIntegerLiteral
protocol, so instead of writing .exactly(43)
I can just write 43
: this is valuable, because just 43
clearly expresses that it's "exactly" 43, and the .exactly(...)
part is just noise. Thus:
extension ConstraintRelation: ExpressibleByIntegerLiteral {
init(integerLiteral value: Int) {
self = .exactly(value)
}
}
This is nice, I can just write relation: 43
. But in some cases, instead of having an hardcoded literal, I have a constant defined somewhere, let myRelationValue: 43
... well, I cannot use the simplified version now, like relation: myRelationValue
, and I'm forced to "wrap" it again with relation: .exactly(myRelationValue)
.
This would be solved by an ExpressibleByWrappedValue
that yields implicit conversion, for example:
protocol ExpressibleByWrappedValue {
associatedtype Wrapped
init(value: Wrapped)
}
Note that the protocol wouldn't probably be enough in this case, because a better definition for
ConstraintRelation
would beDouble
value instead ofInt
, and a conformance to bothExpressibleByIntegerLiteral
andExpressibleByFloatLiteral
, so the wrapped value would have to be "both"Int
andDouble
.
Also note that this would be solved with type unions for generic constraints.
At a domain boundary we often convert things into other things, because the same underlying concept is modeled differently in different domains.
For example suppose that, in a certain domain Foo
, a user action is modeled via the following enum:
// domain Foo
enum UserAction {
case didSucceed
case didFail
case didCancel
}
These are the cases that the domain is interested in, and the model focuses on those.
In another domain Bar
, we care about different things for user action:
enum UserAction {
case didEnter
case didComplete
case didCancel
}
When the even crosses domains, we would need to do the conversion manually:
// `userAction:` requires `Bar.UserAction`
userAction: {
switch someFooUserAction {
case .didSucceed, .didFail:
return .didComplete
case .didCancel
return .didCancel
}
}()
We could of course extend Bar.UserAction
with a initializer that takes a Foo.UserAction
(and this is what we actually do), but in every place where the conversion takes place, we would need to call it:
// `userAction:` requires `Bar.UserAction`
userAction: .init(someFooUserAction)
If ExpressibleByWrappedValue
was possible, we could directly use
// `userAction:` requires `Bar.UserAction`
userAction: someFooUserAction
If this pattern repeats often for the same conversion, the savings become substantial. Also, note the .init(...)
part doesn't really matter, it's just "plumbing" to support the conversion.