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

Impure validation #17

Open
CLOVIS-AI opened this issue May 20, 2023 · 5 comments
Open

Impure validation #17

CLOVIS-AI opened this issue May 20, 2023 · 5 comments

Comments

@CLOVIS-AI
Copy link

Although arrow-exact is originally meant for type refinements, our current DSL can handle any kind of validation.

In my projects, I have two major kinds of validation needs: pure and impure. So far, we have concentrated on pure validation (and I think we're doing a good job of it).

Here's an example of impure validation, that I have seen in the real world (for the curious). We can simplify this example to: the class we want to do validation on is called Reference. It stores an identifier to another business entity called File (here, we don't care what it is). A Reference is only valid if the referenced File exists at the time of instantiation. For this, we need two things:

  • suspend, to make a network request
  • an instance of FileService

Here's an example of how it could look like:

fun interface ImpureExact<out E, A, in C, out R> {
    suspend fun from(value: A, context: C): Either<E, R>
    //
}

// If we just need suspension but no context, we provide a simplified interface
fun interface SuspendExact<out E, A, out R> : ImpureExact<E, A, Unit, R> {
    suspend fun from(value: A): Either<E, R> = from(value, Unit)
    //
}

Usage:

data class Ref private constructor(
    val id: String,
) {
    companion object : ImpureExact<String, String, FileService, Ref> by exact({ it, service ->
        ensure(ExactId)
        
        val file = service.find(it)
        ensureNotNull(file) { "Could not find file $it" }
        
        Ref(it)
    })
}

suspend fun main() {
    val service = FileService(…)
    val id = service.create(…)
    
    Ref.fromOrThrow(id)
}

What do you think?

@ustitc
Copy link
Collaborator

ustitc commented May 21, 2023

Thanks for an example @CLOVIS-AI, haven't thought about such usage before 🤔

There are actually two problems

  1. Factory methods of Exact can't work with multiple input values. So we can't do Ref.fromOrThrow(id, service) and I think we shouldn't allow such usage. If multiple inputs are needed, probably it means that there is a missing abstraction. It also can be the case when Exact factory must be created separately, not in the companion object
  2. With suspend we introduce a notion of time, so the "validity" of value is dependent on the time it was called. So, even after a millisecond from Ref.fromOrThrow(id) call we can't guarantee that the result won't turn intoNonExistingRef. I am not that much into functional programming, but it sounds like IO type solves that kind of a problem

Taking all that into account, I suggest that impure logic can be incapsulated in the type itself (MaybeRef) and Exact factory must be created separately (MaybeRefFactory):

value class Ref private constructor(
  val id: String
) {
  
  companion object : Exact<String, IO<Ref>> by exact({
    ensure(isUUID(raw))
    Ref(it)
  })
}

// IO<Ref>
class MaybeRef(
  private val value: ref: Ref,
  private val predicate: suspend (T) -> Boolean // to validate that id is present at the moment
) {

  suspend fun getOrThrow(): Ref
  suspend fun get(): Either<NonExistingRef, Ref>

}

class MaybeRefFactory(private val service: Service) : Exact<String, MaybeRef> by suspendExact({
    val ref = ensure(Ref)
    MaybeRef(ref) { 
      service.find(raw.id).isPresent()
    }
  })
  
suspend fun main() {
    val service = FileService(…)
    val id = service.create(…)
    
    val maybeRef: MaybeRef = MaybeRefFactory.fromOrThrow(id)
    val ref: Ref = maybeRef.getOrThrow() // will check that id is present, otherwise throws an exception
}

The only thing that is missing now is a suspendExact function, which actually can be quite handy.

In your case maybe an IO type is also needed, but as far as I know Arrow doesn't have it. Probably I can add such logic in krefty because now it sounds to me as a refinement type but with impure suspended predicate. Btw @nomisRev why Arrow doesn't have IO type?

Thanks once again for an example, it gave me plenty of new thoughts of how we can work with types in Exact!

@CLOVIS-AI
Copy link
Author

@ustits Arrow doesn't have IO<E, A> (anymore) because it's the same thing as suspend () -> Either<E, A>, or even better context(Raise<E>) suspend () -> A, which are both handled much better by the compiler.

@CLOVIS-AI
Copy link
Author

Related to both of your notes:

  1. Factory methods of Exact can't work with multiple input values.

Yes, that's why I proposed creating another interface for impure validation. This way, the pure variant keeps its niceness.

  1. With suspend we introduce a notion of time, so the "validity" of value is dependent on the time it was called.

That's true, but there's also no way around it. It's not possible to have a value be continuously validated depending on changes happening on some other machines. But I often know that the lifetime of the Ref value created locally is extremely likely to be less than the remote one, and I'm ok with failing down the line if it has disappeared. It's still important to check as early as possible, even if it's not 100% guaranteed to hold true.

@ustitc
Copy link
Collaborator

ustitc commented May 22, 2023

I see your point, actually you are right, we will need something like SuspendExact with suspend methods, otherwise we won't be able to introduce impure validation from exact context.

But I struggle to get the point of from(value: A, context: C). Isn't it better to pass desired context via class constructor? Why I think so

  1. Probably there would be more complex validations in the wild which would require more than one object. Yes, they can be wrapped in one object, but it will require the clients to make one extra step to use Exact
  2. We will have to hold one more type in Exact. I already don't quite like that we need to define 3 types in ExactEither, having a 4-th parameter, imho, will make the clients frustrated

@CLOVIS-AI
Copy link
Author

To me, having to create an intermediary object is less of an issue than having some Exact-enabled classes for which the Exact instance is not the companion object. I'm interested in what the other members of the project think of this tradeoff.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants