Youssef Shoaib [MOD]
03/13/2021, 3:51 PM@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.Youssef Shoaib [MOD]
03/13/2021, 7:31 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 : @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 }
}
Youssef Shoaib [MOD]
03/13/2021, 7:31 PMYoussef Shoaib [MOD]
03/13/2021, 8:09 PMYoussef Shoaib [MOD]
03/13/2021, 8:09 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 : @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 }
}
Youssef Shoaib [MOD]
03/13/2021, 10:31 PMXBoth
interfaces, and so here's the corrected implementation, which actually looks neater tbh:Youssef Shoaib [MOD]
03/13/2021, 10:31 PMimport 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 }
}
simon.vergauwen
03/15/2021, 12:08 PMYoussef Shoaib [MOD]
03/15/2021, 5:12 PM