Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[DISCUSSION] Exact Core 0.0.1 #8

Merged
merged 12 commits into from
May 8, 2023
6 changes: 6 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,20 @@ kotlin {
implementation("io.kotest:kotest-property:5.6.1")
implementation("io.kotest:kotest-framework-engine:5.6.1")
implementation("io.kotest:kotest-assertions-core:5.6.1")
implementation("io.kotest:kotest-framework-datatest:5.6.1")
implementation("io.kotest.extensions:kotest-assertions-arrow:1.3.3")
}
}

val jvmTest by getting {
dependencies {
implementation("io.kotest:kotest-runner-junit5-jvm:5.6.1")
implementation("io.kotest:kotest-runner-junit5:5.6.1")
}
}
}
}

tasks.withType<Test>().configureEach {
useJUnitPlatform()
}
33 changes: 19 additions & 14 deletions src/commonMain/kotlin/arrow.exact/Exact.kt
Original file line number Diff line number Diff line change
@@ -1,23 +1,28 @@
package arrow.exact

import arrow.core.*
import arrow.core.Either

interface Exact<A, out B> {

fun from(value: A): Either<ExactError, B>
interface Exact<A, out R> : ExactEither<ExactError, A, R>

fun fromOrNull(value: A): B? {
return from(value).getOrNull()
}
data class ExactError(val message: String)
ILIYANGERMANOV marked this conversation as resolved.
Show resolved Hide resolved

interface ExactEither<out E : Any, A, out R> {

fun from(value: A): Either<E, R>

// TODO: This doesn't work for some weird reason :/
// fun Raise<E>.fromOrRaise(value: A): R = from(value).bind()

fun fromOrThrow(value: A): B {
return when (val result = from(value)) {
is Either.Left -> throw ExactException(result.value.message)
is Either.Right -> result.value
}
fun fromOrNull(value: A): R? = from(value).getOrNull()

fun fromOrThrow(value: A): R = when (val result = from(value)) {
is Either.Left -> throw ExactException(result.value)
is Either.Right -> result.value
}
}

open class ExactError(val message: String)
// TODO: What are your thoughts about this?
operator fun invoke(value: A): R = fromOrThrow(value)
Copy link
Collaborator Author

@ILIYANGERMANOV ILIYANGERMANOV May 6, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a bit controversial. What do you think?
My opinion is that there are many cases where you're sure that something matches the constraints and going through the hassle of dealing with an Either is unnecessary.

// here I'm certain that positive + positive is always a positive
fun plus(x: PositiveInt, y: PositiveInt): PositiveInt = PositiveInt(x.value + y.value)

fun plusUgly(x: PositiveInt, y: PositiveInt): PositiveInt = PositiveInt.fromOrThrow(x.value + y.value)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I personally think orThrow is concise enough it's worth the readability. Kotlin favours explicitness in a lot of areas, and this seems a place that is a good trade-off for it as well.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's true @nomisRev . IMO, it just becomes too verbose, distracting and not right making the possibility of throwing an exception for cases where that's obviously impossible like:

val hello = NotBlankTrimmedString.fromOrThrow("Hello")
val world = NotBlankTrimmedString.fromOrThrow("wold!")
val helloWorld = NotBlankTrimmedString.fromOrThrow(hello.value + " " + world.value)

Personal opinion, the above is too much noise and I prefer the below:

val hello = NotBlankTrimmedString("Hello")
val world = NotBlankTrimmedString("wold!")
val helloWorld = NotBlankTrimmedString(hello.value + " " + world.value)

Note these two are artificial examples but in practice we have many places where we're certain that a constraint won't be violated, in my personal projects I reserve the operator fun invoke(value: A): R = fromOrThrow(value) for such cases.

When using for example PositiveInt(x + y) instead of PositiveInt.fromOrThrow(x + y) we show our readers that we're certain that in the first case x + y will always be positive but in the 2nd we direct their attention to knowing that it's not always the case.

I suggest leaving it for discussion and re-visiting before releasing the library.

}

class ExactException(message: String) : IllegalArgumentException(message)
class ExactException(error: Any) : IllegalArgumentException("ArrowExact error: $error")
38 changes: 15 additions & 23 deletions src/commonMain/kotlin/arrow.exact/ExactDsl.kt
Original file line number Diff line number Diff line change
@@ -1,30 +1,22 @@
package arrow.exact

import arrow.core.*
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.either

internal class AndExact<A, B, C>(
private val exact1: Exact<A, B>,
private val exact2: Exact<B, C>
) : Exact<A, C> {
@DslMarker
annotation class ExactDsl

override fun from(value: A): Either<ExactError, C> {
return exact1.from(value)
.flatMap { exact2.from(it) }
}
@ExactDsl
fun <A, R> exact(
construct: Raise<ExactError>.(A) -> R
): Exact<A, R> = object : Exact<A, R> {
override fun from(value: A): Either<ExactError, R> = either { construct(value) }
}

fun <A, B> exact(predicate: Predicate<A>, constructor: (A) -> B): Exact<A, B> {
return object : Exact<A, B> {
override fun from(value: A): Either<ExactError, B> {
return if (predicate.invoke(value)) {
constructor.invoke(value).right()
} else {
ExactError("Value ($value) doesn't match the predicate").left()
}
}
}
}

infix fun <A, B, C> Exact<A, B>.and(other: Exact<B, C>): Exact<A, C> {
return AndExact(this, other)
@ExactDsl
fun <E : Any, A, R> exactEither(
construct: Raise<E>.(A) -> R
): ExactEither<E, A, R> = object : ExactEither<E, A, R> {
override fun from(value: A): Either<E, R> = either { construct(value) }
}
77 changes: 77 additions & 0 deletions src/commonMain/kotlin/arrow.exact/demo/Demo.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package arrow.exact.demo

import arrow.core.Either
import arrow.core.flatMap
import arrow.core.raise.either
import arrow.core.raise.ensure
import arrow.core.right
import arrow.exact.*
import kotlin.jvm.JvmInline
import kotlin.random.Random

// TODO: We need a lint check telling people to make their constructors private
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we make an issue to make a small detekt integration module for this?

@JvmInline
value class NotBlankString private constructor(
val value: String
) {
companion object : Exact<String, NotBlankString> by exact({
ensure(it.isNotBlank()) { ExactError("Cannot be blank.") }
NotBlankString(it)
})
}

// TODO: We need a lint check telling people to make their constructors private
@JvmInline
value class NotBlankTrimmedString private constructor(
val value: String
) {
companion object : Exact<String, NotBlankTrimmedString> by exact({ raw ->
val notBlank = NotBlankString.from(raw).bind()
NotBlankTrimmedString(notBlank.value.trim())
})
}

sealed interface UsernameError {
object Invalid : UsernameError
data class Offensive(val username: String) : UsernameError
}

@JvmInline
value class Username private constructor(
val value: String
) {
companion object : ExactEither<UsernameError, String, Username> by exactEither({ rawUsername ->
val username = NotBlankTrimmedString.from(rawUsername) // compose Exact
.mapLeft { UsernameError.Invalid }.bind().value
ensure(username.length < 100) { UsernameError.Invalid }
ensure(username !in listOf("offensive")) { UsernameError.Offensive(username) }
Username(username)
})
}

@JvmInline
value class PositiveInt private constructor(
val value: Int
) {
companion object : Exact<Int, PositiveInt> by exact({
ensure(it > 0) { ExactError("Must be positive.") }
PositiveInt(it)
})
}

fun demo(): Either<String, Unit> = either {
val hello = NotBlankTrimmedString("Hello")
val world = NotBlankTrimmedString("World")

val helloWorld = NotBlankTrimmedString.from(hello.value + " " + world.value)
.mapLeft { it.message }.bind()
val username = Username.from("user1")
.mapLeft { it.toString() }.bind()


val x = PositiveInt(3)
val y = PositiveInt.from(Random.nextInt())
val z = y.flatMap { y1 ->
PositiveInt(x.value + y1.value).right()
}
}
37 changes: 0 additions & 37 deletions src/commonTest/kotlin/arrow/exact/ExactSpec.kt

This file was deleted.

21 changes: 0 additions & 21 deletions src/commonTest/kotlin/arrow/exact/Strings.kt

This file was deleted.