How to solve generic type issue once we introduce one possible conformation to it in Swift? - swift

I am working on this custom function:
func customPrint<T: CustomStringConvertible>(_ value: T...) {
var string: String = ""
value.forEach { item in
string += " " + String(describing: item)
}
print(string)
}
My issue is when I use one type and then use other type! for example this would work fine:
customPrint("hello", "100")
But this would not:
customPrint("hello", 100)
Xcode issue:
Cannot convert value of type 'Int' to expected argument type 'String'
The error is understandable for me, but my goal is to be able use String or Int beside together for feeding my function.
So how can i let my generic works for all types that conform to generic? obviously String and Int conforming to CustomStringConvertible, but they cannot get used both together for my function, looking to solve this issue.
Here what i am trying to solve:
I am thinking to use type Any like Any... where this Any type can conform to CustomStringConvertible.

Since generics constraint the type of the arguments to be uniform, I would suggest doing something like below:
func customPrint(_ value: CustomStringConvertible...) {
var string: String = ""
value.forEach { item in
string += " " + String(describing: item)
}
print(string)
}
Edit:
Thanks to vacawama, Any would also work with String(describing:):
func customPrint(_ value: Any...) {
print(value.map({String(describing: $0)}).joined(separator: " "))
}

When you are defining generic type T, you're actually giving the specific type for parameters that you will pass. If you give String parameter first, the function expects all the parameter types as String. If you give Int parameter first, function expects all parameters as Int. You should not use generic in this function. NoeOnJupiter's answer can be usable in your case.

Related

How do you provide a default argument for a generic function with type constraints?

The following function definition is legal Swift:
func doSomething<T: StringProtocol>(value: T = "abc") {
// ...
}
The compiler is able to determine that the default argument "abc" is a String, and String conforms to StringProtocol.
This code however does not compile:
func doSomething<T: Collection>(value: T = "abc") where T.Element == Character {
// ...
}
The compiler error:
Default argument value of type 'String' cannot be converted to type 'T'
It seems as though the compiler would have just as much information as in the first case to determine that String is indeed convertible to T. Furthermore, if I remove the default argument and call the function with the same value, it works:
doSomething(value: "abc")
Can this function be written differently so that I can provide the default String argument? Is this a limitation of Swift, or simply a limitation of my mental model?
The significant constraint is T: ExpressibleByStringLiteral. That's what allows something to be initialized from a string literal.
func doSomething<T: Collection>(value: T = "abc")
where T.Element == Character, T: ExpressibleByStringLiteral {
// ...
}
As Leo Dabus notes, T.Element == Character is technically not necessary, but removing it changes the meaning. Just because something is a collection and can be initialized by a string literal does not mean that its elements are characters.
It's also worth noting that while all of this is possible, it generally is poor Swift IMO. Swift does not have any way to express what the default type is, so doSomething() in all of these cases causes "Generic parameter 'T' could not be inferred".
The correct solution IMO is an overload, which avoids all of these problems:
func doSomething<T: StringProtocol>(value: T) {
}
func doSomething() {
doSomething(value: "abc")
}
This allows you to make the default parameter not just "something that can be initialized with the literal "abc"", but what you really mean: the default value is the String "abc".
As a rule, default parameters are just conveniences for overloads, so you can generally replace any default parameter with an explicit overload that lacks that parameter.

Can you Strict Generic types or give one parameter more than one type ?

For example i want to specify a type that might be Integer or String and use it as special type in func i tried typealias
but it wont solve the case because typealiases can't have or arguments as its only uses & therefore consider the case below.
typealias alis = StringProtocol & Numeric
func foo <T: alis> (vee: T) -> T{
// do something
return vee
}
i want this func to accept a parameter type of either ( Int or String ) not anything else (<T>),
as you can see i tried with typealias and i have no compile error.
however trying to use the function will lead to these errors.
foo(vee: 1) //Argument type 'Int' does not conform to expected type 'StringProtocol'
And
foo(vee: "v") //Argument type 'String' does not conform to expected type 'Numeric'
is this achievable with swift ? if so how.
Let's suppose that you could use a OR operator to combine protocols, what would you be able to do with something of type (Int | String)?
Not everything you can do to an Int can be done on (Int | String), because it might be a string underlyingly. Similarly, not everything you can do to an String can be done on (Int | String), because it might be a Int underlyingly.
Now you might say "Ah. I know that Int and String both have a description property. I should be able to access description on a variable of type (Int | String)."
Well, in that case, you can just create such a protocol yourself and only have Int and String conform to it:
protocol IntOrString {
var description: String { get }
}
extension Int : IntOrString {}
extension String : IntOrString {}
(Note that description is already defined in CustomStringConvertible. For the sake of argument, imagine that does not exist.)

Why can’t Swift’s string constructor be used to convert a generic to a string?

Consider the following example.
func printGeneric<T>(_ input: T) {
let output = String(input)
print(output)
}
This will result in the error cannot invoke initializer for type 'String' with an argument list of type '(T)'. (tested in playground)
However, for some reason, this does work.
func printGeneric<T>(_ input: T) {
let output = "\(input)"
print(output)
}
Why does the first method not work, but the second does? What is the 'proper' way to get the string representation of a generic?
How about
func printGeneric<T>(_ input: T) {
let output = String(describing:input)
print(output)
}
As for why:
The first way, String(x), is for specific situations where x is of a type that can be coerced to String, for use within your program. In other words, it is a type where each value has a string equivalent, like 1 and "1". (See on LosslessStringConvertible.) We don't know whether T is that sort of type, so the compilation fails.
But String(describing:x) is exactly the same as "\(x)" — it is intended for printing anything to the console. It is not a coercion but a string representation.
You could also choose to use String(reflecting:); it is subtly different from String(describing:).

Array to String in Swift, stringInterpolationSegment

I am trying to convert an array of enums to a string in Swift. My enum is Printable and has a description property.
I thought this would work:
", ".join(a.map { String($0) })
but the compiler complains
Missing argument label 'stringInterpolationSegment:' in call
So, I follow the suggestion,
", ".join(a.map { String(stringInterpolationSegment: $0) })
But I do not understand:
Why is the argument label needed?
What is the type of stringInterpolationSegment?
You can't call a String initializer with your enum type because there isn't an initializer that takes that type.
There are a number of initializers for String that have the stringInterpolationSegment argument and they each implement it for a different type. The types include Bool, Float, Int, and Character among others. When all else fails, there is a generic fallback:
/// Create an instance containing `expr`\ 's `print` representation
init<T>(stringInterpolationSegment expr: T)
This is the version that is being called for your enum since it isn't one of the supported types.
Note, you can also do the following which is more succinct:
", ".join(a.map { toString($0) })
and you can skip the closure expression (thanks for pointing that out #Airspeed Velocity):
", ".join(a.map(toString))
As #vacawama points out, the error message is a bit of a red herring, and you can use map and toString to convert it.
But what’s nice is, if you’ve already implemented Printable, then the array’s implementation of Printable will also use it, so you can just do toString(a) to get a similar output.

Defining explicit conversion for custom types in Swift

What is currently the best/preferred way to define explicit conversions in Swift? Of the top of my head I can think of two:
Creating custom initializers for the destination type via an extension, like this:
extension String {
init(_ myType: MyType) {
self = "Some Value"
}
}
This way, you could just use String(m) where m is of type MyType to convert m to a string.
Defining toType-Methods in the source type, like this:
class MyType {
func toString() -> String {
return "Some Value"
}
}
This is comparable to Swift's String.toInt(), which returns an Int?. But if this was the definitive way to go I would expect there to be protocols for the basic types for this, like an inversion of the already existing *LiteralConvertible protocols.
Sub-question: None of the two methods allow something like this to compile: let s: MyString = myTypeInstance (as String) (part in parentheses optional), but if I understand right, the as operator is only for downcasting within type hierarchies, is that correct?
The pattern used in swift is the initializer. So for instance when converting an Int to UInt, we have to write:
var i: Int = 10
var u: UInt = UInt(i)
I would stick with that pattern.
As for the subquestion, the documentation states that:
Type casting is a way to check the type of an instance, and/or to treat that instance as if it is a different superclass or subclass from somewhere else in its own class hierarchy.
and
You can also use type casting to check whether a type conforms to a protocol
so no, the as keyword can`t be used to transform a value of a certain type to another type.
That can be tested in a simple way:
var i: Int = 10
var u: UInt = i as UInt
That generates an error:
'Int' is not convertible to 'UInt'
More about Type Casting