I wonder what the Arrow team is planning, however....
# arrow
y
I wonder what the Arrow team is planning, however. I split the Functor interface into 2 btw because I originally had a version with the normal Functor interface but it then required an
@with
decorator for each pair of ins-and-outs, as in you'd need all of an
@with(optionFunctor<Int, String>())
, an
@with(optionFunctor<String, String>())
, an
@with(optionFunctor<Unit, String>())
, and an
@with(optionFunctor<Any, String>())
in the above example to make it work, and that multiples massively for each different type of input or output that you need to include, while with the splitting you only need to have a decorator for the sum of the number of inputs and outputs that you have. I think that probably having a huge amount of decorators will make the code look ugly even if it was generated automatically by the IDE, but I still have that original code if needed. Hopefully though you guys have already made a much much better encoding than this horrendous mess lol.
Copy code
// 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 : @UnsafeVariance K, out A, K> {
  fun <B, KB : K> KA.fmap(fo: FunctorOut<B, KB, @UnsafeVariance K>, mapper: (A) -> B): KB
}

interface FunctorOut<in B, out KB : @UnsafeVariance K, K> {
  fun K.toKB(): KB // this is just used for type safety and to push unsafe conversions to the XFunctorOutImpl
  fun @UnsafeVariance KB.toK(): @UnsafeVariance K
}

interface ApplicativeIn<in KA : @UnsafeVariance K, out A, K> : FunctorIn<KA, A, K> {
  fun <B, KB : K, KFUNC> <http://KA.app|KA.app>(fo: ApplicativeOut<B, KB, KFUNC, @UnsafeVariance K>, applied: KFUNC): KB
}

interface ApplicativeOut<in B, out KB : K, in KFUNC, K> : FunctorOut<B, KB, K> {
  fun pure(b: B): KB
  operator fun KFUNC.invoke(a: Any?): KB
}

interface MonadIn<in KA : K, out A, K> : ApplicativeIn<KA, A, K> {
  fun <B, KB : K> KA.flatMap(fo: FunctorOut<B, KB, @UnsafeVariance K>, mapper: (A) -> KB): KB
}

interface OptionMonadIn<out A> : MonadIn<Option<@UnsafeVariance A>, A, Option<*>>

// 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 a star projection
interface OptionApplicativeOut<in B> : ApplicativeOut<B, Option<@UnsafeVariance B>, Option<(Any?) -> B>, Option<*>>

// 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
  }

  inline fun <B> bind(mapper: (A) -> Option<B>): Option<B> = when (this) {
    is Some -> when (val other = mapper(a)) {
      is Some -> Some(other.a)
      is None -> None
    }
    is None -> None
  }
}

data class Some<A>(val a: A) : Option<A>()
object None : Option<Nothing>()
object OptionMonadInImpl : OptionMonadIn<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<*>> Option<Any?>.fmap(fo: FunctorOut<B, KB, Option<Any?>>, mapper: (Any?) -> B) =
    with(fo) { map(mapper).toKB() }

  override fun <B, KB : Option<*>, KFUNC> Option<Any?>.app(
    fo: ApplicativeOut<B, KB, KFUNC, Option<Any?>>,
    applied: KFUNC
  ): KB {
    with(fo) {
      return when (this@app) {
        is Some -> applied(a)
        is None -> None.toKB()
      }
    }
  }

  override fun <B, KB : Option<*>> Option<Any?>.flatMap(fo: FunctorOut<B, KB, Option<Any?>>, mapper: (Any?) -> KB): KB {
    return with(fo) {
      bind { mapper(it).toK() }.toKB()
    }
  }
}

object OptionApplicativeOutImpl : OptionApplicativeOut<Any?> {
  override fun Option<Any?>.toKB(): Option<Any?> {
    return this
  }

  override fun pure(b: Any?): Option<Any?> {
    return Some(b)
  }

  override fun Option<(Any?) -> Any?>.invoke(a: Any?): Option<Any?> {
    return when (this) {
      is Some -> Some(this.a(a))
      is None -> None
    }
  }

  override fun Option<Any?>.toK(): Option<Any?> {
    return this
  }
}

inline fun <A> optionApplicativeIn() = OptionMonadInImpl as OptionMonadIn<A>
inline fun <B> optionApplicativeOut() = OptionApplicativeOutImpl as OptionApplicativeOut<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(optionApplicativeIn<Any>()) { // This has to be first because subtypes need to come at the very end
  with(optionApplicativeIn<Unit>()) {
    with(optionApplicativeIn<String>()) {
      with(optionApplicativeIn<Int>()) {
        with(optionApplicativeOut<String>()) OFS@{
          with(optionApplicativeOut<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))
            println(optionLife.flatMap(this@OFS) { life ->
              optionName.flatMap(this@OFS) { name ->
                optionUnit.flatMap(this@OFS) { unit ->
                  optionExistence.flatMap(this@OFS){ existence ->
                    Some("$life + $name + $unit + $existence")
                  }
                }
              }
            })
          }
        }
      }
    }
  }
  }
}

fun <KA : K, KB : K, 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 : K, KB : K, 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 : K, KB : K, K> FunctorIn<KA, Int, K>.performMap(fo: FunctorOut<Int, KB, K>, ka: KA): KB {
  return ka.fmap(fo) { it + 5 }
}
I couldn't help it, and so I also created the definition for Applicative and Monad, There were a few interesting challenges along the way, but thankfully nothing was too hard (I did a few small changes like constraining KA and KB to be subtypes of K but that's all):
And here's an example using an invariant Option to show that it is indeed possible to support it with just a few unsafe-casts-which-aren't-really-unsafe, which in turn proves that this modelling works for any 1-type-parameter monad:
Copy code
// 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 : @UnsafeVariance K, out A, K> {
  fun <B, KB : K> KA.fmap(fo: FunctorOut<B, KB, K>, mapper: (A) -> B): KB
}

interface FunctorOut<in B, out KB : K, K> {
  fun K.toKB(): KB // this is just used for type safety and to push unsafe conversions to the XFunctorOutImpl
  fun @UnsafeVariance KB.toK(): K
}

interface ApplicativeIn<in KA : K, out A, K> : FunctorIn<KA, A, K> {
  fun <B, KB : K, KFUNC> <http://KA.app|KA.app>(fo: ApplicativeOut<B, KB, KFUNC, K>, applied: KFUNC): KB
}

interface ApplicativeOut<in B, out KB : K, in KFUNC, K> : FunctorOut<B, KB, K> {
  fun pure(b: B): KB
  operator fun KFUNC.invoke(a: Any?): KB
}

interface MonadIn<in KA : K, out A, K> : ApplicativeIn<KA, A, K> {
  fun <B, KB : K> KA.flatMap(fo: FunctorOut<B, KB, K>, mapper: (A) -> KB): KB
}

interface OptionMonadIn<out A> : MonadIn<Option<@UnsafeVariance A>, A, Option<*>>

// 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 a star projection
interface OptionApplicativeOut<in B> : ApplicativeOut<B, Option<@UnsafeVariance B>, Option<(Any?) -> @UnsafeVariance B>, Option<*>>

// Can also use an inline class here possibly but whatever
sealed class Option<A> {
  inline fun <B> map(mapper: (A) -> B): Option<B> = when (this) {
    is Some -> Some(mapper(a))
    is None -> None as Option<B>
  }

  inline fun <B> bind(mapper: (A) -> Option<B>): Option<B> = when (this) {
    is Some -> when (val other = mapper(a)) {
      is Some -> Some(other.a)
      is None -> None
    }
    is None -> None
  }as Option<B>
}

data class Some<A>(val a: A) : Option<A>()
object None : Option<Nothing>()
object OptionMonadInImpl : OptionMonadIn<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<*>> Option<Any?>.fmap(fo: FunctorOut<B, KB, Option<*>>, mapper: (Any?) -> B) =
    with(fo) { map(mapper).toKB() }

  override fun <B, KB : Option<*>, KFUNC> Option<Any?>.app(
    fo: ApplicativeOut<B, KB, KFUNC, Option<*>>,
    applied: KFUNC
  ): KB {
    with(fo) {
      return when (this@app) {
        is Some -> applied(a)
        else -> None.toKB()
      }
    }
  }

  override fun <B, KB : Option<*>> Option<Any?>.flatMap(fo: FunctorOut<B, KB, Option<*>>, mapper: (Any?) -> KB): KB {
    return with(fo) {
      bind { mapper(it).toK() }.toKB()
    }
  }
}

object OptionApplicativeOutImpl : OptionApplicativeOut<Any?> {
  override fun Option<*>.toKB(): Option<Any?> {
    return this as Option<Any?>
  }

  override fun pure(b: Any?): Option<Any?> {
    return Some(b)
  }

  override fun Option<(Any?) -> Any?>.invoke(a: Any?): Option<Any?> {
    return when (this) {
      is Some -> Some(this.a(a))
      else -> None as Option<Any?>
    }
  }

  override fun Option<Any?>.toK(): Option<Any?> {
    return this
  }
}

inline fun <A> optionApplicativeIn() = OptionMonadInImpl as OptionMonadIn<A>
inline fun <B> optionApplicativeOut() = OptionApplicativeOutImpl as OptionApplicativeOut<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 as Option<Any>
  val optionName = Some("name")
  val optionUnit = Some(Unit)
  println("hello world")
  // Using with to emulate the @with(value) decorator
  with(optionApplicativeIn<Any>()) { // This has to be first because subtypes need to come at the very end
    with(optionApplicativeIn<Unit>()) {
      with(optionApplicativeIn<String>()) {
        with(optionApplicativeIn<Int>()) {
          with(optionApplicativeOut<String>()) OFS@{
            with(optionApplicativeOut<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))
              println(optionLife.flatMap(this@OFS) { life ->
                optionName.flatMap(this@OFS) { name ->
                  optionUnit.flatMap(this@OFS) { unit ->
                    optionExistence.flatMap(this@OFS) { existence ->
                      Some("$life + $name + $unit + $existence")
                    }
                  }
                }
              })
            }
          }
        }
      }
    }
  }
}

fun <KA : K, KB : K, 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 : K, KB : K, 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 : K, KB : K, K> FunctorIn<KA, Int, K>.performMap(fo: FunctorOut<Int, KB, K>, ka: KA): KB {
  return ka.fmap(fo) { it + 5 }
}
I realised that I messed up with the definition of Applicative, especially with its KFUNC type param, which was an eye opener to the need for having
XBoth
interfaces, and so here's the corrected implementation, which actually looks neater tbh:
Copy code
import kotlin.experimental.ExperimentalTypeInference

// 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 : K, out A, K> {
  fun <B, KB : K> KA.fmap(fo: FunctorOut<B, KB, K>, mapper: (A) -> B): KB
}

interface FunctorOut<in B, out KB : K, K> {
  fun K.toKB(): KB // this is just used for type safety and to push unsafe conversions to the XFunctorOutImpl
  fun @UnsafeVariance KB.toK(): K
}

interface FunctorBoth<in KA : K, out A, in B, out KB : K, K> : FunctorIn<KA, A, K>,
  FunctorOut<B, KB, K>

interface ApplicativeIn<in KA : K, out A, K> : FunctorIn<KA, A, K> {
}

interface ApplicativeOut<in B, out KB : K, K> : FunctorOut<B, KB, K> {
  fun pure(b: B): KB
}

interface ApplicativeBoth<in KA : K, out A, in B, out KB : K, KFUNC: K, K> : ApplicativeIn<KA, A, K>,
  ApplicativeOut<B, KB, K>, FunctorBoth<KA, A, B, KB, K> {
  fun <B, KB : K> <http://KA.app|KA.app>(fo: ApplicativeOut<B, KB, K>, applied: KFUNC): KB
  fun pure(b: (A) -> B): KFUNC = pure(b as B) as KFUNC

  @OptIn(ExperimentalTypeInference::class)
  @Suppress("INAPPLICABLE_JVM_NAME")
  @JvmName("fmapFunc")
  @OverloadResolutionByLambdaReturnType
  fun <B> KA.fmap(fo: FunctorOut<B, K, K>, mapper: (A) -> ((A) -> B)): KFUNC = this.fmap(fo, mapper as (A) -> B) as KFUNC
}

interface MonadIn<in KA : K, out A, K> : ApplicativeIn<KA, A, K> {
  fun <B, KB : K> KA.flatMap(fo: FunctorOut<B, KB, K>, mapper: (A) -> KB): KB
}

interface MonadOut<in B, out KB : K, K> : ApplicativeOut<B, KB, K>

interface MonadBoth<in KA : K, out A, in B, out KB : K, KFUNC: K, K> : MonadIn<KA, A, K>, MonadOut<B, KB, K>,
  ApplicativeBoth<KA, A, B, KB, KFUNC, K> {
  @OptIn(ExperimentalTypeInference::class)
  @Suppress("INAPPLICABLE_JVM_NAME")
  @JvmName("flatMapFunc")
  @OverloadResolutionByLambdaReturnType
  fun <B> KA.flatMap(fo: FunctorOut<B, K, K>, mapper: (A) -> (KFUNC)): KFUNC = this.flatMap<B, K>(fo, mapper) as KFUNC
  }

interface OptionMonadIn<out A> : MonadIn<Option<@UnsafeVariance A>, A, Option<*>>

// 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 a star projection
interface OptionMonadOut<in B> : MonadOut<B, Option<@UnsafeVariance B>, Option<*>>
interface OptionMonadBoth<out A, in B> : OptionMonadIn<A>, OptionMonadOut<B>,
  MonadBoth<Option<@UnsafeVariance A>, A, B, Option<@UnsafeVariance B>, Option<(@UnsafeVariance A) -> @UnsafeVariance B>, Option<*>>

// Can also use an inline class here possibly but whatever
sealed class Option<A> {
  inline fun <B> map(mapper: (A) -> B): Option<B> = when (this) {
    is Some -> Some(mapper(a))
    is None -> None as Option<B>
  }

  inline fun <B> bind(mapper: (A) -> Option<B>): Option<B> = when (this) {
    is Some -> when (val other = mapper(a)) {
      is Some -> Some(other.a)
      is None -> None
    }
    is None -> None
  } as Option<B>
}

data class Some<A>(val a: A) : Option<A>()
object None : Option<Nothing>()
object OptionMonadBothImpl : OptionMonadBoth<Any?, Any?> {
  override fun Option<*>.toKB(): Option<Any?> {
    return this as Option<Any?>
  }

  override fun pure(b: Any?): Option<Any?> {
    return Some(b)
  }

  override fun Option<Any?>.toK(): Option<Any?> {
    return this
  }

  // 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<*>> Option<Any?>.fmap(fo: FunctorOut<B, KB, Option<*>>, mapper: (Any?) -> B) =
    with(fo) { map(mapper).toKB() }

  override fun <B, KB : Option<*>> Option<Any?>.app(
    fo: ApplicativeOut<B, KB, Option<*>>,
    applied: Option<(Any?) -> Any?>
  ): KB {
    with(fo) {
      return when (applied) {
        is Some -> when(this@app) {
          is Some -> Some(applied.a(a))
          else -> None
        }
        else -> None
      }.toKB()
    }
  }

  override fun <B, KB : Option<*>> Option<Any?>.flatMap(fo: FunctorOut<B, KB, Option<*>>, mapper: (Any?) -> KB): KB {
    return with(fo) {
      bind { mapper(it).toK() }.toKB()
    }
  }
}

inline fun <A> optionApplicativeIn() = OptionMonadBothImpl as OptionMonadIn<A>
inline fun <B> optionApplicativeOut() = OptionMonadBothImpl as OptionMonadOut<B>
inline fun <A, B> optionApplicativeBoth() = OptionMonadBothImpl as OptionMonadBoth<A, 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 as Option<Any>
  val optionName = Some("name")
  val optionUnit = Some(Unit)
  println("hello world")
  // Using with to emulate the @with(value) decorator
  with(optionApplicativeIn<Any>()) { // This has to be first because subtypes need to come at the very end
    with(optionApplicativeIn<Unit>()) {
      with(optionApplicativeIn<String>()) {
        with(optionApplicativeIn<Int>()) {
          with(optionApplicativeOut<String>()) OFS@{
            with(optionApplicativeOut<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))
              println(optionLife.flatMap(this@OFS) { life ->
                optionName.flatMap(this@OFS) { name ->
                  optionUnit.flatMap(this@OFS) { unit ->
                    optionExistence.flatMap(this@OFS) { existence ->
                      Some("$life + $name + $unit + $existence")
                    }
                  }
                }
              })
            }
          }
        }
      }
    }
  }
}

fun <KA : K, KB : K, 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 : K, KB : K, 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 : K, KB : K, K> FunctorIn<KA, Int, K>.performMap(fo: FunctorOut<Int, KB, K>, ka: KA): KB {
  return ka.fmap(fo) { it + 5 }
}
s
Hey @Youssef Shoaib [MOD], We're tried many different encodings including something very similar to this but it results in code that was to complex for users to use comfortably. In addition to a bunch of IDEA performance issues. Currently, we're just removing HKT from Arrow. If it returns it would a compiler plugin that automatically enables it without requiring any encodings like this or like we had before.
🙌 1
y
@simon.vergauwen nice that makes absolutely perfect sense. I'm guessing that probably the benefits of hkts is not worth it in this case. Thank you!