Krefty is a Kotlin library that empowers the creation of domain-specific types while addressing the Primitive Obsession anti-pattern. It provides a robust framework for constructing types inspired by the Refinement Type Theory, where types are composed of a predicate and a value that satisfies it.
Krefty is particularly useful for Domain-Driven Design (DDD) users, where refined types can be viewed as a viable alternative to Value Objects. Inspired by implementations in Haskell and Scala.
Also check out Arrow-Exact and Values4k which solve the same problem.
Add Krefty to your dependencies:
implementation("dev.ustits.krefty:krefty-core:<latest_version>")
For snapshot versions
repositories {
maven("https://s01.oss.sonatype.org/content/repositories/snapshots")
}
implementation("dev.ustits.krefty:krefty-core:<latest_version>-SNAPSHOT")
Let's consider an example where we need a type for describing names. We must ensure that the name is not empty and that we have a specific type for it:
@JvmInline
value class Name private constructor(private val value: String) {
companion object : Refinery<String, Name>() {
override fun Refinement<String>.refine() = filter { it.isNotBlank() }.map { Name(it) }
}
}
We define a Name
class that holds a String
value.
Refinery
serves as a medium to fine-tune the conversion from String to Name. This is done by implementing
refine()
function, which applies filters to check if the string is non-empty.
If the string satisfies this condition, it is then transformed into a Name
instance.
You now have the ability to generate Name instances, for example by using the fromOrThrow
method:
val grog = Name.fromOrThrow("Grog") // Name instance "Grog"
val void = Name.fromOrThrow("") // throws RefinementException
For a simplified version of from
, you can apply specific Refinery
implementations:
NullRefinery
ThrowingRefinery
ResultRefinery
For instance, with ResultRefinery
, the usage would look like this:
companion object : ResultRefinery<String, Name>()
Name.from("Scanlan") // Result.success
Refinement
is a core concept in Krefty. You can think of it as a container for a value and a predicate.
If the value matches the predicate, it holds that value, otherwise, it holds an error.
Refinement
provides familiar operations, like map
and filter
, to validate and transform the refined type:
refinement
.filter { it.isNotBlank() }
.map { NotBlankString(it) }
.flatMap { refine(it, this::isName) }
Refinery
itself can also be used as a transformation:
refinement
.filter(NotBlankString)
.flatMap(Name)
You can also use Refinement
separately from Refinery
, for example by using refine
function:
val name = refine("Krefty") { it.isNotBlank() }
name.getOrThrow() // "Krefty"
name.isRefined() // true
val version = refine("") { it.isNotBlank() }
version.getOrThrow() // throws RefinementException
version.isRefined() // false
For refinements that involve side effects, the suspendRefine
function can be used:
suspendRefine("94926946-2e51-4b14-a9bd-2ce9ad02b29b") {
service.existsById(it)
}
class Service {
suspend fun existsById(id: String): Boolean
}
Krefty can be used with Either
type from Arrow. In order to use it add krefty-arrow
to your dependencies:
implementation("dev.ustits.krefty:krefty-arrow:<latest_version>")
Then you can use EitherRefinery
to get results as an Either
:
Name.from("Keyleth") // Either<RefinementError, Name>