oolong-kt / oolong

MVU for Kotlin Multiplatform
http://oolong-kt.org
Apache License 2.0
300 stars 14 forks source link

Remove ': Any' bounds in generic types #108

Closed jcornaz closed 4 years ago

jcornaz commented 4 years ago

Is your feature request related to a problem? Please describe.

In Kotlin, we have the chance to have a compiler that makes safe to deal with nullability. Therefore, unlike in Java where null is very dangerous value that must be avoided at all cost, in Kotlin null is a valid and useful value to represent the absence of something. Yet the compiler will help us to make sure we treat it safely. On other words, the null of Kotlin is the equiavlent of Nothing in Elm: Useful and safe to use.

Yet, many generic functions in oolong force the generic argument to be not null. Example: fun <A : Any, B : Any> map(effect: Effect<A>, f: (A) -> B): Effect<B>

This adds an unecessary constraint on something that could have been safely nullable.

It is also viral and will cause any helpers built on top to also add that generic boundary. Example:

// Doesn't compile unless adding `: Any` bound to `T` and `R`
fun <T, R> Effect<T>.map(transform: (T) -> R): Effect<R> = map(this, transform)

By the way, as a side question: why not making map an extension function on Effect? It would be easier to discover and natural, since in Kotlin we're use to map over collections, sequences, flows, etc.

I aggree, that it is probably very uncomon to use nullable types for model or messages. But, in a world of safe nullability, I don't see why it should completly be prevented by the framework. In elm for instance, one can freely use Maybe for model and messages.

Describe the solution you'd like Remove uncessary : Any bound on generic arguments.

Describe alternatives you've considered For a usage point of view, I can use Optional when I want deal with nullability in model or message. But it is not very idiomatic, we have nullability directly in the Kotlin type system.

pardom commented 4 years ago

While my original intent was to align more with languages like Elm, Haskell, and PureScript, I can see relaxing this constraint might fit better in Kotlin. I'll want to consider the implications of this change first.

With regard to utility functions not being extensions, again the reasoning is to align more with the pure functional languages which inspired this library. I think extension function delegates would be a good addition.

Glad to accept a PR for the latter while investigating the former.

pardom commented 4 years ago

@oolong-kt/developers thoughts?

pardom commented 4 years ago

@jcornaz please update this issue with any other extension function you would like to see added: https://github.com/oolong-kt/oolong/pull/112

pardom commented 4 years ago

@jcornaz could you provide some use-cases where any of Model, Msg, or Props might be null? The non-null bounds on these functions don't restrict properties of these types from being null, just the types themselves.

On other words, the null of Kotlin is the equiavlent of Nothing in Elm

Elm does not have null and Kotlin's nullable types are not a good analog for Maybe because null occupies every type whereas Nothing does not. In Kotlin an Int? can be null, but in Elm an Int can never be Nothing.

For a usage point of view, I can use Optional when I want deal with nullability in model or message. But it is not very idiomatic, we have nullability directly in the Kotlin type system.

You can still use nullable types as properties of Model or Msg, just not the types themselves. For example:

data class Model(
    val itemId: ItemId,
    val item: Item? = null // null when not itemId not loaded yet
)

sealed class Msg {
    object LoadItem : Msg()
    data class SetItem(val item: Item?) : Msg() // can assign null to item
}

Until there is a compelling use-case to justify allowing nullable types for the runtime, I will keep the bounds in place.

jcornaz commented 4 years ago

null occupies every type

No! Sorry, but that's plain wrong.

In Kotlin an Int, String, List<T>, etc. cannot be null. null does not occupy every type. Only the nullable types.

In Kotlin an Int? can be null

Likewise a Maybe Int in Elm can be Nothing.

but in Elm an Int can never be Nothing

Likewise an Int in Kotlin can never be null.

I am saying that Kotlin's Int? is equivalent to Elm's Maybe Int and Kotlin's Int to Elm's Int. Why are you comparing kotlin's Int? to Elm's Int? Of course Kotlin's Int? is not equivalent to Elm's Int, and is in fact not equivalent to the Kotlin Int either!

Basically your sentence could be written using only Kotlin: "In Kotlin a Int? can be null, but in Kotlin an Int can never be null"

Or only using Elm: "In Elm a Maybe Int can be Nothing, but in Elm an Int can never be Nothing"

That doesn't help.

Both Elm and Kotlin try to achieve compile-time safety by encoding the potential absence of value in the type. So that the compiler can fail in case we try to use a value that is potentially absent.

Here is an example in Elm:

-- This function takes a normal value. We are guaranteed that the value is never null
foo : String -> Int
foo = String.length

-- This does not compile, because we must be sure to have a value before calling `foo`
invalidUsage : Maybe String -> Int
invalidUsage String.length

-- This compiles, because we made sure we have a value before calling foo
validUsage : Maybe String -> Int
validUsage value = 
  Maybe.map foo value |> Maybe.withDefault 0

And here is how it would be written Kotlin:

// This function takes a normal value. We are guaranteed that the value is never null
fun foo(value: String): Int = value.length

// This does not compile, because we must be sure to have a value before calling `foo`
fun invalidUsage(value: String?): Int = foo(value)

// This compiles, because we made sure we have a value before calling foo
fun validUsage(value: String?): Int = value?.let(::foo) ?: 0

As you see, Elm's Maybe and Kotlin's nullabibility shares the same properties and try to achieve the same goal: Checking at compile time that we are sure to have a value before using it.

About my use case:

My use case is actually already mentionned. I would like to write extenstion functions and utilities to extend oolong. But the : Any bound forces me to also add this bound to each of my function, adding unecessary complexity to my code.

pardom commented 4 years ago

null occupies every type

No! Sorry, but that's plain wrong.

You're correct, I meant that null occupies every nullable type.

I am saying that Kotlin's Int? is equivalent to Elm's Maybe Int and Kotlin's Int to Elm's Int.

I understand that Maybe operations and null safety operations are similar in practice, however those types are not equivalent.

In elm for instance, one can freely use Maybe for model and messages.

You are free to use Maybe for Model, Msg, and Props in Oolong as well. The non-null type bounds prevent you from using Maybe? in your example. That being said, this is Kotlin, not Elm, and I can appreciate that some users -- for whatever reason -- may want to express Model, Msg, and Props as null.

I'm glad to accept a PR for further review.

pardom commented 4 years ago

Let's continue discussing here https://github.com/oolong-kt/oolong/pull/115