Reputation: 825
I have 4 methods with one logic and 4 possible type mapping:
def convertStringToString(in: String): String = ???
def convertIntToString(in: Int): String = ???
def convertIntToInt(in: Int): Int = ???
def convertStringToInt(in: String): Int = ???
I want to generalize input and output type and write logic in one methods. Tried to generelize input parameter:
def convertToInt[IN](in: IN): Int = in match {
case x: String if x.forall(_.isDigit) => x.toInt
case y: Int => y
case _ => 0
}
def convertToString[IN](in: IN): String = convertToInt[IN](in).toString
Could you help me to generalize second:
def convertToInt[IN, OUT](in: IN): OUT = ???
Upvotes: 0
Views: 55
Reputation: 20561
If you really wanted to, you could have something typeclass-based:
def convert[I, O](in: I)(implicit c: ConversionRule[I, O]): O = {
if (c.isConvertible(in)) c.convert(in)
else c.zero
}
trait ConversionRule[I, O] {
def isConvertible(in: I): Boolean
def convert(in: I): O
def zero: O // Could possibly derive the zero from, e.g., a cats Monoid instance where such exists
}
The eagle-eyed may notice that the isConvertible
/convert
methods match the contract of PartialFunction[I, O]
's isDefinedAt
/apply
, so may as well just use PartialFunction
(and rewrite convert
with isDefinedAt
/apply
)
trait ConversionRule[I, O] extends PartialFunction[I, O] {
def zero: O
}
zero
can be implemented in terms of PartialFunction.applyOrElse
, but for the case where zero
is constant (which is the case where referential transparency is preserved), this is much faster.
Smart constructors can be defined:
object ConversionRule {
def apply[I, O](zeroValue: O)(pf: PartialFunction[I, O]): ConversionRule[I, O] =
new ConversionRule[I, O] {
override def apply(i: I): O = pf(i)
override def isDefinedAt(i: I): Boolean = pf.isDefinedAt(i)
val zero: O = zeroValue
}
def totalConversion[I, O](f: I => O): ConversionRule[I, O] =
new ConversionRule[I, O] {
override def apply(i: I) = f(i)
override def isDefinedAt(i: I) = true
override def zero: O = throw new AssertionError("Should not call since conversion is defined")
}
// Might want to put this in a `LowPriorityImplicits` trait which this object extends
implicit def identityConversion[I]: ConversionRule[I, I] =
totalConversion(identity)
}
identityConversion
means that a convertIntToInt
gets automatically generated.
convertStringToInt
can then be defined as
implicit val stringToIntConversion = ConversionRule[String, Int](0) {
case x if x.forAll(_.isDigit) => x.toInt
}
One can define a toString
based conversion (basically the non-lawful Show
proposed for alleycats):
implicit def genericToString[I]: ConversionRule[I, String] =
ConversionRule.totalConversionRule(_.toString)
And it should then be possible to define a stringViaInt
ConversionRule
derivation like:
implicit def stringViaInt[I, O](implicit toInt: ConversionRule[I, Int]): ConversionRule[I, String] =
convert(convert(in)(toInt))
The only really useful thing this provides is an opt-in to usage of implicit conversions. Whether that's enough of a gain to justify? shrug
(Disclaimer: only the scala compiler in my head has attempted to compile this)
Upvotes: 1