MarioAriasC / funKTionale

Functional constructs for Kotlin
915 stars 71 forks source link

Refine generics in Option and Either #15

Closed laynepenney closed 7 years ago

laynepenney commented 7 years ago

Description

Better handle Option and Either generics. The default upper bounds in Kotlin, if not specified, is Any? -> [https://kotlinlang.org/docs/reference/generics.html#upper-bounds (https://kotlinlang.org/docs/reference/generics.html#upper-bounds).

Without these changes, it is currently legal to have a nullable option value:

fun getNull(): Option<String?> = Option.Some(null) 

Examples

    class Value<out T>(val value: T) /* i.e. NullableValue<out T : Any?> */ {
        override fun hashCode(): Int = value!!.hashCode() // must account for possibly null value (!! operator)
    }

    class NonNullValue<out T : Any>(val value: T) {
        override fun hashCode(): Int = value.hashCode() // value is not null, no need for !!
    }

    @Test fun generics() {
        // Legal to use 'String?' instead of 'String'
        val nullValue: Value<String?> = Value(null)

        // Compile error for nullable generic type
//        val nonNullValue: NonNullValue<String?>

        val nonNullValue: NonNullValue<String>
        nonNullValue = NonNullValue("non-null")

        assertEquals("default", nullValue.value ?: "default")
        assertEquals("non-null", nonNullValue.value ?: "default")
    }
ilya-g commented 7 years ago

Without these changes, it is currently legal to have a nullable option value:

Could you explain what's wrong with having Option for nullable types?

MarioAriasC commented 7 years ago

@ilya-g 'cause Option represent the presence of absence of a value. Allowing nulls for Options defeat the whole purpose.

Do you have any specific use case for an Option of nullable type?

ilya-g commented 7 years ago

null is a valid value for a nullable type and is not an absence of value.

As for the use cases, consider collection of unconstrained type T. Before I could write an extension, say Collection<T>.findOption(predicate): Option<T> which would have returned Some(T) when it found an item and None when it didn't. Another use case is Map<K, V>.getOption(K): Option<V> which again could return Some(V) even if the key is mapped to null value.

Now I either have to constrain the receivers to have non-nullable type arguments, or I can't distinguish missing value from null. Then what is the purpose of Option? Why should I use it instead of plain nullable types?

MarioAriasC commented 7 years ago

Indeed both cases were valid before this PR

https://github.com/MarioAriasC/funKTionale/blob/0.9/src/main/kotlin/org/funktionale/option/Option.kt#L177

But we did have a map.option[x] that returns Some<T> or None

https://github.com/MarioAriasC/funKTionale/blob/0.9/src/main/kotlin/org/funktionale/option/Option.kt#L165

toOption() doesn't make sense if is already not null...

And I, indeed, had this biting my ass a few days ago with a map, that I ended replacing with :? https://github.com/MarioAriasC/funKTionale/blob/master/src/main/kotlin/org/funktionale/memoization/namespace.kt#L288

Interesting... let me think about it

A decision should be made before hitting 1.0

MarioAriasC commented 7 years ago

@ilya-g your first case is already implemented (Collection<T>.findOption(predicate): Option<T>)

https://github.com/MarioAriasC/funKTionale/blob/master/src/main/kotlin/org/funktionale/option/Option.kt#L274

MarioAriasC commented 7 years ago

and toOption() is still possible

ilya-g commented 7 years ago

If it uses firstOrNull it still doesn't allow to distinguish between null and missing value.

d3xter commented 7 years ago

I've always used "null" as None in kotlin.

Functional languages do not have anything like null, they just have None.

@ilya-g In which real-world use-cases do you need both null and None?

MarioAriasC commented 7 years ago

@d3xter The actual question is, shall we allow nullable types inside Option?

By having an Option<String?> technically is possible to have a Some(null) weird, but is explicit and the Kotlin compiler will provide all the null safe guards.

Right now isn't possible as we don't allow nullable types...

d3xter commented 7 years ago

I'd say no. As said above, None is usually used as Value does not exist or Value is not set

When having Some(null), what does null stand for? It must not be Value does not exist Does it mean "Not Available"? "Not yet loaded"? I'd rather get an Enum-constant called Unknown or an empty class with a name, because then it is clear what it means.

And it can also lead to confusion for People, who come from functional languages, because they expect to get Some with a value in it.

MarioAriasC commented 7 years ago

This is a non exhaustive list of pros and cons of both approaches

Option<T: Any> aka Strict Not-Nullabe Option (Current implementation)

Pros Counter argument Counter-counter argument
Some doesn't allow null (expected behaviour) The same could be achieved with the other softer option type N/A
Strict default N/A N/A
Cons Counter argument Counter-counter argument
Limited functionality when interacts with types other than T: Any (e.g Writing extension functions) N/A N/A
T: Any is uncommon on Kotlin codebases, making Option<T: Any> unidiomatic N/A N/A

Option<T> aka Soft Option (previous implementation, considering coming back)

Pros Counter argument Counter-counter argument
Flexible Allows null It must be explicitly declared (Option<String?>) and all Kotlin's null-safety features apply
Idiomatic (covariance, extension functions and so on) N/A N/A
Other implementations are similar (@kmizu [https://github.com/kmizu/kollection]) N/A N/A
Cons Counter argument Counter-counter argument
Allows null It must be explicitly declared (Option<String?>) and all Kotlin's null-safety features apply N/A

Right now I'm seriously thinking on coming back to Option<T>

Your thoughts?

exallium commented 7 years ago

+1 for Option (Soft Option). I think that when it comes to nulls, we should allow the syntactic sugar of the language handle itself as necessary. I think that Option<T?> would be a rare occurrence as well. And as previously stated somewhere else, null does not necessarily mean absent, so it makes sense to me that we shouldn't be trying to reinvent a language feature in this sense, and opt for the more open, flexible approach.

The type system will automatically enforce non-nullability. It would be up to the programmer to decide to allow nulls in the optional.

MarioAriasC commented 7 years ago

Sorted https://github.com/MarioAriasC/funKTionale/pull/20