Youssef Shoaib [MOD]
03/13/2021, 3:51 PM// K is used to ensure that FunctorIn and FunctorOut conform to the same higher-kinded type. It's in because imagine
// that you internally produce a Some for example that you want to coerce to an Option, this allows you to do so safely
interface FunctorIn<in KA, out A, in K> {
fun <B, KB> KA.fmap(fo: FunctorOut<B, KB, @UnsafeVariance K>, mapper: (A) -> B): KB
}
interface FunctorOut<in B, out KB, in K> {
fun K.toK(): KB // this is just used for type safety and to push unsafe conversions to the XFunctorOutImpl
}
interface OptionFunctorIn<out A> : FunctorIn<Option<@UnsafeVariance A>, A, Option<Any?>>
// For a class with an out type param, a K value using Any? should be used just like below.
// For a class with an in type param, a K value using Nothing should be used.
// For an invariant class, use Any? or Nothing or Unit even and unsafely cast that to KB
interface OptionFunctorOut<in B> : FunctorOut<B, Option<@UnsafeVariance B>, Option<Any?>>
// Can also use an inline class here possibly but whatever
sealed class Option<out A> {
inline fun <B> map(mapper: (A) -> B): Option<B> = when (this) {
is Some -> Some(mapper(a))
is None -> None
}
}
data class Some<A>(val a: A) : Option<A>()
object None : Option<Nothing>()
object OptionFunctorInImpl : OptionFunctorIn<Any?> {
// Functor out should most likely be OptionFunctorOutImpl, but we can't just assume that it'll be that and we can't
// require this subclass to only accept OptionFunctorOut interface instances because that breaks the contract of
// FunctorIn, and so we instead use K to indicate that this FunctorOut knows how to transform our K to a KB safely.
override fun <B, KB> Option<Any?>.fmap(fo: FunctorOut<B, KB, Option<Any?>>, mapper: (Any?) -> B) =
with(fo) { map(mapper).toK() }
}
object OptionFunctorOutImpl : OptionFunctorOut<Any?> {
override fun Option<Any?>.toK(): Option<Any?> {
return this
}
}
inline fun <A> optionFunctorIn() = OptionFunctorInImpl as OptionFunctorIn<A>
inline fun <B> optionFunctorOut() = OptionFunctorOutImpl as OptionFunctorOut<B>
fun main() {
// Note that some of these options' types are Some, and yet they still work with the normal OptionalFunctorX implementation
val optionLife = Some(42)
val optionExistence: Option<Any> = None
val optionName = Some("name")
val optionUnit = Some(Unit)
println("hello world")
// Using with to emulate the @with(value) decorator
with(optionFunctorIn<Any>()) { // This has to be first because subtypes need to come at the very end
with(optionFunctorIn<Unit>()) {
with(optionFunctorIn<String>()) {
with(optionFunctorIn<Int>()) {
with(optionFunctorOut<String>()) OFS@{
with(optionFunctorOut<Int>()) {
// All of these require 2 receivers and so they can't be 100% prototyped right now but they should work as expected
// Use Ctrl+Shift+P in IDEA to check the types of both the its and the return types of fmap and performMap
println(optionLife.fmap(this@OFS) { it.toString() + " is a Some" })
println(optionExistence.fmap(this@OFS) { it.toString() + " is a Some" })
println(optionName.fmap(this@OFS) { it.toString() + " is a Some" })
println(optionUnit.fmap(this@OFS) { it.toString() + " is a Some" })
println(performMap(this@OFS, optionLife))
println(performMap(this, optionLife))
// Note that this performMap just needs a higher kinded container that has a CharSequence and not
// specifically a String, but because the types are declared with the proper variance it's automatically
// inferred by the compiler that this String value will satisfy the CharSequence constraint
println(performMap(this@OFS, optionName))
}
}
}
}
}
}
}
fun <KA, KB, K> FunctorIn<KA, CharSequence, K>.performMap(fo: FunctorOut<String, KB, K>, ka: KA): KB {
return ka.fmap(fo) { it.toString() + " is a CharSequence" }
}
@JvmName("fmapIntToString")
fun <KA, KB, K> FunctorIn<KA, Int, K>.performMap(fo: FunctorOut<String, KB, K>, ka: KA): KB {
return ka.fmap(fo) { it.toString() + " is an Int" }
}
@JvmName("fmapInt")
fun <KA, KB, K> FunctorIn<KA, Int, K>.performMap(fo: FunctorOut<Int, KB, K>, ka: KA): KB {
return ka.fmap(fo) { it + 5 }
}