Hi, I require Higher Kinded types in my current pr...
# arrow
t
Hi, I require Higher Kinded types in my current project. I believe Arrow has deprecated them though, therefore what approach has replaced Arrows Higher Kinded Types?
j
Strictly speaking: Nothing. Higher kinds are simply not very usable in kotlin and have been quite the pain point for a while. Now that is not the whole story: Higher kinds are usually used for effects, and those can be emulated sufficiently via suspend functions and concrete types. For example most normal application use can get by with using
IO = suspend
and
Either
for modeling errors. To be clear, this is simply not equally powerful as higher kinded types in kotlin and there is a lot this cannot do, however it is easier to use and sufficient for most. There are two common uses for higher kinds: • monad stacks for effects: This is replaced by
suspend
functions and comprehensions, however atm it is not equally powerful because kotlin continuations cannot be resumed multiple times and thus we can only model short-circuit-like monads with effects (such as
either
or any nullable type).
IO
is equal to
suspend
in arrow terms. • Abstracting over "containers" of values. This is simply not possible without kinds, suspend functions or not. If your use case falls under this category, you are better off rolling your own kind implementation 😕 But with arrow-meta this may very well come back at some point, but for now its simply not there...
So in short: Arrow has moved away from kinds and typeclasses, to exporting more function over suspend and the base types such as
Either
. Kinds may or may not come back, but not in the form as before and certainly not without compiler plugins
r
I was writing a long response but @Jannis’s is better 🙂. I would just add that suspend is more powerful than IO in the sense that it can also avoid transformer stacks by using inline functions. This is more ergonomic and more Kotlin like that something based on
F
or transformer stacks for the case of IO which is what F is most of the time. It’s also much faster because the compiler optimizes suspend programs over your traditional IO based ones. Even with kinds, since in Kotlin they are emulated, partially applying type arguments of kinds like Either and downcasting with
fix
is painful and something we don’t want to proliferate as idiom in Arrow.
t
thanks for your time in responding, maybe I have the wrong solution for my requirement. What I am attempting to achieve is to to define a relationship between two types. as an example imagine an
Input
and
Output
, what I require is to define that
Input1
can only produce
Output1
, and
Input2
can only produce
Output2
etc.. in addition I also need to define the data type contained in each
Input
and
Output
, so that
Input1
can only contain a
String
, and
Output1
can only contain a
Long
(I am only using these simple data types for example purposes, in reality I would use Complex custom
data classes
) , wouldnt Higher Kinded Types solve this for me?, I am lost as to develop another solution that can both define a strict releationship between my Input & Output types and the data they contain. I need to be able to define that Input1 can only ever contain DataTypeA and only ever produce Output1; That Output1 can only ever contain DataTypeB. I wish to define an entire "family" of Input(s) and Output(s) that have the above restrictions. Is that possible in Kotlin?
s
Are you thinking as about type proofs?

https://youtu.be/lK80dPcsNUg

r
Type proofs can constraint that but is not ready for use and it’s being split into smaller focused plugins for given, macros and refined types.
@Tower Guidev2 Do you have a small example with kinds or without of what it looks like with code?
What you describe there sounds more like a type refinement than a kind itself. Maybe value classes and refined types can help https://meta.arrow-kt.io/apidocs/arrow-refined-types/arrow.refinement/
t
Im an Android developer trying to get to grips with MVI Im attempting to define User Actions such as LoginAction. Each Action will have an associated Reaction, e.g. LoginReaction. I want to restrict that LoginAction can only ever produce a LoginReaction And more generally Action1 can only ever produce Reaction1, Action2 can only ever produce Reaction2 In pseudo code
val action: Action = LoginAction(username, password)
viewmodel.react(action).collect {
reaction ->
when (reaction) {
LoginReaction -> println("\t\t ${reaction.reaction.output}")
Reaction1 -> println("\t\t ${reaction.reaction.output}")
Reaction2 -> println("\t\t ${reaction.reaction.output}")
Reaction3 -> println("\t\t ${reaction.reaction.output}")
}
}
In my viewmodel the react function I would have
fun react(action: Action): Flow<Reaction> = flow {
val reaction: Reaction = when (action) {
is LoginAction -> {
Val accessCode = callLoginApi(it.password, it.username)
LoginReaction(accessCode)
}
is ActionOne -> {
Reaction1(42L)
}
is ActionTwo -> {
Reaction2(CustomOutput(frank = 24L, homer = "Help!"))
}
is ActionThree -> {
Reaction3(CustomOutputTwo(geezer = 2442L, su = "Why"))
}
else -> TODO()
}
emit(reaction)
}
r
@Tower Guidev2 This is how I would model what I understood you are trying to achieve to ensure a given Reaction belongs to a specific type of Action https://gist.github.com/raulraja/5ebbd75061343b78260148719e3dcd05
🤩 1
t
@raulraja thanks very much for taking the time to develop this solution. It is indeed elegant 😄 However I am having difficulty in refactoring your solution to fit my requirement (as I am a Kotlin newbee) the issue I am having is I cannot get past this compile error
Type mismatch: inferred type is LoginReaction but Reaction<{A & LoginAction}> was expected
, when splitting your solution to fit how I wish to design my Android App. My issue is that each Android Activity will create multiple Action(s) and collect the related Reaction(s) I've attempted to refactor your code as follows:- In My Android ViewModel class I have created these two functions
Copy code
fun <A : Action> react(action: A): Flow<*> = flow {
    
    val reaction = when (action) {
        is LoginAction -> {
            val accessToken : String = LoginApi.login(action.password, action.password) // This is an Asynchronous RESTful API call
            react(action) { LoginReaction(accessToken) }  // COMPILE ERROR HERE!!!!!!!!!!!!
        }

        else -> TODO()
    }


    emit(reaction)

}

/**
 * This is the motherload. This function ensures that a given [Action] [A]
 * creates a [Flow] of [Reaction] for [R], a Reaction that only accepts actions of type [A]
 */
private inline fun <X : Action, Y : Reaction<X>> react(action: X, crossinline function: (X) -> Y): Reaction<X> {
    return function(action)
}
I call my public react fucntion from my activity:-
Copy code
val loginAction = LoginAction(UserName("test"), Password("test"))
viewmodel.react(loginAction).collectLatest {
    println("debug $it")
}
where has my lack of Kotlin expertise let me down? 🤔
r
Hi @Tower Guidev2, I think it may be some issue with the definition of the Action and Reaction hierarchies. Do you have this code somewhere public I can try to compile that shows the issue? Ideally without the Android deps 🙂
t
r
That makes sense but you are hitting a known limitation of Kotlin. Kotlin does not unify generics over all the values of a sealed hierarchy, it only uses sealed hierarchies for exhaustive pattern matching. Let me try to attempt to reencode the problem in a way where types would be fine. It involves making actions aware of which reactions they support in their flows.
t
Thanks for the explanation.. 😄
r
I guess in your example if you just want to work around the type limitation because your handler receives a generic action value and you just have to pattern match you could encode it like this:
Copy code
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.flow

class Aardvark {

  inline suspend fun <A : Action> react(action: A): Flow<Reaction<*>> =
    when (action) {
      is LoginAction -> react(action) { LoginReaction("UUID.randomUUID().toString()") }
      is ActionOne -> react(action) { ReactionOne("UUID.randomUUID().toString()") }
      else -> TODO()
    }

  inline suspend fun <X : Action, Y : Reaction<X>> react(action: X, crossinline function: (X) -> Y): Flow<Y> =
    flow { emit(function(action)) }
  
}
Embed the Flow in the reaction so it embeds the type
Y
of Reaction in its type argument and inference then knows
Y
above has to unify as a
Reaction<*>
. Since
Y
is declared as
Reaction<X>
and
*
in kotlin means
DONT_CARE
then it unifies properly. If you try to decompose in cases you will have to manually cast like in your case if you want to make it compile because Kotlin does not make an effort to unify generic type bounds with sealed hierarchies.
Also you don’t need in this case
A : Action
should work with just
Action
no type args since A is not reified or used in a projection in the return type
BTW this is my favorite sauce, should try it if you can ever get your hands on it https://secretaardvark.com/ xD
😆 1
t
@raulraja Thanks for resolving my issue and the hot sauce recommendation 😄 I hope I havent wasted too much of your time 😄
r
no problem! glad I could help 🙂
j
@raulraja I'm having trouble understanding this, can you unpack it a bit?
Kotlin does not make an effort to unify generic type bounds with sealed hierarchies
Also, while the use of generics and inline is of great interest to me, it's unclear to me what has been gained over the original implementation i.e.
Copy code
fun react(action: Action): Flow<Reaction> = flow {
    val reaction: Reaction = when (action) {
        is LoginAction -> {
            Val accessCode = callLoginApi(it.password, it.username)
            LoginReaction(accessCode)
        }
        is ActionOne -> {
            Reaction1(42L)
        }
        is ActionTwo -> {
            Reaction2(CustomOutput(frank = 24L, homer = "Help!"))
        }
        is ActionThree -> {
            Reaction3(CustomOutputTwo(geezer = 2442L, su = "Why"))
        }
        else -> TODO()
    }
    emit(reaction)
}
since the consumer of the flow is going to have to
when-is
on the
Reaction
s in the flow, either way.
r
Hi @julian , what I ment in the first case is that Kolin can’t make Generic ADTs unify to a type parameter A even if you contemplate all cases of the match, here is an unrelated example of the same problem and without type associations between the ADTs beside the generic type argument.
Copy code
sealed interface Exp<A>
data class NumberExp(val value: Int): Exp<Int>
data class StringExp(val value: String): Exp<String>

sealed interface Codec<A>
object IntCodec: Codec<Int>
object StringCodec: Codec<String>

fun <A> cantUnifyA(expr: Exp<A>): Codec<A> =
  when (expr) {
    is NumberExp -> IntCodec //Type mismatch: inferred type is IntCodec but Codec<A> was expected
    is StringExp -> StringCodec //Type mismatch: inferred type is StringCodec but Codec<A> was expected
  }
there is no gain unless the user uses directly the typed
react(action) { reaction }
to create its flows all individually
A way to work around the issue unifying the type arg A on two separate ADTs is to define abstract members in Action to react and making the subclasses implement them.
This would require no unification as you would just call
action.react()
which on the base class would have returned
Reaction<A>
where A is always matched by the members instead of using pattern matching.
With this new version the example above can indeed unify A:
Copy code
sealed interface Exp<A> {
  fun codec(): Codec<A>
}
data class NumberExp(val value: Int): Exp<Int> {
  override fun codec(): Codec<Int> = IntCodec
}
data class StringExp(val value: String): Exp<String> {
  override fun codec(): Codec<String> = StringCodec
}

sealed interface Codec<A>
object IntCodec: Codec<Int>
object StringCodec: Codec<String>

fun <A> canUnifyA(expr: Exp<A>): Codec<A> =
  expr.codec()
But this encoding forces you to handle the cases in the classes
here it knows
Codec<A>
refers to the same
A
as
Expr<A>
j
Okay, I get it now. Thanks so much for explaining in such detail.
r
The general issue is that kotlin does not know how to unify type arguments in when exhaustive matches for ADTs that are GADTs meaning they have type arguments
🙏 1
189 Views