Hi folks. Very new to Arrow, fairly new to Kotlin...
# arrow
l
Hi folks. Very new to Arrow, fairly new to Kotlin. I have a tri-state purpose-built Result-ish object, like so:
Copy code
sealed class RefreshResult(val message: String) {
    class Queued(val id: UUID) : RefreshResult("queued")
    class Exists(val id: UUID) : RefreshResult("exists")
    class Rejected(val reason: String) : RefreshResult("rejected")
}
Is there a more Arrow-y tool for such a tri-state case? or utilities that would make sense to borrow from Arrow/Either to make it cleaner? Or would you typically shoehorn that into an Either somehow? (I’m not sure how effectively I can do that given the specific properties involved.)
s
Hey Larry, Welcome to Kotlin (& Arrow) ☺️ Interesting, Quiver has DSL for Outcome. A tri-state type but a concrete monad transformer of Either & Option. It requires a custom DSL, because we don't have context receivers yet with them a lot more is possible! You can just have 2x Raise context in your lambdas for example, which doesn't work with interfaces or classes. However, I am not sure I understand how your type works? Can you share some usage? Or clarify a bit how it's used?
l
I have no idea what context receivers are, so I’ll just nod and smile there…
The logic is roughly this (one moment while I abstract the code sample):
Copy code
class Controller {
  fun refresh(payload: RefreshRequest): ResponseEntity<String> {
       val validatedRequest = payload.toValidated()

        return validatedRequest.fold<ResponseEntity<String>>({
            return ResponseEntity.badRequest().body(it.errorMessage())
        }, {
            return when (val result = someService.refreshEntity(it)) {
                is RefreshResult.Queued ->
                    ResponseEntity
                        .accepted()
                        .body(objectMapper.writeValueAsString(EntityRefreshResponse(result.id, result.message)))

                is RefreshResult.Exists ->
                    ResponseEntity
                        .accepted()
                        .body(objectMapper.writeValueAsString(EntityRefreshResponse(result.id, result.message)))

                is RefreshResult.Rejected ->
                    ResponseEntity
                        .internalServerError()
                        .body(objectMapper.writeValueAsString(EntityRefreshResponse(null, result.message)))
            }
        })
  }
}

class SomeService(request: ValidatedRequest): RefreshResult {
  if (we already got the request) {
    return RefreshResult.Exists(existing id)
  }

  // Save new record
  if (save failed for some reason) {
    return RefreshResult.Rejected(some message)
  }
  return RefreshResult.Queued(newly created id)
}
Which… works, and I don’t think is bad, I just am wondering if it could be better.
(This is literally my first day using Arrow; I heard about it yesterday. 🙂 )
(Going to lunch, bbl. Would love to know if there’s a better way to do what I’m doing.)
s
You have too many returns, and I think it has caused some inference issues. Within
fold
there should no additional returns, those are redundant in lambdas and thus they return the result to the refresh function. Bypassing fold entirely. I would split the code a bit more, and perhaps use
getOrElse
instead of
fold
like this, and the DSL to clean it up a bit more.
Copy code
fun refresh(payload: RefreshRequest): ResponseEntity<String> =
      either {
         val value = payload.toValidated().bind()
         val result = someService.refreshEntity(value)
         result.toResponseEntity()
      }.getOrElse { ResponseEntity.badRequest().body(it.errorMessage()) }

fun RefreshResult.toReponseEntity() =
  when (this) {
                is RefreshResult.Queued ->
                    ResponseEntity
                        .accepted()
                        .body(objectMapper.writeValueAsString(EntityRefreshResponse(id, message)))

                is RefreshResult.Exists ->
                    ResponseEntity
                        .accepted()
                        .body(objectMapper.writeValueAsString(EntityRefreshResponse(id, message)))

                is RefreshResult.Rejected ->
                    ResponseEntity
                        .internalServerError()
                        .body(objectMapper.writeValueAsString(EntityRefreshResponse(null, message)))
}
y
I mean a simple transformation would be something like:
Either<String, Pair<UUID, Bool>>
but I think having your own custom type is fine. I think you likely want to define a DSL like the following to let you use your type much more easily:
Copy code
inline fun refreshResult(block: Raise<String> -> Pair<UUID, Boolean>): RefreshResult = fold(block, RefreshResult::Rejected) { (queued, id) -> if(queued) RefreshResult.Queued(id) else RefreshResult.Exists(id) }
Then you'll be able to
raise("my reason")
when you want to reject a refresh
l
Oh, moving the when clause into a toResponseEntity() sounds nice.
@Youssef Shoaib [MOD] Hm. I still don’t grok Raise. The docs on that seemed thin.
Hm, no, because then I cannot inject ObjectMapper into the extension function. 😞
s
Do you really need to object mapper in Spring though? Don't they do that for you?
Oh, it's because you typed to
ResponseEntity<String>
in cases like this I return
ResponseEntity<*>
.. Not great, but I can just return typed entities and Spring will do the serialisation.
l
🤔
Huh. That worked… surprisingly well. Would it make more sense to make the toResponseEntity() method an extension function or a method of the sealed RefresthResult class? (I think it would work the same either way; I’m just not sure what “best practices” are on such things yet.)
s
Extensions are common, but having seen a lot both for many years I think just doing a method is better. Prefer the simple way, reason being that an extension will require an import in other packages while an instance method doesn't.
l
Hm, good point. Though, is it a layer violation if the result object is part of the service layer, for it to have a method that produces an HTTP response object?
y
I would say for layer separation sake that you make it an extension yes, especially if this result type is gonna be used in multiple layers.
s
It's a pure function.. At least if you use
ResponseEntity<*>
, so imho it's not problematic. What is the benefit of splitting, besides aesthetically?
l
I dunno, that’s why I’m asking. 🙂 This is the first language I’ve used with extension functions, so I don’t have a good muscle memory for them yet.
s
Extensions are great, but I don't use them over what would normally be a method unless I only use it in a single file. Within a single file you can just do a private top level function, and no import is needed. If only used within a single class, put it privately in the class. If splitting into modules or layers, like Youssef mentioned, then extension functions also work great to make types appear as instance methods. You can really not do anything wrong here, just know that an extension method requires an import in another package.
Refactoring from one to the other is 1 refactor away if you own all the code.
l
Got it. I think I’ll go with the extension function for now, for layer cleanliness, since I’m doing that on some other objects here anyway.