-
Notifications
You must be signed in to change notification settings - Fork 4
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
Conversation
Demo @JvmInline
value class NotBlankString private constructor(
override val value: String
) : Refined<String> {
companion object : Exact<String, NotBlankString> by exact({
ensure(it.isNotBlank()) { ExactError("Cannot be blank.") }
NotBlankString(it)
})
}
@JvmInline
value class NotBlankTrimmedString private constructor(
override val value: String
) : Refined<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(
override val value: String
) : Refined<String> {
companion object : ExactEither<UsernameError, String, Username> by exactEither({ rawUsername ->
val username = NotBlankTrimmedString.from(rawUsername)
.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(
override val value: Int
) : Refined<Int> {
companion object : Exact<Int, PositiveInt> by exact({
ensure(it > 0) { ExactError("Must be positive.") }
PositiveInt(it)
})
} I don't know about you but creating |
} | ||
|
||
// TODO: What are your thoughts about this? | ||
operator fun invoke(value: A): R = fromOrThrow(value) |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
Looks fantastic 🚀 I also want to explore these topics
ensure(it, NotBlankString) {
UsernameError.Invalid
} It will make chaining of
class UserName {
companion object : NotBlankString and StringWithLengthLt(16)
} It will also give us room to ship predefined Arrow Exacts and for communities to build their own and share. I had the same idea in
Will make one more round later but already looks as a good candidate for 0.0.1 |
+1 for all topics! Let's explore them 🚀
ensure(it, NotBlankString) { UsernameError.Invalid }
@ustits I'm wondering should we merge this PR so we can build 1) and 2) on top of it? Alternatively, each of us can play on a different versions of |
import kotlin.jvm.JvmInline | ||
import kotlin.random.Random | ||
|
||
// TODO: We need a lint check telling people to make their constructors private |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
All looks great 👏 👏 Great discussion and improvements overall
Looks doable and awesome for predicates! The downside is that with this type of ensure we lose all transformations - e.g. won't work with NotBlankTrimmedString.
ensure(it, NotBlankString) { UsernameError.Invalid }
Hmm, I'm not sure about that.. We could have NotBlankString
as a sort of filter map. So something like.
import arrow.core.Either
import arrow.core.raise.Raise
import arrow.core.raise.either
import arrow.core.raise.ensureNotNull
fun interface FilterMap<A : Any, B : Any> {
operator fun invoke(a: A): B?
}
fun <E, A : Any, B : Any> Raise<E>.ensure(
a: A,
filterMap: FilterMap<A, B>,
error: (A) -> E
): B {
val b = filterMap(a)
return ensureNotNull(b) { error(a) }
}
val NotBlankString: FilterMap<String, String> =
FilterMap { it.ifBlank { null } }
object Invalid
val x: Either<Invalid, String> = either {
ensure("hello", NotBlankString) { Invalid }
}
So by constraining A
and B
to non-null we can avoid needing Option
, and we can use null as a false signal for the predicate. I choose FilterMap
as a name here, but that's perhaps not a great name. MapNotNull
might be more in line with Kotlin Std. It offers a similar pattern for Iterable
.
I really like the idea of being able to compose, expose/share predicates, or refinements like this as @ustits did in krefty
.
We can probably remove Refined
, or replace it with a typealias Refined<A> = Either<ExactError, A>
and see how it plays out.
I am okay merging this PR, and doing follow ups. That would be easier for discussing further I think. Again really awesome @ustits and @ILIYANGERMANOV ❤️
This week I am going to setup all the configuration for publishing this library, so we can publish 0.0.1 whenever we think is a good moment 🥳
We can borrow some ideas from Clojure community, they had much more time refining them in specs https://clojure.org/guides/spec#_composing_predicates
Came up with the same thought after changing an api of refine(NotBlank(), "Krefty")
.map { NotBlankString(it) }
.flatMap { refine(UserNamePredicate(), it) }
Thanks! 🚀 |
Thank you both @ustits and @nomisRev! We're making awesome progress on this library! You suggested great points, let's explore them 🚀 I merged this PR for the sake of easier discussions and building on top of it. Changes:
TODO:
|
Is there a commit you can share? Would love to see how you implemented this?
Interesting, I like Clojure and hadn't thought of this for inspiration. I've always thought of Scala refined libraries, but the Clojure docs you shared have a lot of great ideas! |
@nomisRev you can check it by this commit where I added map and flatMap ustitc/krefty@ac67a74#diff-78a62f4e2244be79cf81a394e044b8971ac9ddd195afd6add4ef31d4c3e2f830 Also has a local commit which introduces refine("Krefty")
.filter(NotBlank())
.filter { it.length > 3 }
.map { NotBlankString(it) }
.flatMap { refine(UserNamePredicate(), it) } |
Looking at this I'd love to have some val Either<NoExact, NotBlankString> =
refine("Krefty") {
notBlank() // refine(NotBlank)
ensure(it.length > 3)
refine(UserNamePredicate)
NotBlankString(it)
} // getOrElse, getOrNull, bind, etc.
|
@nomisRev, that looks awesome! +1 |
I guess this is already close to what we have with interface ExactScope<A> : Raise<ExactError> {
val raw: A
fun refine(refine: FilterMap<A>): A
// ...
}
val NotBlank: FilterMap<String> =
FilterMap { it.ifBlank { null } }
fun ExactScope<String>.notBlank(): String = refine(NotBlank) PS: I don't like the |
Awesome! @nomisRev can you submit a PR with it? I like it a lot! I'm excited to add |
I can do a PR for that this week. |
Thank you @ustits for the awesome idea to drop the entire builder and just use a simple
Raise<E>.f(A) -> R
function!Based on our discussions in #4 , #6 and #7. This is how I imagine the entire Exact Core to look like.
Exact
s andRefined
typesRefined
types likePositiveInt
,NotBlankTrimmedString
, etcThis PR is raised for discussion purposes and if we happen to like the ideas in it - we can merge it and build on top of it.
As always I'm more than open to suggestions and any changes - big or small! 👍
Can't wait to use our small Arrow Exact library in my personal projects! 🚀