├── assets ├── konad-logo.png └── konad-symbol.png ├── .mvn └── wrapper │ ├── maven-wrapper.jar │ ├── maven-wrapper.properties │ └── MavenWrapperDownloader.java ├── .travis.yml ├── src ├── main │ └── kotlin │ │ └── io │ │ └── konad │ │ ├── exceptions │ │ ├── EitherException.kt │ │ └── ResultException.kt │ │ ├── Error.kt │ │ ├── applicative │ │ ├── builders │ │ │ ├── applicativeBuildersPureStyle.kt │ │ │ └── applicativeBuilders.kt │ │ └── Flatten.kt │ │ ├── hkt │ │ ├── unaryHigherKindedTypes.kt │ │ └── binaryHigherKindedTypes.kt │ │ ├── Result.kt │ │ ├── Validation.kt │ │ ├── Maybe.kt │ │ └── curry.kt └── test │ └── kotlin │ └── io │ └── konad │ ├── usage │ └── examples │ │ ├── model │ │ ├── NameOfAPerson.kt │ │ ├── Password.kt │ │ ├── UserContacts.kt │ │ ├── PhoneNumber.kt │ │ ├── Email.kt │ │ └── User.kt │ │ └── CreateNewUserTests.kt │ ├── FunctionCurryAndApplyTests.kt │ ├── laws │ ├── MaybeRespectesLaws.kt │ ├── ResultRespectesLaws.kt │ ├── ValidationRespectsLaws.kt │ ├── FunctorLaws.kt │ ├── MonadLaws.kt │ └── ApplicativeLaws.kt │ ├── MaybeTests.kt │ ├── generators │ └── generators.kt │ ├── ApplicativeBuildersTestsOnMaybe.kt │ ├── ApplicativeBuildersTests.kt │ ├── ValidationTests.kt │ └── ResultTests.kt ├── .github └── workflows │ ├── build-and-test.yml │ └── maven-central-publish.yml ├── LICENSE ├── .gitignore ├── mvnw.cmd ├── pom.xml ├── mvnw └── README.md /assets/konad-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucapiccinelli/konad/HEAD/assets/konad-logo.png -------------------------------------------------------------------------------- /assets/konad-symbol.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucapiccinelli/konad/HEAD/assets/konad-symbol.png -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lucapiccinelli/konad/HEAD/.mvn/wrapper/maven-wrapper.jar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | 2 | language: java 3 | jdk: 4 | - openjdk11 5 | 6 | cache: 7 | directories: 8 | - $HOME/.m2 9 | 10 | script: 11 | - mvn clean package 12 | -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/exceptions/EitherException.kt: -------------------------------------------------------------------------------- 1 | package io.konad.exceptions 2 | 3 | data class EitherException(val data: Any?) : RuntimeException(data.toString()) 4 | -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/exceptions/ResultException.kt: -------------------------------------------------------------------------------- 1 | package io.konad.exceptions 2 | 3 | import io.konad.Result 4 | 5 | class ResultException(val errors: Result.Errors) : RuntimeException(errors.error.description) -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/usage/examples/model/NameOfAPerson.kt: -------------------------------------------------------------------------------- 1 | package io.konad.usage.examples.model; 2 | 3 | data class NameOfAPerson( 4 | val firstname: String, 5 | val lastname: String){ 6 | 7 | val displayName = "$firstname $lastname" 8 | } -------------------------------------------------------------------------------- /.mvn/wrapper/maven-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.3/apache-maven-3.6.3-bin.zip 2 | wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar 3 | -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/Error.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | data class Error(val description: String, val title: String? = null, val code:Int = 0){ 4 | override fun toString(): String = title?.run { "$this: $description" } ?: description 5 | } 6 | fun Collection.string(separator: String = ",") = joinToString(separator) { it.toString() } -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/applicative/builders/applicativeBuildersPureStyle.kt: -------------------------------------------------------------------------------- 1 | package io.konad.applicative.builders 2 | 3 | import io.konad.hkt.ApplicativeFunctorKind 4 | import io.konad.hkt.FunctorKind 5 | 6 | infix fun ((T) -> R).map(f: FunctorKind): FunctorKind = on(f) 7 | infix fun FunctorKind R)>.ap(f: ApplicativeFunctorKind): ApplicativeFunctorKind = on(f) 8 | infix fun FunctorKind R)>.pure(t: T): FunctorKind = on(t) 9 | infix fun ((T) -> R).apply(t: T): R = on(t) -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/FunctionCurryAndApplyTests.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import io.kotest.core.spec.style.StringSpec 4 | import io.konad.applicative.builders.* 5 | import io.kotest.matchers.shouldBe 6 | 7 | class FunctionCurryAndApplyTests : StringSpec({ 8 | 9 | data class Person(val isCustomer: Boolean, val id: Int, val name: String) 10 | val expected = Person(false, 4, "Foo") 11 | 12 | "currying a person" { 13 | val person = ::Person.curry() on false on 4 on "Foo" 14 | person shouldBe expected 15 | } 16 | 17 | }) -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/usage/examples/model/Password.kt: -------------------------------------------------------------------------------- 1 | package io.konad.usage.examples.model 2 | 3 | import io.konad.Result 4 | import io.konad.error 5 | import io.konad.ok 6 | 7 | data class Password private constructor(val value: String){ 8 | 9 | companion object { 10 | fun of(passwordValue: String): Result = if(passwordValue.length >= 6) 11 | Password(passwordValue).ok() 12 | else "Password should be at least 6 characters length. It was ${passwordValue.length} characters".error() 13 | } 14 | 15 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/usage/examples/model/UserContacts.kt: -------------------------------------------------------------------------------- 1 | package io.konad.usage.examples.model 2 | 3 | import io.konad.applicative.builders.plus 4 | import io.konad.field 5 | import io.konad.plus 6 | 7 | data class UserContacts( 8 | val email: Email? = null, 9 | val phoneNumber: PhoneNumber? = null){ 10 | 11 | companion object{ 12 | fun of(email: String?, phoneNumber: String?) = 13 | ::UserContacts + 14 | Email.of(email).field(UserContacts::email) + 15 | PhoneNumber.of(phoneNumber).field(UserContacts::phoneNumber) 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/hkt/unaryHigherKindedTypes.kt: -------------------------------------------------------------------------------- 1 | package io.konad.hkt 2 | 3 | 4 | interface Kind 5 | 6 | interface FunctorKind: Kind{ 7 | fun mapK(fn: (T) -> R): FunctorKind 8 | } 9 | 10 | interface ApplicativeFunctorKind: FunctorKind{ 11 | fun apMapK(fn: (T) -> R): ApplicativeFunctorKind 12 | fun apK(liftedFn: FunctorKind R>): ApplicativeFunctorKind 13 | } 14 | 15 | interface MonadKind: FunctorKind{ 16 | fun flatMapK(fn: (T) -> MonadKind): MonadKind 17 | } 18 | -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/hkt/binaryHigherKindedTypes.kt: -------------------------------------------------------------------------------- 1 | import io.konad.hkt.ApplicativeFunctorKind 2 | import io.konad.hkt.FunctorKind 3 | import io.konad.hkt.Kind 4 | import io.konad.hkt.MonadKind 5 | 6 | interface Kind2: Kind, B> 7 | interface FunctorKind2: Kind2, FunctorKind, B> 8 | interface ApplicativeFunctorKind2: FunctorKind2, ApplicativeFunctorKind, B> 9 | interface MonadKind2: FunctorKind2, MonadKind, B> 10 | 11 | typealias Kind2_ = Kind, B> 12 | typealias FunctorKind2_ = FunctorKind, B> 13 | -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/usage/examples/model/PhoneNumber.kt: -------------------------------------------------------------------------------- 1 | package io.konad.usage.examples.model 2 | 3 | import io.konad.Result 4 | import io.konad.error 5 | import io.konad.ok 6 | 7 | const val PHONENUMBER_REGEX = "^(\\+\\d{2})?\\s?(\\d\\s?)+\$" 8 | 9 | data class PhoneNumber private constructor(val value: String){ 10 | 11 | companion object { 12 | fun of(phoneNumberValue: String?): Result = phoneNumberValue 13 | ?.let { 14 | if(Regex(PHONENUMBER_REGEX).matches(phoneNumberValue)) 15 | PhoneNumber(phoneNumberValue).ok() 16 | else "$phoneNumberValue should match a valid phone number, but it doesn't".error() 17 | } 18 | ?: Result.Ok(null) 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/applicative/Flatten.kt: -------------------------------------------------------------------------------- 1 | package io.konad.applicative 2 | 3 | import io.konad.applicative.builders.ap 4 | import io.konad.applicative.builders.map 5 | import io.konad.hkt.ApplicativeFunctorKind 6 | 7 | inline fun Collection>.flatten(pureLift: (Collection) -> ApplicativeFunctorKind>): ApplicativeFunctorKind> = 8 | { c: Collection -> { t: T -> c + t } }.let { accumulate -> 9 | fold(pureLift(emptyList())) { acc, f -> accumulate map acc ap f } 10 | } 11 | 12 | inline fun Sequence>.flatten(pureLift: (Sequence) -> ApplicativeFunctorKind>): ApplicativeFunctorKind> = 13 | { c: Sequence -> { t: T -> c + t } }.let { accumulate -> 14 | fold(pureLift(emptySequence())) { acc, f -> accumulate map acc ap f } 15 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/laws/MaybeRespectesLaws.kt: -------------------------------------------------------------------------------- 1 | package io.konad.laws 2 | 3 | import io.konad.Maybe 4 | import io.konad.MaybeOf 5 | import io.konad.generators.functionAToB 6 | import io.konad.generators.maybe 7 | import io.kotest.property.Arb 8 | import io.kotest.property.arbitrary.double 9 | import io.kotest.property.arbitrary.string 10 | 11 | class MaybeFunctorLaws: FunctorLaws(Arb.maybe(Arb.double()), Arb.double()) 12 | class MaybeMonadLaws: MonadLaws( 13 | Maybe.Companion::pure, 14 | Arb.maybe(Arb.double()), 15 | Arb.maybe(Arb.string()), 16 | Arb.double()) 17 | class MaybeApplicativeLaws: ApplicativeLaws( 18 | Maybe.Companion::pure, 19 | Maybe.Companion::pure, 20 | Maybe.Companion::pure, 21 | Maybe.Companion::pure, 22 | Arb.maybe(Arb.double()), 23 | Arb.maybe(Arb.functionAToB(Arb.double())), 24 | Arb.double()) -------------------------------------------------------------------------------- /.github/workflows/build-and-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a Java project with Maven 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven 3 | 4 | name: build-and-test 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | - name: Set up JDK 11 20 | uses: actions/setup-java@v1 21 | with: 22 | java-version: 11 23 | - name: Cache local Maven repository 24 | uses: actions/cache@v2 25 | with: 26 | path: ~/.m2/repository 27 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 28 | restore-keys: | 29 | ${{ runner.os }}-maven- 30 | - name: Build with Maven 31 | run: mvn -B package --file pom.xml 32 | -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/laws/ResultRespectesLaws.kt: -------------------------------------------------------------------------------- 1 | package io.konad.laws 2 | 3 | import io.konad.Result 4 | import io.konad.ResultOf 5 | import io.konad.generators.functionAToB 6 | import io.konad.generators.result 7 | import io.kotest.property.Arb 8 | import io.kotest.property.arbitrary.double 9 | import io.kotest.property.arbitrary.string 10 | 11 | class ResultFunctorLaws: FunctorLaws(Arb.result(Arb.double()), Arb.double()) 12 | class ResultMonadLaws: MonadLaws( 13 | Result.Companion::pure, 14 | Arb.result(Arb.double()), 15 | Arb.result(Arb.string()), 16 | Arb.double()) 17 | class ResultApplicativeLaws: ApplicativeLaws( 18 | Result.Companion::pure, 19 | Result.Companion::pure, 20 | Result.Companion::pure, 21 | Result.Companion::pure, 22 | Arb.result(Arb.double()), 23 | Arb.result(Arb.functionAToB(Arb.double())), 24 | Arb.double()) -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/MaybeTests.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import io.konad.applicative.builders.plus 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.matchers.shouldBe 6 | 7 | class MaybeTests : StringSpec({ 8 | 9 | "with maybe I can compose nullables without any explicit wrapping" { 10 | 11 | val x: Int? = 1 12 | val y: Float? = 2.0f 13 | val z: String? = "3" 14 | 15 | ::testFn + x + y + z shouldBe 6 16 | ::testFn2 + x + y + z shouldBe 6 17 | } 18 | 19 | "with maybe I can compose nullables without any explicit wrapping also in case of null" { 20 | 21 | val x: Int = 1 22 | val y: Float? = null 23 | val z: String? = "3" 24 | 25 | ::testFn + x + y + z shouldBe null 26 | } 27 | 28 | }) 29 | 30 | fun testFn(a: Int, b: Float, c: String): Float = a + b + c.toInt() 31 | fun testFn2(a: Int?, b: Float, c: String): Float = a!! + b + c.toInt() -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/generators/generators.kt: -------------------------------------------------------------------------------- 1 | package io.konad.generators 2 | 3 | import io.konad.* 4 | import io.konad.Maybe.Companion.maybe 5 | import io.kotest.property.Arb 6 | import io.kotest.property.arbitrary.* 7 | 8 | fun Arb.Companion.result(okGen: Arb): Arb> = 9 | arbitrary { rs -> rs.random.nextDouble(1.0).run { if(this > 0.5) 10 | okGen.next(rs).ok() 11 | else Arb.string().next(rs).error() 12 | }} 13 | 14 | fun Arb.Companion.maybe(valueGen: Arb): Arb> = 15 | valueGen.orNull().map { it.maybe } 16 | 17 | fun Arb.Companion.validation(failGen: Arb, successGen: Arb): Arb> = 18 | arbitrary { rs -> rs.random.nextDouble(1.0).run { if(this > 0.5) 19 | successGen.next(rs).success() 20 | else failGen.next(rs).fail() 21 | }} 22 | 23 | fun Arb.Companion.functionAToB(genB: Arb): Arb<(A) -> B> = genB.map{ b -> { b } } 24 | -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/usage/examples/model/Email.kt: -------------------------------------------------------------------------------- 1 | package io.konad.usage.examples.model 2 | import io.konad.Result 3 | import io.konad.error 4 | import io.konad.ok 5 | 6 | const val EMAIL_REGEX = "(?:[a-z0-9!#\$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#\$%&'*+/=?^_`{|}~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9]))\\.){3}(?:(2(5[0-5]|[0-4][0-9])|1[0-9][0-9]|[1-9]?[0-9])|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21-\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\])" 7 | 8 | data class Email private constructor (val value: String) { 9 | companion object{ 10 | fun of(value: String?): Result = value?.let { 11 | if (Regex(EMAIL_REGEX).matches(value)) 12 | Email(value).ok() 13 | else "$value doesn't match an email format".error() 14 | } ?: Result.Ok(null) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/usage/examples/model/User.kt: -------------------------------------------------------------------------------- 1 | package io.konad.usage.examples.model 2 | 3 | import io.konad.Result 4 | import io.konad.applicative.builders.plus 5 | import io.konad.ifNull 6 | import io.konad.ok 7 | import io.konad.plus 8 | 9 | data class User( 10 | val username: String, 11 | val name: NameOfAPerson, 12 | val password: Password, 13 | val contacts: UserContacts?, 14 | val jobDescription: String 15 | ) { 16 | companion object { 17 | fun of( 18 | username: String, 19 | firstname: String, 20 | lastname: String, 21 | password: String, 22 | email: String? = null, 23 | phoneNumber: String? = null, 24 | jobDescription: String? = null 25 | ): Result = ::User + 26 | username + 27 | NameOfAPerson(firstname, lastname).ok() + 28 | Password.of(password) + 29 | (::UserContacts + Email.of(email) + PhoneNumber.of(phoneNumber)) + 30 | jobDescription.ifNull("job description should not be null") 31 | } 32 | } -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/ApplicativeBuildersTestsOnMaybe.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import io.konad.Maybe.Companion.maybe 4 | import io.konad.Maybe.Companion.nullable 5 | import io.konad.applicative.builders.on 6 | import io.kotest.core.spec.style.StringSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | class ApplicativeBuildersTestsOnMaybe : StringSpec({ 10 | 11 | data class Person(val isCustomer: Boolean, val id: Int, val name: String) 12 | val expected = Person(false, 4, "Foo") 13 | 14 | "Build a Result of a Person from intermediate results. Example of Result composition" { 15 | val person: Person? = ::Person.curry() 16 | .on(false.maybe) 17 | .on(4.maybe) 18 | .on( "Foo") 19 | .nullable 20 | 21 | person shouldBe expected 22 | } 23 | 24 | "When building a Result of a Person from any null value, then person should be null" { 25 | val person: Person? = ::Person.curry() 26 | .on(false.maybe) 27 | .on(null.maybe) 28 | .on( "Foo") 29 | .nullable 30 | 31 | person shouldBe null 32 | } 33 | 34 | }) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Luca Piccinelli 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/laws/ValidationRespectsLaws.kt: -------------------------------------------------------------------------------- 1 | package io.konad.laws 2 | 3 | import io.konad.Validation 4 | import io.konad.ValidationOf 5 | import io.konad.generators.functionAToB 6 | import io.konad.generators.validation 7 | import io.konad.hkt.Kind 8 | import io.kotest.property.Arb 9 | import io.kotest.property.arbitrary.double 10 | import io.kotest.property.arbitrary.int 11 | import io.kotest.property.arbitrary.string 12 | 13 | class ValidationFunctorLaws: FunctorLaws, Double, Double>( 14 | Arb.validation(Arb.int(), Arb.double()), 15 | Arb.double()) 16 | class ValidationMonadLaws: MonadLaws, Double, String>( 17 | Validation.Companion::pure, 18 | Arb.validation(Arb.int(), Arb.double()), 19 | Arb.validation(Arb.int(), Arb.string()), 20 | Arb.double()) 21 | class ValidationApplicativeLaws: ApplicativeLaws, Double>( 22 | Validation.Companion::pure, 23 | Validation.Companion::pure, 24 | Validation.Companion::pure, 25 | Validation.Companion::pure, 26 | Arb.validation(Arb.int(), Arb.double()), 27 | Arb.validation(Arb.int(), Arb.functionAToB(Arb.double())), 28 | Arb.double()) -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .idea/shelf 3 | /android.tests.dependencies 4 | /confluence/target 5 | /dependencies/repo 6 | /dist 7 | /local 8 | /gh-pages 9 | /ideaSDK 10 | /clionSDK 11 | /android-studio/sdk 12 | out/ 13 | /tmp 14 | workspace.xml 15 | *.versionsBackup 16 | /idea/testData/debugger/tinyApp/classes* 17 | /jps-plugin/testData/kannotator 18 | /js/js.translator/testData/out/ 19 | /js/js.translator/testData/out-min/ 20 | .gradle/ 21 | build/ 22 | !**/src/**/build 23 | !**/test/**/build 24 | *.iml 25 | !**/testData/**/*.iml 26 | .idea/libraries/Gradle*.xml 27 | .idea/libraries/Maven*.xml 28 | .idea/artifacts/PILL_*.xml 29 | .idea/modules 30 | .idea/runConfigurations/JPS_*.xml 31 | .idea/runConfigurations/PILL_*.xml 32 | .idea/libraries 33 | .idea/modules.xml 34 | .idea/gradle.xml 35 | .idea/compiler.xml 36 | .idea/inspectionProfiles/profiles_settings.xml 37 | .idea/.name 38 | .idea/artifacts/dist_auto_* 39 | kotlin-ultimate/ 40 | node_modules/ 41 | .rpt2_cache/ 42 | libraries/tools/kotlin-test-nodejs-runner/lib/target 43 | target/test-classes 44 | target 45 | target/classes target/KtBowling-1.0-SNAPSHOT.jar target/maven-archiver target/maven-status target/surefire-reports target/test-classes 46 | .idea 47 | .keys 48 | settings.xml 49 | -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/laws/FunctorLaws.kt: -------------------------------------------------------------------------------- 1 | package io.konad.laws 2 | 3 | import io.konad.hkt.FunctorKind 4 | import io.kotest.core.spec.style.StringSpec 5 | import io.kotest.matchers.shouldBe 6 | import io.kotest.property.checkAll 7 | 8 | import io.konad.* 9 | import io.konad.generators.functionAToB 10 | import io.kotest.property.Arb 11 | import io.kotest.property.arbitrary.bind 12 | import io.kotest.property.arbitrary.double 13 | import io.kotest.property.arbitrary.map 14 | 15 | abstract class FunctorLaws( 16 | private val functorGen: Arb>, 17 | private val tranformedValueGen: Arb 18 | ): StringSpec({ 19 | 20 | 21 | "first functor law (identity)"{ 22 | val id: (T) -> T = { x: T -> x } 23 | checkAll(functorGen) { functor -> functor.mapK(id) shouldBe functor } 24 | } 25 | 26 | "second functor law (composition)"{ 27 | Arb.bind( 28 | functorGen, 29 | Arb.functionAToB(tranformedValueGen), 30 | Arb.functionAToB(tranformedValueGen) ){ functor, f, g -> Triple(functor, f, g) } 31 | 32 | .checkAll { (functor, f, g) -> 33 | functor.mapK(f).mapK(g) shouldBe functor.mapK { v -> g(f(v)) } 34 | } 35 | 36 | } 37 | 38 | }) -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/laws/MonadLaws.kt: -------------------------------------------------------------------------------- 1 | package io.konad.laws 2 | 3 | import io.konad.generators.functionAToB 4 | import io.konad.hkt.MonadKind 5 | import io.kotest.core.spec.style.StringSpec 6 | import io.kotest.matchers.shouldBe 7 | import io.kotest.property.Arb 8 | import io.kotest.property.arbitrary.bind 9 | import io.kotest.property.checkAll 10 | 11 | abstract class MonadLaws( 12 | private val pure: (T) -> MonadKind, 13 | private val monadGen1: Arb>, 14 | private val monadGen2: Arb>, 15 | private val valueGen1: Arb, 16 | ): StringSpec({ 17 | 18 | "first monad law (left identity)" { 19 | Arb.bind(Arb.functionAToB>(monadGen1), valueGen1){ f, v -> f to v} 20 | .checkAll { (f, v) -> 21 | pure(v).flatMapK(f) shouldBe f(v) 22 | } 23 | } 24 | 25 | "second monad law (right identity)"{ 26 | checkAll(monadGen1){ m -> 27 | m.flatMapK { pure(it) } shouldBe m 28 | } 29 | } 30 | 31 | "third monad law (associativity)"{ 32 | Arb.bind( 33 | Arb.functionAToB>(monadGen1), 34 | Arb.functionAToB>(monadGen2), 35 | valueGen1){ f, g, v -> Triple(f , g, v) } 36 | .checkAll { (f, g, v) -> 37 | 38 | pure(v).flatMapK(f).flatMapK(g) shouldBe pure(v).flatMapK { x -> f(x).flatMapK(g) } 39 | 40 | } 41 | } 42 | 43 | }) -------------------------------------------------------------------------------- /.github/workflows/maven-central-publish.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a package using Maven and then publish it to GitHub packages when a release is created 2 | # For more information see: https://github.com/actions/setup-java#apache-maven-with-a-settings-path 3 | 4 | name: maven-central-publish 5 | 6 | on: 7 | release: 8 | types: [created] 9 | workflow_dispatch: 10 | inputs: 11 | logLevel: 12 | description: 'Log level' 13 | required: true 14 | default: 'warning' 15 | 16 | jobs: 17 | build: 18 | 19 | runs-on: ubuntu-latest 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Set up JDK 11 24 | uses: actions/setup-java@v1 25 | with: 26 | java-version: 11 27 | server-id: ossrh 28 | server-username: MAVEN_USERNAME 29 | server-password: MAVEN_PASSWORD 30 | 31 | - id: install-secret-key 32 | name: Install gpg secret key 33 | run: | 34 | cat <(echo -e "${{ secrets.OSSRH_GPG_SECRET_KEY }}") | base64 --decode | gpg --batch --import 35 | 36 | - name: Cache local Maven repository 37 | uses: actions/cache@v2 38 | with: 39 | path: ~/.m2/repository 40 | key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-maven- 43 | 44 | - name: Publish package to maven central repository 45 | run: | 46 | export GPG_TTY=$(tty) 47 | mvn -B clean deploy -Dgpg.passphrase=${{ secrets.OSSRH_GPG_SECRET_KEY_PASSWORD }} -P publish 48 | env: 49 | MAVEN_USERNAME: ${{ secrets.OSSRH_USERNAME }} 50 | MAVEN_PASSWORD: ${{ secrets.OSSRH_TOKEN }} 51 | -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/laws/ApplicativeLaws.kt: -------------------------------------------------------------------------------- 1 | package io.konad.laws 2 | 3 | import io.konad.generators.functionAToB 4 | import io.konad.hkt.ApplicativeFunctorKind 5 | import io.kotest.core.spec.style.StringSpec 6 | import io.kotest.matchers.shouldBe 7 | import io.kotest.property.Arb 8 | import io.kotest.property.arbitrary.bind 9 | import io.kotest.property.checkAll 10 | 11 | abstract class ApplicativeLaws( 12 | private val pureT: (T) -> ApplicativeFunctorKind, 13 | private val pureF: ((T) -> T) -> ApplicativeFunctorKind T>, 14 | private val pureInterchange: (((T) -> T) -> T) -> ApplicativeFunctorKind T) -> T>, 15 | private val pureComposition: (((T) -> T) -> ((T) -> T) -> (T) -> T) -> ApplicativeFunctorKind T) -> ((T) -> T) -> (T) -> T>, 16 | private val applicativeGen1: Arb>, 17 | private val applicativeGenF: Arb T>>, 18 | private val valueGen1: Arb 19 | ): StringSpec({ 20 | 21 | "first applicative law (identity)" { 22 | val id: (T) -> T = { x: T -> x } 23 | checkAll(applicativeGen1) { applicative -> applicative.apK(pureF(id)) shouldBe applicative } 24 | } 25 | 26 | "second applicative law (Homomorphism)" { 27 | Arb.bind(valueGen1, Arb.functionAToB(valueGen1)){ v, f -> v to f } 28 | .checkAll { (v, f) -> 29 | pureT(v).apK(pureF(f)) shouldBe pureT(f(v)) 30 | } 31 | } 32 | 33 | "third applicative law (Interchange)" { 34 | Arb.bind(valueGen1, applicativeGenF){ v, applicativeF -> v to applicativeF } 35 | .checkAll { (v, applicativeF) -> 36 | 37 | pureT(v).apK(applicativeF) shouldBe applicativeF.apK(pureInterchange { f2: (T) -> T -> f2(v) }) 38 | } 39 | } 40 | 41 | "fourth applicative law (Composition)" { 42 | val compose = { f: (T) -> T -> { g: (T) -> T -> { a: T -> f(g(a)) } } } 43 | Arb.bind( 44 | applicativeGen1, 45 | Arb.functionAToB(valueGen1), 46 | Arb.functionAToB(valueGen1) 47 | ){ applicative, f, g -> Triple(applicative , f, g)} 48 | .checkAll { (applicative, f, g) -> 49 | val u = pureF(g) 50 | val v = pureF(f) 51 | 52 | applicative.apK(v.apK(u.apK(pureComposition(compose)))) shouldBe applicative.apK(v).apK(u) 53 | } 54 | } 55 | 56 | }) -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/ApplicativeBuildersTests.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import io.konad.applicative.builders.on 4 | import io.konad.applicative.builders.plus 5 | import io.konad.exceptions.ResultException 6 | import io.kotest.assertions.throwables.shouldThrow 7 | import io.kotest.core.spec.style.StringSpec 8 | import io.kotest.matchers.shouldBe 9 | 10 | class ApplicativeBuildersTests : StringSpec({ 11 | 12 | data class Person(val isCustomer: Boolean, val id: Int, val name: String) 13 | val expected = Person(false, 4, "Foo") 14 | 15 | "Build a Result of a Person from intermediate results. Example of Result composition" { 16 | val person: Result = ::Person + 17 | Result.pure(false) + 18 | Result.pure(4) + 19 | "Foo" 20 | 21 | person.get() shouldBe expected 22 | } 23 | 24 | "When building a Result of a Person from intermediate error results, then the errors get accumulated" { 25 | val error1 = Result.Errors("banana") 26 | val error2 = Result.Errors("apple") 27 | val error3 = Result.Errors("pear") 28 | 29 | val person: Result = ::Person + error1 + error2 + error3 30 | 31 | val ex = shouldThrow { person.get() } 32 | ex.errors.toList() shouldBe listOf(error1.error, error2.error, error3.error) 33 | } 34 | 35 | "When building a Result of a Person from intermediate error results and Ok result, then the errors get accumulated as expected" { 36 | val error1 = Result.Errors("banana") 37 | val value = Result.Ok(1) 38 | val error3 = Result.Errors("pear") 39 | 40 | val person: Result = ::Person.curry() 41 | .on(error1) 42 | .on(value) 43 | .on(error3) 44 | .result 45 | 46 | val ex = shouldThrow { person.get() } 47 | ex.errors.toList() shouldBe listOf(error1.error, error3.error) 48 | } 49 | 50 | "nested errors with a title should be formatted as expected" { 51 | val error1 = Result.Errors("banana").errorTitle("fruit1") 52 | val value = Result.Ok(1) 53 | val error3 = Result.Errors("pear").errorTitle("fruit2") 54 | 55 | val person: Result = (::Person + error1 + value + error3) 56 | .errorTitle("Person") 57 | 58 | person.ifError { it.description(", ") } shouldBe "Person: fruit1: banana, Person: fruit2: pear" 59 | } 60 | 61 | "When i have a collection of results I want to flatten it as a result of a collection" { 62 | 63 | listOf(Result.Ok(1), Result.Ok(2)).flatten() shouldBe Result.Ok(listOf(1, 2)) 64 | 65 | } 66 | 67 | "When i have a collection of results with more than one error I want to have the complete list of errors" { 68 | 69 | val ex = shouldThrow { 70 | listOf(Result.Errors("x"), Result.Ok(1), Result.Errors("y")) 71 | .flatten() 72 | .get() 73 | } 74 | 75 | ex.errors.description shouldBe "x - y" 76 | } 77 | 78 | "GIVEN a collection of nullables WHEN any is null THEN the flattening is null as well" { 79 | val listOfNullables: Collection = setOf("", null, "") 80 | 81 | val flattened: Collection? = listOfNullables.flatten() 82 | 83 | flattened shouldBe null 84 | } 85 | 86 | "GIVEN a collection of nullables WHEN no element is null THEN the flattening should success" { 87 | val listOfNullables: Set = setOf("a", "b", "c") 88 | 89 | val flattened: Collection? = listOfNullables.flatten() 90 | 91 | val expectedSet: Set? = setOf("a", "b", "c") 92 | flattened shouldBe expectedSet 93 | } 94 | 95 | }) -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/Result.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import io.konad.Maybe.Companion.maybe 4 | import io.konad.applicative.flatten 5 | import io.konad.exceptions.ResultException 6 | import io.konad.hkt.ApplicativeFunctorKind 7 | import io.konad.hkt.FunctorKind 8 | import io.konad.hkt.Kind 9 | import io.konad.hkt.MonadKind 10 | import io.konad.applicative.builders.on 11 | import kotlin.reflect.KProperty1 12 | 13 | sealed class Result: ApplicativeFunctorKind, MonadKind{ 14 | 15 | data class Ok(val value: T): Result() 16 | data class Errors(val error: Error, val prev: Errors? = null): Result(){ 17 | 18 | constructor(description: String) : this(Error(description)) 19 | 20 | val description = description(" - ") 21 | 22 | fun description(errorDescriptionsSeparator: String = ",") = toList().string(errorDescriptionsSeparator) 23 | 24 | fun toList(): Collection = (prev 25 | ?.run { toList() } 26 | ?: emptyList()) + 27 | listOf(error) 28 | } 29 | 30 | companion object{ 31 | fun pure(value: T): Result = value.ok() 32 | } 33 | 34 | inline fun map(fn: (T) -> R): Result = flatMap { Ok(fn(it)) } 35 | 36 | inline fun flatMap(fn: (T) -> Result): Result = when(this){ 37 | is Ok -> fn(this.value) 38 | is Errors -> this 39 | } 40 | 41 | fun ap(liftedFn: Result<(T) -> R>): Result = when(liftedFn){ 42 | is Ok -> map(liftedFn.value) 43 | is Errors -> when(this){ 44 | is Ok -> liftedFn 45 | is Errors -> Errors(error, prev?.let { Errors(it.error, liftedFn)} ?: liftedFn) 46 | } 47 | } 48 | 49 | inline fun fold(onOk: (T) -> R, onErrors: (Errors) -> R): R = 50 | when (this) { 51 | is Ok -> onOk(this.value) 52 | is Errors -> onErrors(this) 53 | } 54 | 55 | fun get(): T = when(this){ 56 | is Ok -> value 57 | is Errors -> throw ResultException(this) 58 | } 59 | 60 | fun toMaybe(): ApplicativeFunctorKind = toNullable().maybe 61 | 62 | fun toNullable(): T? = when(this){ 63 | is Ok -> value 64 | is Errors -> null 65 | } 66 | 67 | fun errorTitle(title: String, separator: String = ": "): Result = when(this) { 68 | is Ok -> this 69 | is Errors -> copy( 70 | error = error.copy(title = (error.title?.run { "$title$separator${error.title}" } ?: title)), 71 | prev = prev?.run { errorTitle(title, separator) as Errors }) 72 | } 73 | 74 | override fun mapK(fn: (T) -> R): FunctorKind = map(fn) 75 | override fun apMapK(fn: (T) -> R): ApplicativeFunctorKind = map(fn) 76 | override fun flatMapK(fn: (T) -> MonadKind): MonadKind = flatMap { fn(it).result } 77 | override fun apK(liftedFn: FunctorKind R>): ApplicativeFunctorKind = ap(liftedFn.result) 78 | } 79 | 80 | 81 | 82 | inline fun Result

.field(property: KProperty1): Result

= 83 | errorTitle(property.name, separator = ".") 84 | 85 | fun Result.ifError(defaultValue: T) = ifError { defaultValue } 86 | 87 | inline fun Result.ifError(errorHandler: (Result.Errors) -> T) = when(this){ 88 | is Result.Ok -> value 89 | is Result.Errors -> errorHandler(this) 90 | } 91 | 92 | fun T?.ifNull(errorMessage: String, errorCode: Int = 0) = this 93 | ?.run { Result.Ok(this) } 94 | ?: Result.Errors(Error(errorMessage, code = errorCode)) 95 | 96 | open class ResultOf 97 | val Kind.result 98 | get() = this as Result 99 | 100 | fun Collection>.flatten(): Result> = 101 | flatten(Result.Companion::pure) 102 | .result 103 | 104 | fun T.ok(): Result.Ok = Result.Ok(this) 105 | fun String.error(): Result.Errors = Result.Errors(this) 106 | 107 | infix operator fun ((T) -> R).plus(t: Result): Result = on(t).result 108 | infix operator fun ApplicativeFunctorKind R)>.plus(t: Result): Result = on(t).result 109 | infix operator fun FunctorKind R)>.plus(t: Result): Result = on(t).result 110 | infix operator fun FunctorKind R)>.plus(t: T): Result = on(t).result -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/ValidationTests.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import io.konad.applicative.builders.plus 4 | import io.konad.exceptions.EitherException 5 | import io.kotest.assertions.throwables.shouldThrow 6 | import io.kotest.core.spec.style.StringSpec 7 | import io.kotest.matchers.shouldBe 8 | 9 | class ValidationTests : StringSpec({ 10 | 11 | "fail case should throw"{ 12 | shouldThrow { Validation.Fail("ciao").get() } 13 | } 14 | 15 | "success case should return the value"{ 16 | Validation.Success("ciao").get() shouldBe "ciao" 17 | } 18 | 19 | "If the result is success, when i get the result or default, then it returns the value"{ 20 | Validation.Success(1).ifFail(2) shouldBe 1 21 | } 22 | 23 | "If the result is OK, when i get the result or default, then it returns the default"{ 24 | Validation.Fail("booom").ifFail(2) shouldBe 2 25 | } 26 | 27 | "mapping the success case should execute the mapping function"{ 28 | Validation.Success(1) 29 | .map { it.toString() } 30 | .get() shouldBe "1" 31 | } 32 | 33 | "flatMapping the success case should execute the mapping function"{ 34 | Validation.Success(1) 35 | .flatMap { Validation.Success(it.toString()) } 36 | .ifFail(2) shouldBe "1" 37 | } 38 | 39 | "flatMapping the fail case should not the mapping function"{ 40 | val success: Validation = Validation.Success(1) 41 | success 42 | .flatMap { Validation.Fail("booom") } 43 | .ifFail(2) shouldBe 2 44 | } 45 | 46 | "flatMapping starting from a fail case returns fail"{ 47 | val success: Validation = Validation.Fail("booom") 48 | success 49 | .flatMap { Validation.Success(1) } 50 | .ifFail(2) shouldBe 2 51 | } 52 | 53 | "flatMapping starting from a fail short-circuits on the first fail"{ 54 | val success: Validation = Validation.Fail("booom 1") 55 | success 56 | .flatMap { Validation.Fail("booom 2") } 57 | .ifFail { it.first() } shouldBe "booom 1" 58 | } 59 | 60 | "mapping the fail case, must not tranform the content"{ 61 | Validation.Fail("booom").map { it } shouldBe Validation.Fail("booom") 62 | } 63 | 64 | "A lifted function can be applied" { 65 | val liftedFn: Validation Int> = Validation.pure { x: Int -> x + 1 } 66 | val y: Validation = Validation.pure(1).ap(liftedFn) 67 | 68 | y.get() shouldBe 2 69 | } 70 | 71 | "validation fail gets accumulated"{ 72 | val x: Validation = TestError.fail() 73 | val fnError: Validation Int> = TestError.fail() 74 | val result = x.ap(fnError) 75 | 76 | result.ifFail { it.size } shouldBe 2 77 | } 78 | 79 | "validation can be composed"{ 80 | val f: (Int, String, Double) -> String = { _, _, _ -> "ciao"} 81 | val out: Validation = f + "error".fail() + "".success() + 0.0 82 | 83 | out.ifFail { it.first() } shouldBe "error" 84 | } 85 | 86 | "validation can be flattened"(){ 87 | val l: List> = listOf(0.success(), TestError.fail(), 1.success()) 88 | 89 | l.flatten() shouldBe TestError.fail() 90 | } 91 | 92 | "validation can map fail value"{ 93 | 1.fail().mapFail { it + 1 }.ifFail { it.first().toString() } shouldBe "2" 94 | } 95 | 96 | "validation map fail return all the cumulated errors"{ 97 | val fn: (Int, Int, Int) -> List = { x, y, z -> listOf(x, y, z) } 98 | val result: Validation> = fn + 1.fail() + 2.fail() + 3.fail() 99 | 100 | result.mapFail { it * 10 }.ifFail { it.toList() } shouldBe listOf(10, 20, 30) 101 | } 102 | 103 | "validation can map all failed values"{ 104 | 1.fail().mapAllFailures { MappedError("mapped", it.toList()) }.ifFail { it.first() } shouldBe MappedError("mapped", listOf(1)) 105 | } 106 | 107 | "mapping the failers of a success, return the success value"{ 108 | 1.success().mapAllFailures { MappedError("mapped", it.toList()) }.ifFail { it.first() } shouldBe 1 109 | } 110 | 111 | "mapping the fail of a success, return the success value"{ 112 | 1.success().mapFail { it }.ifFail { it } shouldBe 1 113 | } 114 | 115 | }) 116 | 117 | internal object TestError 118 | internal data class MappedError(val title: String, val errors: Collection) -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/ResultTests.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import io.konad.exceptions.ResultException 4 | import io.kotest.assertions.throwables.shouldThrow 5 | import io.kotest.core.spec.style.StringSpec 6 | import io.kotest.matchers.shouldBe 7 | 8 | class ResultTests: StringSpec({ 9 | 10 | "If the result is OK, when i get the result, then it returns the value" { 11 | val x: Result = Result.Ok(1) 12 | x.get() shouldBe 1 13 | } 14 | 15 | "If the result is Error, when i get the result, then it throws an Error" { 16 | val x: Result = Result.Errors("error") 17 | shouldThrow { x.get() } 18 | } 19 | 20 | "If the result is OK, when i get the result or default, then it returns the value" { 21 | val x: Result = Result.Ok(1) 22 | x.ifError(2) shouldBe 1 23 | } 24 | 25 | "If the result is Error, when i get the result or default, then it returns the default" { 26 | val x: Result = Result.Errors("bla") 27 | x.ifError(2) shouldBe 2 28 | } 29 | 30 | "Mapping an Ok result, when i get the result, i get the mapped result " { 31 | val x: Result = Result.Ok(1).map { x -> x + 1 } 32 | x.get() shouldBe 2 33 | } 34 | 35 | "Mapping an Error result, when i get the result, then it throws an Error" { 36 | val x: Result = Result.Errors("error").map { x: Int -> x + 1 } 37 | shouldThrow { x.get() } 38 | } 39 | 40 | "FlatMapping an Ok result, when i get the result, i get the mapped result " { 41 | val x: Result = Result.Ok(1).flatMap { x -> Result.Ok(x + 1) } 42 | x.get() shouldBe 2 43 | } 44 | 45 | "FlatMapping an Error result, when i get the result, then it throws an Error" { 46 | val x: Result = Result.Errors("error").flatMap { x: Int -> Result.Ok(x + 1) } 47 | shouldThrow { x.get() } 48 | } 49 | 50 | "FlatMapping an Ok result with a function that returns an error, when i get the result, then it throws an Error" { 51 | val x: Result = Result.Ok(1).flatMap { Result.Errors("banana") } 52 | val ex = shouldThrow { x.get() } 53 | ex.errors.error.description shouldBe "banana" 54 | } 55 | 56 | "FlatMapping an Error result with a function that returns an error, when i get the result, then it throws the first Error" { 57 | val x: Result = Result.Errors("error1").flatMap { Result.Errors("banana") } 58 | val ex = shouldThrow { x.get() } 59 | ex.errors.error.description shouldBe "error1" 60 | } 61 | 62 | "A lifted function can be applied" { 63 | val liftedFn = Result.pure { x: Int -> x + 1 } 64 | val y: Result = Result.pure(1).ap(liftedFn) 65 | 66 | y.get() shouldBe 2 67 | } 68 | 69 | "Applying a function in error status to an Error result, the errors gets accumulated" { 70 | val liftedFn: Result<(Int) -> Int> = Result.Errors("fn error") 71 | val y: Result = Result.Errors("value error").ap(liftedFn) 72 | 73 | y.ifError { errors -> errors.toList().joinToString(",") { it.description } } shouldBe "fn error,value error" 74 | } 75 | 76 | "null to Result: value should convert to Ok"{ 77 | 1.ifNull("error") shouldBe Result.Ok(1) 78 | } 79 | 80 | "null to Result: value should convert to null"{ 81 | null.ifNull("error") shouldBe Result.Errors("error") 82 | } 83 | 84 | "Can use ok extension method to create a new Result.Ok" { 85 | 1.ok() shouldBe Result.Ok(1) 86 | } 87 | 88 | "Can use error extension method to create a new Result.Errors" { 89 | "booom".error() shouldBe Result.Errors("booom") 90 | } 91 | 92 | "Result.Ok folds first" { 93 | 1.ok().fold({ it + 1 }, { it.toList().size }) shouldBe 2 94 | } 95 | 96 | "Result.Errors folds as second" { 97 | "booom".error().fold({ "ok" }, { it.description }) shouldBe "booom" 98 | } 99 | 100 | "can use description() with a separator to obtain the error descriptions of multiple Result.Errors" { 101 | val errors = Result.Errors(Error("z"), Result.Errors(Error("y"), Result.Errors(Error("x")))) 102 | 103 | errors.description(errorDescriptionsSeparator = ",") shouldBe "x,y,z" 104 | } 105 | 106 | "can enrich the error description prefixing it with a title" { 107 | val errors: Result = Result.Errors(Error("y"), Result.Errors(Error("x"))) 108 | val newError = errors.errorTitle("banana") 109 | 110 | (newError as Result.Errors).description(errorDescriptionsSeparator = ",") shouldBe "banana: x,banana: y" 111 | } 112 | }) -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/Validation.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import ApplicativeFunctorKind2 4 | import Kind2_ 5 | import MonadKind2 6 | import io.konad.applicative.flatten 7 | import io.konad.exceptions.EitherException 8 | import io.konad.hkt.ApplicativeFunctorKind 9 | import io.konad.hkt.FunctorKind 10 | import io.konad.hkt.Kind 11 | import io.konad.hkt.MonadKind 12 | import io.konad.applicative.builders.on 13 | 14 | open class ValidationOf 15 | 16 | @Deprecated("favour the use of the property version", ReplaceWith("Kind, B>.validation")) 17 | fun Kind2_.validation() = 18 | this as Validation 19 | 20 | val Kind2_.validation 21 | get() = this as Validation 22 | 23 | sealed class Validation: MonadKind2, ApplicativeFunctorKind2 { 24 | data class Success(val success: B): Validation() 25 | data class Fail(val fail: A, val prev: Fail? = null): Validation(){ 26 | 27 | fun failures(): Collection = (prev 28 | ?.run { failures() } 29 | ?: emptyList()) + 30 | listOf(fail) 31 | 32 | fun transform(fn: (A) -> D): Fail = 33 | Fail(fail = fn(fail), prev = prev?.transform(fn)) 34 | } 35 | 36 | companion object{ 37 | fun pure(value: B): Validation = Success(value) 38 | } 39 | 40 | fun get(): B = when(this){ 41 | is Fail -> throw EitherException(fail) 42 | is Success -> success 43 | } 44 | 45 | inline fun map(fn: (B) -> C): Validation = 46 | flatMap { Success(fn(it)) } 47 | 48 | fun mapFail(fn: (A) -> D): Validation = when(this){ 49 | is Success -> Success(success) 50 | is Fail -> transform(fn) 51 | } 52 | 53 | inline fun mapAllFailures(fn: (Collection) -> D): Validation = when(this){ 54 | is Success -> Success(success) 55 | is Fail -> Fail(fn(failures())) 56 | } 57 | 58 | 59 | inline fun flatMap(fn: (B) -> Validation): Validation = when(this){ 60 | is Success -> fn(success) 61 | is Fail -> this 62 | } 63 | 64 | fun ap(liftedFn: Validation C>): Validation = when(liftedFn){ 65 | is Success -> map(liftedFn.success) 66 | is Fail -> when(this){ 67 | is Success -> liftedFn 68 | is Fail -> Fail(fail, prev?.let { Fail(it.fail, liftedFn) } ?: liftedFn) 69 | } 70 | } 71 | 72 | override fun apMapK(fn: (B) -> R): ApplicativeFunctorKind, R> = map(fn) 73 | 74 | override fun apK(liftedFn: FunctorKind, (B) -> R>): ApplicativeFunctorKind, R> = ap(liftedFn.validation) 75 | 76 | override fun mapK(fn: (B) -> R): FunctorKind, R> = map(fn) 77 | 78 | override fun flatMapK(fn: (B) -> MonadKind, R>): MonadKind, R> = flatMap { fn(it).validation } 79 | } 80 | 81 | fun Validation.ifFail(default: B): B = when(this){ 82 | is Validation.Fail -> default 83 | is Validation.Success -> success 84 | } 85 | 86 | inline fun Validation.ifFail(errorHandler: (Collection) -> B): B = when(this){ 87 | is Validation.Fail -> errorHandler(failures()) 88 | is Validation.Success -> success 89 | } 90 | 91 | fun Collection>.flatten(): Validation> = 92 | flatten(Validation.Companion::pure) 93 | .validation 94 | 95 | inline fun Result.ifErrors(errorTransform: (Result.Errors) -> A): Validation = when(this){ 96 | is Result.Ok -> value.success() 97 | is Result.Errors -> errorTransform(this).fail() 98 | } 99 | 100 | inline fun B?.ifNullValidation(errorTransform: () -> A): Validation = this 101 | ?.run { this.success() } 102 | ?: errorTransform().fail() 103 | 104 | fun Result.toValidation(): Validation = when(this){ 105 | is Result.Ok -> value.success() 106 | is Result.Errors -> toFail(this) 107 | } 108 | 109 | fun B.success(): Validation.Success = Validation.Success(this) 110 | fun A.fail(): Validation.Fail = Validation.Fail(this) 111 | 112 | private fun toFail(errors: Result.Errors): Validation.Fail = 113 | Validation.Fail(fail = errors.error, prev = errors.prev?.run(::toFail)) 114 | 115 | infix operator fun ((B) -> C).plus(v: Validation): Validation = on(v).validation 116 | infix operator fun ApplicativeFunctorKind, ((B) -> C)>.plus(v: Validation): Validation = on(v).validation 117 | infix operator fun FunctorKind, ((B) -> C)>.plus(v: Validation): Validation = on(v).validation 118 | infix operator fun FunctorKind, ((B) -> C)>.plus(v: B): Validation = on(v).validation -------------------------------------------------------------------------------- /.mvn/wrapper/MavenWrapperDownloader.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2007-present the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | import java.net.*; 17 | import java.io.*; 18 | import java.nio.channels.*; 19 | import java.util.Properties; 20 | 21 | public class MavenWrapperDownloader { 22 | 23 | private static final String WRAPPER_VERSION = "0.5.6"; 24 | /** 25 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. 26 | */ 27 | private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" 28 | + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; 29 | 30 | /** 31 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to 32 | * use instead of the default one. 33 | */ 34 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH = 35 | ".mvn/wrapper/maven-wrapper.properties"; 36 | 37 | /** 38 | * Path where the maven-wrapper.jar will be saved to. 39 | */ 40 | private static final String MAVEN_WRAPPER_JAR_PATH = 41 | ".mvn/wrapper/maven-wrapper.jar"; 42 | 43 | /** 44 | * Name of the property which should be used to override the default download url for the wrapper. 45 | */ 46 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; 47 | 48 | public static void main(String args[]) { 49 | System.out.println("- Downloader started"); 50 | File baseDirectory = new File(args[0]); 51 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); 52 | 53 | // If the maven-wrapper.properties exists, read it and check if it contains a custom 54 | // wrapperUrl parameter. 55 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); 56 | String url = DEFAULT_DOWNLOAD_URL; 57 | if(mavenWrapperPropertyFile.exists()) { 58 | FileInputStream mavenWrapperPropertyFileInputStream = null; 59 | try { 60 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); 61 | Properties mavenWrapperProperties = new Properties(); 62 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); 63 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); 64 | } catch (IOException e) { 65 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); 66 | } finally { 67 | try { 68 | if(mavenWrapperPropertyFileInputStream != null) { 69 | mavenWrapperPropertyFileInputStream.close(); 70 | } 71 | } catch (IOException e) { 72 | // Ignore ... 73 | } 74 | } 75 | } 76 | System.out.println("- Downloading from: " + url); 77 | 78 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); 79 | if(!outputFile.getParentFile().exists()) { 80 | if(!outputFile.getParentFile().mkdirs()) { 81 | System.out.println( 82 | "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); 83 | } 84 | } 85 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); 86 | try { 87 | downloadFileFromURL(url, outputFile); 88 | System.out.println("Done"); 89 | System.exit(0); 90 | } catch (Throwable e) { 91 | System.out.println("- Error downloading"); 92 | e.printStackTrace(); 93 | System.exit(1); 94 | } 95 | } 96 | 97 | private static void downloadFileFromURL(String urlString, File destination) throws Exception { 98 | if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { 99 | String username = System.getenv("MVNW_USERNAME"); 100 | char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); 101 | Authenticator.setDefault(new Authenticator() { 102 | @Override 103 | protected PasswordAuthentication getPasswordAuthentication() { 104 | return new PasswordAuthentication(username, password); 105 | } 106 | }); 107 | } 108 | URL website = new URL(urlString); 109 | ReadableByteChannel rbc; 110 | rbc = Channels.newChannel(website.openStream()); 111 | FileOutputStream fos = new FileOutputStream(destination); 112 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); 113 | fos.close(); 114 | rbc.close(); 115 | } 116 | 117 | } 118 | -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/Maybe.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | import io.konad.Maybe.Companion.maybe 4 | import io.konad.Maybe.Companion.nullable 5 | import io.konad.applicative.builders.on 6 | import io.konad.applicative.flatten 7 | import io.konad.hkt.ApplicativeFunctorKind 8 | import io.konad.hkt.FunctorKind 9 | import io.konad.hkt.Kind 10 | import io.konad.hkt.MonadKind 11 | 12 | open class MaybeOf 13 | 14 | data class Maybe private constructor(private val value: T?): 15 | MonadKind, 16 | ApplicativeFunctorKind { 17 | 18 | companion object{ 19 | fun Kind.downcast() = this as Maybe 20 | 21 | fun pure(value: T) = Maybe(value) 22 | val T?.maybe: Maybe 23 | get() = Maybe(this) 24 | val Kind.nullable: T? 25 | get() = downcast().value 26 | } 27 | 28 | private inline fun map(fn: (T) -> R): Maybe = flatMap{ Maybe(fn(it)) } 29 | private inline fun flatMap(fn: (T) -> Maybe): Maybe = when(value){ 30 | null -> Maybe(null) 31 | else -> fn(value) 32 | } 33 | private fun ap(liftedFn: Maybe<((T) -> R)>): Maybe = liftedFn.flatMap(this::map) 34 | 35 | override fun flatMapK(fn: (T) -> MonadKind): MonadKind = flatMap { fn(it).downcast() } 36 | override fun mapK(fn: (T) -> R): FunctorKind = map(fn) 37 | override fun apMapK(fn: (T) -> R): ApplicativeFunctorKind = map(fn) 38 | override fun apK(liftedFn: FunctorKind R>): ApplicativeFunctorKind = ap(liftedFn.downcast()) 39 | } 40 | 41 | fun Collection.flatten(): Collection? = asSequence().map { it.maybe } 42 | .flatten(Maybe.Companion::pure) 43 | .nullable 44 | ?.toList() 45 | 46 | infix operator fun ((T) -> R)?.plus(t: T?): R? = this?.on(t.maybe)?.nullable 47 | infix operator fun ApplicativeFunctorKind R)>?.plus(t: T?): R? = this?.on(t.maybe)?.nullable 48 | infix operator fun FunctorKind R)>?.plus(t: T?): R? = this?.on(t.maybe)?.nullable 49 | 50 | infix operator fun ((A, B) -> RESULT).plus(a: A?) = curry().on(a.maybe) 51 | infix operator fun ((A, B, C) -> RESULT).plus(a: A?) = curry().on(a.maybe) 52 | infix operator fun ((A, B, C, D) -> RESULT).plus(a: A?) = curry().on(a.maybe) 53 | infix operator fun ((A, B, C, D, E) -> RESULT).plus(a: A?) = curry().on(a.maybe) 54 | infix operator fun ((A, B, C, D, E, F) -> RESULT).plus(a: A?) = curry().on(a.maybe) 55 | infix operator fun ((A, B, C, D, E, F, G) -> RESULT).plus(a: A?) = curry().on(a.maybe) 56 | infix operator fun ((A, B, C, D, E, F, G, H) -> RESULT).plus(a: A?) = curry().on(a.maybe) 57 | infix operator fun ((A, B, C, D, E, F, G, H, I) -> RESULT).plus(a: A?) = curry().on(a.maybe) 58 | infix operator fun ((A, B, C, D, E, F, G, H, I, J) -> RESULT).plus(a: A?) = curry().on(a.maybe) 59 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K) -> RESULT).plus(a: A?) = curry().on(a.maybe) 60 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L) -> RESULT).plus(a: A?) = curry().on(a.maybe) 61 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M) -> RESULT).plus(a: A?) = curry().on(a.maybe) 62 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N) -> RESULT).plus(a: A?) = curry().on(a.maybe) 63 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) -> RESULT).plus(a: A?) = curry().on(a.maybe) 64 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) -> RESULT).plus(a: A?) = curry().on(a.maybe) 65 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) -> RESULT).plus(a: A?) = curry().on(a.maybe) 66 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) -> RESULT).plus(a: A?) = curry().on(a.maybe) 67 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) -> RESULT).plus(a: A?) = curry().on(a.maybe) 68 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) -> RESULT).plus(a: A?) = curry().on(a.maybe) 69 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) -> RESULT).plus(a: A?) = curry().on(a.maybe) 70 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) -> RESULT).plus(a: A?) = curry().on(a.maybe) 71 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W) -> RESULT).plus(a: A?) = curry().on(a.maybe) -------------------------------------------------------------------------------- /src/test/kotlin/io/konad/usage/examples/CreateNewUserTests.kt: -------------------------------------------------------------------------------- 1 | package io.konad.usage.examples 2 | 3 | import io.konad.* 4 | import io.konad.Maybe.Companion.nullable 5 | import io.konad.applicative.builders.* 6 | import io.konad.exceptions.ResultException 7 | import io.konad.usage.examples.model.* 8 | import io.kotest.assertions.throwables.shouldThrow 9 | import io.kotest.core.spec.style.StringSpec 10 | import io.kotest.matchers.shouldBe 11 | import io.kotest.matchers.shouldNotBe 12 | 13 | class CreateNewUserTests : StringSpec({ 14 | 15 | val emailValue = "foo.bar@gmail.com" 16 | val phoneNumberValue = "+39 4756258" 17 | val passwordValue = "blablabla" 18 | val username = "foobar" 19 | val firstname = "Foo" 20 | val lastname = "Bar" 21 | val jobDescription = "developer" 22 | 23 | val expectedUser = 24 | Email.of(emailValue) 25 | .flatMap { email -> 26 | PhoneNumber.of(phoneNumberValue) 27 | .flatMap { phoneNumber -> 28 | Password.of(passwordValue) 29 | .map { password -> 30 | User( 31 | username, 32 | NameOfAPerson(firstname, lastname), 33 | password, 34 | UserContacts(email, phoneNumber), 35 | jobDescription 36 | ) 37 | } } } 38 | 39 | "How to build a new user" { 40 | val contacts: Result = ::UserContacts + 41 | Email.of(emailValue) + 42 | PhoneNumber.of(phoneNumberValue) 43 | 44 | val user: Result = ::User + 45 | username + 46 | NameOfAPerson(firstname, lastname).ok() + 47 | Password.of(passwordValue) + 48 | contacts + 49 | jobDescription.ifNull("job description should not be null") 50 | 51 | user shouldBe expectedUser 52 | } 53 | 54 | 55 | "How to build a new user in pure style" { 56 | 57 | val user = ::User.curry() 58 | .apply(username) 59 | .apply(NameOfAPerson(firstname, lastname)) 60 | .map(Password.of(passwordValue)) 61 | .ap(::UserContacts.curry() on Email.of(emailValue) on PhoneNumber.of(phoneNumberValue)) 62 | .pure(jobDescription) 63 | .result 64 | 65 | user shouldBe expectedUser 66 | } 67 | 68 | "Example of cumulating errors" { 69 | val user = ::User.curry() 70 | .on(username.ok()) 71 | .on(NameOfAPerson(firstname, lastname)) 72 | .on(Password.of(passwordValue)) 73 | .on(::UserContacts.curry() on Email.of(emailValue) on PhoneNumber.of("xxx")) 74 | .on(null.ifNull("job description should not be null")) 75 | .result 76 | 77 | val errors: Result.Errors? = when(user){ 78 | is Result.Ok -> null 79 | is Result.Errors -> user 80 | } 81 | 82 | errors shouldNotBe null 83 | errors?.toList()?.joinToString(",") { it.description } shouldBe "xxx should match a valid phone number, but it doesn't,job description should not be null" 84 | } 85 | 86 | "Example of cumulating errors with better error description using field" { 87 | val user = ::User + 88 | username.ok() + 89 | NameOfAPerson(firstname, lastname) + 90 | Password.of("123").field(User::password) + 91 | UserContacts.of(emailValue, "xxx").field(User::contacts) + 92 | "bla" 93 | 94 | val expectedError = """ 95 | password: Password should be at least 6 characters length. It was 3 characters 96 | contacts.phoneNumber: xxx should match a valid phone number, but it doesn't 97 | """.trimIndent() 98 | val ex = shouldThrow { user.get() } 99 | ex.errors.description("\n") shouldBe expectedError 100 | } 101 | 102 | "Example of build a nullable User" { 103 | val userContacts: UserContacts? = ::UserContacts + 104 | Email.of(emailValue).toNullable() + 105 | PhoneNumber.of(phoneNumberValue).toNullable() 106 | 107 | val user: User? = ::User + 108 | username + 109 | NameOfAPerson(firstname, lastname) + 110 | Password.of(passwordValue).toNullable() + 111 | userContacts + 112 | jobDescription 113 | 114 | user shouldBe expectedUser.get() 115 | } 116 | 117 | "Example of build a nullable that returns null"{ 118 | val user: User? = ::User.curry() 119 | .on(username) 120 | .on(NameOfAPerson(firstname, lastname)) 121 | .on(Password.of(passwordValue).toMaybe()) 122 | .on(::UserContacts.curry() on Email.of(emailValue).toMaybe() on PhoneNumber.of("xxx").toMaybe()) 123 | .on(jobDescription) 124 | .nullable 125 | 126 | user shouldBe null 127 | } 128 | 129 | "How to exit the monad with default values"{ 130 | val xx: Int? = 1 131 | val yy: Int? = null 132 | 133 | val total: Int = ({ x: Int, y: Int -> x + y } + 134 | xx.ifNull("xx should not be null") + 135 | yy.ifNull("yy should not be null")) 136 | .ifError(-1) 137 | 138 | total shouldBe -1 139 | } 140 | 141 | "How to cheat. (I would suggest not using it)"{ 142 | val xx: Int? = 1 143 | val yy: Int? = null 144 | 145 | shouldThrow{ 146 | { x: Int, y: Int -> x + y }.curry() 147 | .on(xx.ifNull("xx should not be null")) 148 | .on(yy.ifNull("yy should not be null")) 149 | .result 150 | .get() 151 | } 152 | } 153 | }) 154 | -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/curry.kt: -------------------------------------------------------------------------------- 1 | package io.konad 2 | 3 | fun ((A, B) -> RESULT).curry() = { a: A -> { b: B -> this(a, b) } } 4 | fun ((A, B, C) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> this(a, b, c) } } } 5 | fun ((A, B, C, D) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> this(a, b, c, d) } } } } 6 | fun ((A, B, C, D, E) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> {e: E -> this(a, b, c, d, e) } } } } } 7 | fun ((A, B, C, D, E, F) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> {e: E -> {f: F -> this(a, b, c, d, e, f) } } } } } } 8 | fun ((A, B, C, D, E, F, G) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> {e: E -> { f: F -> {g: G -> this(a, b, c, d, e, f, g) } } } } } } } 9 | fun ((A, B, C, D, E, F, G, H) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> {e: E -> { f: F -> { g: G -> { h: H -> this(a, b, c, d, e, f, g, h) } } } } } } } } 10 | fun ((A, B, C, D, E, F, G, H, I) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I ->this(a, b, c, d, e, f, g, h, i) } } } } } } } } } 11 | fun ((A, B, C, D, E, F, G, H, I, J) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J ->this(a, b, c, d, e, f, g, h, i, j) } } } } } } } } } } 12 | fun ((A, B, C, D, E, F, G, H, I, J, K) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> this(a, b, c, d, e, f, g, h, i, j, k) } } } } } } } } } } } 13 | fun ((A, B, C, D, E, F, G, H, I, J, K, L) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> this(a, b, c, d, e, f, g, h, i, j, k, l) } } } } } } } } } } } } 14 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> this(a, b, c, d, e, f, g, h, i, j, k, l, m) } } } } } } } } } } } } } 15 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n) } } } } } } } } } } } } } } 16 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o) } } } } } } } } } } } } } } } 17 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> { p: P -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p) } } } } } } } } } } } } } } } } 18 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> { p: P -> { q: Q -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q) } } } } } } } } } } } } } } } } } 19 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> { p: P -> { q: Q -> { r: R -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r) } } } } } } } } } } } } } } } } } } 20 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> { p: P -> { q: Q -> { r: R -> { s:S -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s) } } } } } } } } } } } } } } } } } } } 21 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> { p: P -> { q: Q -> { r: R -> { s: S -> { t: T -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t) } } } } } } } } } } } } } } } } } } } } 22 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> { p: P -> { q: Q -> { r: R -> { s: S -> { t: T -> { u: U -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u) } } } } } } } } } } } } } } } } } } } } } 23 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> { p: P -> { q: Q -> { r: R -> { s: S -> { t: T -> { u: U -> { v: V -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v) } } } } } } } } } } } } } } } } } } } } } } 24 | fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W) -> RESULT).curry() = { a: A -> { b: B -> { c: C -> { d: D -> { e: E -> { f: F -> { g: G -> { h: H -> { i: I -> { j: J -> { k: K -> { l: L -> { m: M -> { n: N -> { o: O -> { p: P -> { q: Q -> { r: R -> { s: S -> { t: T -> { u: U -> { v: V -> { w: W -> this(a, b, c, d, e, f, g, h, i, j, k, l, m, n, o, p, q, r, s, t, u, v, w) } } } } } } } } } } } } } } } } } } } } } } } 25 | -------------------------------------------------------------------------------- /mvnw.cmd: -------------------------------------------------------------------------------- 1 | @REM ---------------------------------------------------------------------------- 2 | @REM Licensed to the Apache Software Foundation (ASF) under one 3 | @REM or more contributor license agreements. See the NOTICE file 4 | @REM distributed with this work for additional information 5 | @REM regarding copyright ownership. The ASF licenses this file 6 | @REM to you under the Apache License, Version 2.0 (the 7 | @REM "License"); you may not use this file except in compliance 8 | @REM with the License. You may obtain a copy of the License at 9 | @REM 10 | @REM http://www.apache.org/licenses/LICENSE-2.0 11 | @REM 12 | @REM Unless required by applicable law or agreed to in writing, 13 | @REM software distributed under the License is distributed on an 14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 15 | @REM KIND, either express or implied. See the License for the 16 | @REM specific language governing permissions and limitations 17 | @REM under the License. 18 | @REM ---------------------------------------------------------------------------- 19 | 20 | @REM ---------------------------------------------------------------------------- 21 | @REM Maven Start Up Batch script 22 | @REM 23 | @REM Required ENV vars: 24 | @REM JAVA_HOME - location of a JDK home dir 25 | @REM 26 | @REM Optional ENV vars 27 | @REM M2_HOME - location of maven2's installed home dir 28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands 29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending 30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven 31 | @REM e.g. to debug Maven itself, use 32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files 34 | @REM ---------------------------------------------------------------------------- 35 | 36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' 37 | @echo off 38 | @REM set title of command window 39 | title %0 40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' 41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% 42 | 43 | @REM set %HOME% to equivalent of $HOME 44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") 45 | 46 | @REM Execute a user defined script before this one 47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre 48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending 49 | if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" 50 | if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" 51 | :skipRcPre 52 | 53 | @setlocal 54 | 55 | set ERROR_CODE=0 56 | 57 | @REM To isolate internal variables from possible post scripts, we use another setlocal 58 | @setlocal 59 | 60 | @REM ==== START VALIDATION ==== 61 | if not "%JAVA_HOME%" == "" goto OkJHome 62 | 63 | echo. 64 | echo Error: JAVA_HOME not found in your environment. >&2 65 | echo Please set the JAVA_HOME variable in your environment to match the >&2 66 | echo location of your Java installation. >&2 67 | echo. 68 | goto error 69 | 70 | :OkJHome 71 | if exist "%JAVA_HOME%\bin\java.exe" goto init 72 | 73 | echo. 74 | echo Error: JAVA_HOME is set to an invalid directory. >&2 75 | echo JAVA_HOME = "%JAVA_HOME%" >&2 76 | echo Please set the JAVA_HOME variable in your environment to match the >&2 77 | echo location of your Java installation. >&2 78 | echo. 79 | goto error 80 | 81 | @REM ==== END VALIDATION ==== 82 | 83 | :init 84 | 85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn". 86 | @REM Fallback to current working directory if not found. 87 | 88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% 89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir 90 | 91 | set EXEC_DIR=%CD% 92 | set WDIR=%EXEC_DIR% 93 | :findBaseDir 94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound 95 | cd .. 96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound 97 | set WDIR=%CD% 98 | goto findBaseDir 99 | 100 | :baseDirFound 101 | set MAVEN_PROJECTBASEDIR=%WDIR% 102 | cd "%EXEC_DIR%" 103 | goto endDetectBaseDir 104 | 105 | :baseDirNotFound 106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR% 107 | cd "%EXEC_DIR%" 108 | 109 | :endDetectBaseDir 110 | 111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig 112 | 113 | @setlocal EnableExtensions EnableDelayedExpansion 114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a 115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% 116 | 117 | :endReadAdditionalConfig 118 | 119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" 120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" 121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 122 | 123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 124 | 125 | FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( 126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B 127 | ) 128 | 129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data. 131 | if exist %WRAPPER_JAR% ( 132 | if "%MVNW_VERBOSE%" == "true" ( 133 | echo Found %WRAPPER_JAR% 134 | ) 135 | ) else ( 136 | if not "%MVNW_REPOURL%" == "" ( 137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 138 | ) 139 | if "%MVNW_VERBOSE%" == "true" ( 140 | echo Couldn't find %WRAPPER_JAR%, downloading it ... 141 | echo Downloading from: %DOWNLOAD_URL% 142 | ) 143 | 144 | powershell -Command "&{"^ 145 | "$webclient = new-object System.Net.WebClient;"^ 146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ 147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ 148 | "}"^ 149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ 150 | "}" 151 | if "%MVNW_VERBOSE%" == "true" ( 152 | echo Finished downloading %WRAPPER_JAR% 153 | ) 154 | ) 155 | @REM End of extension 156 | 157 | @REM Provide a "standardized" way to retrieve the CLI args that will 158 | @REM work with both Windows and non-Windows executions. 159 | set MAVEN_CMD_LINE_ARGS=%* 160 | 161 | %MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* 162 | if ERRORLEVEL 1 goto error 163 | goto end 164 | 165 | :error 166 | set ERROR_CODE=1 167 | 168 | :end 169 | @endlocal & set ERROR_CODE=%ERROR_CODE% 170 | 171 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost 172 | @REM check for post script, once with legacy .bat ending and once with .cmd ending 173 | if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" 174 | if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" 175 | :skipRcPost 176 | 177 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' 178 | if "%MAVEN_BATCH_PAUSE%" == "on" pause 179 | 180 | if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% 181 | 182 | exit /B %ERROR_CODE% 183 | -------------------------------------------------------------------------------- /src/main/kotlin/io/konad/applicative/builders/applicativeBuilders.kt: -------------------------------------------------------------------------------- 1 | package io.konad.applicative.builders 2 | 3 | import io.konad.Maybe.Companion.maybe 4 | import io.konad.curry 5 | import io.konad.hkt.ApplicativeFunctorKind 6 | import io.konad.hkt.FunctorKind 7 | 8 | infix fun ((T) -> R).on(f: FunctorKind): FunctorKind = f.mapK(this) 9 | infix fun ((T) -> R).on(f: ApplicativeFunctorKind): ApplicativeFunctorKind = f.apMapK(this) 10 | infix fun FunctorKind R)>.on(f: ApplicativeFunctorKind): ApplicativeFunctorKind = f.apK(this) 11 | infix fun FunctorKind R)>.on(t: T): FunctorKind = mapK { it(t) } 12 | infix fun ((T) -> R).on(t: T): R = this(t) 13 | 14 | infix operator fun ((A, B) -> RESULT).plus(a: FunctorKind) = curry().on(a) 15 | infix operator fun ((A, B, C) -> RESULT).plus(a: FunctorKind) = curry().on(a) 16 | infix operator fun ((A, B, C, D) -> RESULT).plus(a: FunctorKind) = curry().on(a) 17 | infix operator fun ((A, B, C, D, E) -> RESULT).plus(a: FunctorKind) = curry().on(a) 18 | infix operator fun ((A, B, C, D, E, F) -> RESULT).plus(a: FunctorKind) = curry().on(a) 19 | infix operator fun ((A, B, C, D, E, F, G) -> RESULT).plus(a: FunctorKind) = curry().on(a) 20 | infix operator fun ((A, B, C, D, E, F, G, H) -> RESULT).plus(a: FunctorKind) = curry().on(a) 21 | infix operator fun ((A, B, C, D, E, F, G, H, I) -> RESULT).plus(a: FunctorKind) = curry().on(a) 22 | infix operator fun ((A, B, C, D, E, F, G, H, I, J) -> RESULT).plus(a: FunctorKind) = curry().on(a) 23 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K) -> RESULT).plus(a: FunctorKind) = curry().on(a) 24 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L) -> RESULT).plus(a: FunctorKind) = curry().on(a) 25 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M) -> RESULT).plus(a: FunctorKind) = curry().on(a) 26 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N) -> RESULT).plus(a: FunctorKind) = curry().on(a) 27 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) -> RESULT).plus(a: FunctorKind) = curry().on(a) 28 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) -> RESULT).plus(a: FunctorKind) = curry().on(a) 29 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) -> RESULT).plus(a: FunctorKind) = curry().on(a) 30 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) -> RESULT).plus(a: FunctorKind) = curry().on(a) 31 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) -> RESULT).plus(a: FunctorKind) = curry().on(a) 32 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) -> RESULT).plus(a: FunctorKind) = curry().on(a) 33 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) -> RESULT).plus(a: FunctorKind) = curry().on(a) 34 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) -> RESULT).plus(a: FunctorKind) = curry().on(a) 35 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W) -> RESULT).plus(a: FunctorKind) = curry().on(a) 36 | 37 | infix operator fun ((A, B) -> RESULT).plus(a: A) = curry().on(a) 38 | infix operator fun ((A, B, C) -> RESULT).plus(a: A) = curry().on(a) 39 | infix operator fun ((A, B, C, D) -> RESULT).plus(a: A) = curry().on(a) 40 | infix operator fun ((A, B, C, D, E) -> RESULT).plus(a: A) = curry().on(a) 41 | infix operator fun ((A, B, C, D, E, F) -> RESULT).plus(a: A) = curry().on(a) 42 | infix operator fun ((A, B, C, D, E, F, G) -> RESULT).plus(a: A) = curry().on(a) 43 | infix operator fun ((A, B, C, D, E, F, G, H) -> RESULT).plus(a: A) = curry().on(a) 44 | infix operator fun ((A, B, C, D, E, F, G, H, I) -> RESULT).plus(a: A) = curry().on(a) 45 | infix operator fun ((A, B, C, D, E, F, G, H, I, J) -> RESULT).plus(a: A) = curry().on(a) 46 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K) -> RESULT).plus(a: A) = curry().on(a) 47 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L) -> RESULT).plus(a: A) = curry().on(a) 48 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M) -> RESULT).plus(a: A) = curry().on(a) 49 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N) -> RESULT).plus(a: A) = curry().on(a) 50 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O) -> RESULT).plus(a: A) = curry().on(a) 51 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P) -> RESULT).plus(a: A) = curry().on(a) 52 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q) -> RESULT).plus(a: A) = curry().on(a) 53 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R) -> RESULT).plus(a: A) = curry().on(a) 54 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S) -> RESULT).plus(a: A) = curry().on(a) 55 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T) -> RESULT).plus(a: A) = curry().on(a) 56 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U) -> RESULT).plus(a: A) = curry().on(a) 57 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V) -> RESULT).plus(a: A) = curry().on(a) 58 | infix operator fun ((A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P, Q, R, S, T, U, V, W) -> RESULT).plus(a: A) = curry().on(a) 59 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 4.0.0 5 | 6 | io.github.lucapiccinelli 7 | konad 8 | 1.2.6 9 | jar 10 | 11 | io.github.lucapiccinelli konad 12 | Simple Kotlin monads for every day error handling 13 | https://github.com/lucapiccinelli/konad 14 | 15 | 16 | 17 | MIT License 18 | https://opensource.org/licenses/MIT 19 | repo 20 | 21 | 22 | 23 | 24 | 25 | Luca Piccinelli 26 | luca.picci@gmail.com 27 | Luca Piccinelli 28 | https://medium.com/@luca_picci 29 | 30 | 31 | 32 | 33 | scm:git:git://github.com/lucapiccinelli/konad.git 34 | scm:git:ssh://github.com:lucapiccinelli/konad.git 35 | https://github.com/lucapiccinelli/konad/tree/master 36 | 37 | 38 | 39 | 40 | ossrh 41 | https://oss.sonatype.org/content/repositories/snapshots 42 | 43 | 44 | ossrh 45 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 46 | 47 | 48 | 49 | 50 | UTF-8 51 | 1.6.10 52 | official 53 | 4.12 54 | 5.2.2 55 | 56 | 57 | 58 | 59 | publish 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-gpg-plugin 65 | 1.6 66 | 67 | 68 | sign-artifacts 69 | verify 70 | 71 | sign 72 | 73 | 74 | 75 | 76 | 77 | 78 | --pinentry-mode 79 | loopback 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | org.jetbrains.kotlin 91 | kotlin-stdlib 92 | ${kotlin.version} 93 | 94 | 95 | org.jetbrains.kotlin 96 | kotlin-reflect 97 | ${kotlin.version} 98 | runtime 99 | 100 | 101 | org.jetbrains.kotlin 102 | kotlin-test-junit 103 | ${kotlin.version} 104 | test 105 | 106 | 107 | junit 108 | junit 109 | ${junit.version} 110 | test 111 | 112 | 113 | 114 | 115 | io.kotest 116 | kotest-runner-junit5-jvm 117 | ${kotest.version} 118 | test 119 | 120 | 121 | 122 | io.kotest 123 | kotest-assertions-core-jvm 124 | ${kotest.version} 125 | test 126 | 127 | 128 | 129 | io.kotest 130 | kotest-property-jvm 131 | ${kotest.version} 132 | test 133 | 134 | 135 | 136 | 137 | 138 | 139 | jcenter 140 | JCenter 141 | https://jcenter.bintray.com/ 142 | 143 | 144 | 145 | 146 | src/main/kotlin 147 | src/test/kotlin 148 | 149 | 150 | 151 | org.jetbrains.kotlin 152 | kotlin-maven-plugin 153 | ${kotlin.version} 154 | 155 | 156 | compile 157 | compile 158 | 159 | compile 160 | 161 | 162 | 163 | test-compile 164 | test-compile 165 | 166 | test-compile 167 | 168 | 169 | 170 | 171 | 11 172 | 173 | 174 | 175 | 176 | org.apache.maven.plugins 177 | maven-compiler-plugin 178 | 3.8.1 179 | 180 | 181 | compile 182 | compile 183 | 184 | compile 185 | 186 | 187 | 188 | testCompile 189 | test-compile 190 | 191 | testCompile 192 | 193 | 194 | 195 | 196 | 197 | 198 | org.apache.maven.plugins 199 | maven-surefire-plugin 200 | 2.22.2 201 | 202 | 203 | 204 | org.sonatype.plugins 205 | nexus-staging-maven-plugin 206 | 1.6.8 207 | true 208 | 209 | ossrh 210 | https://oss.sonatype.org/ 211 | true 212 | 213 | 214 | 215 | 216 | org.apache.maven.plugins 217 | maven-source-plugin 218 | 3.1.0 219 | 220 | 221 | attach-sources 222 | 223 | jar-no-fork 224 | 225 | 226 | 227 | 228 | 229 | 230 | org.jetbrains.dokka 231 | dokka-maven-plugin 232 | 1.4.20 233 | 234 | 235 | prepare-package 236 | 237 | dokka 238 | javadoc 239 | javadocJar 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | -------------------------------------------------------------------------------- /mvnw: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | # ---------------------------------------------------------------------------- 3 | # Licensed to the Apache Software Foundation (ASF) under one 4 | # or more contributor license agreements. See the NOTICE file 5 | # distributed with this work for additional information 6 | # regarding copyright ownership. The ASF licenses this file 7 | # to you under the Apache License, Version 2.0 (the 8 | # "License"); you may not use this file except in compliance 9 | # with the License. You may obtain a copy of the License at 10 | # 11 | # http://www.apache.org/licenses/LICENSE-2.0 12 | # 13 | # Unless required by applicable law or agreed to in writing, 14 | # software distributed under the License is distributed on an 15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 16 | # KIND, either express or implied. See the License for the 17 | # specific language governing permissions and limitations 18 | # under the License. 19 | # ---------------------------------------------------------------------------- 20 | 21 | # ---------------------------------------------------------------------------- 22 | # Maven Start Up Batch script 23 | # 24 | # Required ENV vars: 25 | # ------------------ 26 | # JAVA_HOME - location of a JDK home dir 27 | # 28 | # Optional ENV vars 29 | # ----------------- 30 | # M2_HOME - location of maven2's installed home dir 31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven 32 | # e.g. to debug Maven itself, use 33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files 35 | # ---------------------------------------------------------------------------- 36 | 37 | if [ -z "$MAVEN_SKIP_RC" ] ; then 38 | 39 | if [ -f /etc/mavenrc ] ; then 40 | . /etc/mavenrc 41 | fi 42 | 43 | if [ -f "$HOME/.mavenrc" ] ; then 44 | . "$HOME/.mavenrc" 45 | fi 46 | 47 | fi 48 | 49 | # OS specific support. $var _must_ be set to either true or false. 50 | cygwin=false; 51 | darwin=false; 52 | mingw=false 53 | case "`uname`" in 54 | CYGWIN*) cygwin=true ;; 55 | MINGW*) mingw=true;; 56 | Darwin*) darwin=true 57 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home 58 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html 59 | if [ -z "$JAVA_HOME" ]; then 60 | if [ -x "/usr/libexec/java_home" ]; then 61 | export JAVA_HOME="`/usr/libexec/java_home`" 62 | else 63 | export JAVA_HOME="/Library/Java/Home" 64 | fi 65 | fi 66 | ;; 67 | esac 68 | 69 | if [ -z "$JAVA_HOME" ] ; then 70 | if [ -r /etc/gentoo-release ] ; then 71 | JAVA_HOME=`java-config --jre-home` 72 | fi 73 | fi 74 | 75 | if [ -z "$M2_HOME" ] ; then 76 | ## resolve links - $0 may be a link to maven's home 77 | PRG="$0" 78 | 79 | # need this for relative symlinks 80 | while [ -h "$PRG" ] ; do 81 | ls=`ls -ld "$PRG"` 82 | link=`expr "$ls" : '.*-> \(.*\)$'` 83 | if expr "$link" : '/.*' > /dev/null; then 84 | PRG="$link" 85 | else 86 | PRG="`dirname "$PRG"`/$link" 87 | fi 88 | done 89 | 90 | saveddir=`pwd` 91 | 92 | M2_HOME=`dirname "$PRG"`/.. 93 | 94 | # make it fully qualified 95 | M2_HOME=`cd "$M2_HOME" && pwd` 96 | 97 | cd "$saveddir" 98 | # echo Using m2 at $M2_HOME 99 | fi 100 | 101 | # For Cygwin, ensure paths are in UNIX format before anything is touched 102 | if $cygwin ; then 103 | [ -n "$M2_HOME" ] && 104 | M2_HOME=`cygpath --unix "$M2_HOME"` 105 | [ -n "$JAVA_HOME" ] && 106 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"` 107 | [ -n "$CLASSPATH" ] && 108 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"` 109 | fi 110 | 111 | # For Mingw, ensure paths are in UNIX format before anything is touched 112 | if $mingw ; then 113 | [ -n "$M2_HOME" ] && 114 | M2_HOME="`(cd "$M2_HOME"; pwd)`" 115 | [ -n "$JAVA_HOME" ] && 116 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" 117 | fi 118 | 119 | if [ -z "$JAVA_HOME" ]; then 120 | javaExecutable="`which javac`" 121 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then 122 | # readlink(1) is not available as standard on Solaris 10. 123 | readLink=`which readlink` 124 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then 125 | if $darwin ; then 126 | javaHome="`dirname \"$javaExecutable\"`" 127 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" 128 | else 129 | javaExecutable="`readlink -f \"$javaExecutable\"`" 130 | fi 131 | javaHome="`dirname \"$javaExecutable\"`" 132 | javaHome=`expr "$javaHome" : '\(.*\)/bin'` 133 | JAVA_HOME="$javaHome" 134 | export JAVA_HOME 135 | fi 136 | fi 137 | fi 138 | 139 | if [ -z "$JAVACMD" ] ; then 140 | if [ -n "$JAVA_HOME" ] ; then 141 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 142 | # IBM's JDK on AIX uses strange locations for the executables 143 | JAVACMD="$JAVA_HOME/jre/sh/java" 144 | else 145 | JAVACMD="$JAVA_HOME/bin/java" 146 | fi 147 | else 148 | JAVACMD="`which java`" 149 | fi 150 | fi 151 | 152 | if [ ! -x "$JAVACMD" ] ; then 153 | echo "Error: JAVA_HOME is not defined correctly." >&2 154 | echo " We cannot execute $JAVACMD" >&2 155 | exit 1 156 | fi 157 | 158 | if [ -z "$JAVA_HOME" ] ; then 159 | echo "Warning: JAVA_HOME environment variable is not set." 160 | fi 161 | 162 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher 163 | 164 | # traverses directory structure from process work directory to filesystem root 165 | # first directory with .mvn subdirectory is considered project base directory 166 | find_maven_basedir() { 167 | 168 | if [ -z "$1" ] 169 | then 170 | echo "Path not specified to find_maven_basedir" 171 | return 1 172 | fi 173 | 174 | basedir="$1" 175 | wdir="$1" 176 | while [ "$wdir" != '/' ] ; do 177 | if [ -d "$wdir"/.mvn ] ; then 178 | basedir=$wdir 179 | break 180 | fi 181 | # workaround for JBEAP-8937 (on Solaris 10/Sparc) 182 | if [ -d "${wdir}" ]; then 183 | wdir=`cd "$wdir/.."; pwd` 184 | fi 185 | # end of workaround 186 | done 187 | echo "${basedir}" 188 | } 189 | 190 | # concatenates all lines of a file 191 | concat_lines() { 192 | if [ -f "$1" ]; then 193 | echo "$(tr -s '\n' ' ' < "$1")" 194 | fi 195 | } 196 | 197 | BASE_DIR=`find_maven_basedir "$(pwd)"` 198 | if [ -z "$BASE_DIR" ]; then 199 | exit 1; 200 | fi 201 | 202 | ########################################################################################## 203 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central 204 | # This allows using the maven wrapper in projects that prohibit checking in binary data. 205 | ########################################################################################## 206 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then 207 | if [ "$MVNW_VERBOSE" = true ]; then 208 | echo "Found .mvn/wrapper/maven-wrapper.jar" 209 | fi 210 | else 211 | if [ "$MVNW_VERBOSE" = true ]; then 212 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." 213 | fi 214 | if [ -n "$MVNW_REPOURL" ]; then 215 | jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 216 | else 217 | jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" 218 | fi 219 | while IFS="=" read key value; do 220 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;; 221 | esac 222 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" 223 | if [ "$MVNW_VERBOSE" = true ]; then 224 | echo "Downloading from: $jarUrl" 225 | fi 226 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" 227 | if $cygwin; then 228 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` 229 | fi 230 | 231 | if command -v wget > /dev/null; then 232 | if [ "$MVNW_VERBOSE" = true ]; then 233 | echo "Found wget ... using wget" 234 | fi 235 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 236 | wget "$jarUrl" -O "$wrapperJarPath" 237 | else 238 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" 239 | fi 240 | elif command -v curl > /dev/null; then 241 | if [ "$MVNW_VERBOSE" = true ]; then 242 | echo "Found curl ... using curl" 243 | fi 244 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then 245 | curl -o "$wrapperJarPath" "$jarUrl" -f 246 | else 247 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f 248 | fi 249 | 250 | else 251 | if [ "$MVNW_VERBOSE" = true ]; then 252 | echo "Falling back to using Java to download" 253 | fi 254 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" 255 | # For Cygwin, switch paths to Windows format before running javac 256 | if $cygwin; then 257 | javaClass=`cygpath --path --windows "$javaClass"` 258 | fi 259 | if [ -e "$javaClass" ]; then 260 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 261 | if [ "$MVNW_VERBOSE" = true ]; then 262 | echo " - Compiling MavenWrapperDownloader.java ..." 263 | fi 264 | # Compiling the Java class 265 | ("$JAVA_HOME/bin/javac" "$javaClass") 266 | fi 267 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then 268 | # Running the downloader 269 | if [ "$MVNW_VERBOSE" = true ]; then 270 | echo " - Running MavenWrapperDownloader.java ..." 271 | fi 272 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") 273 | fi 274 | fi 275 | fi 276 | fi 277 | ########################################################################################## 278 | # End of extension 279 | ########################################################################################## 280 | 281 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} 282 | if [ "$MVNW_VERBOSE" = true ]; then 283 | echo $MAVEN_PROJECTBASEDIR 284 | fi 285 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" 286 | 287 | # For Cygwin, switch paths to Windows format before running java 288 | if $cygwin; then 289 | [ -n "$M2_HOME" ] && 290 | M2_HOME=`cygpath --path --windows "$M2_HOME"` 291 | [ -n "$JAVA_HOME" ] && 292 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` 293 | [ -n "$CLASSPATH" ] && 294 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"` 295 | [ -n "$MAVEN_PROJECTBASEDIR" ] && 296 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` 297 | fi 298 | 299 | # Provide a "standardized" way to retrieve the CLI args that will 300 | # work with both Windows and non-Windows executions. 301 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" 302 | export MAVEN_CMD_LINE_ARGS 303 | 304 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain 305 | 306 | exec "$JAVACMD" \ 307 | $MAVEN_OPTS \ 308 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ 309 | "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ 310 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" 311 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Konad 2 | 3 | --- 4 | 5 | [![Build and Test](https://github.com/lucapiccinelli/konad/workflows/build-and-test/badge.svg)](https://github.com/lucapiccinelli/konad/actions) 6 | [![Maven Central](http://img.shields.io/maven-central/v/io.github.lucapiccinelli/konad.svg)](https://search.maven.org/search?q=a:konad) 7 | 8 | Monads composition API that just works. For OOP developers. It is well suited to compose also Kotlin nullables. 9 | 10 | ## Why another functional library for Kotlin? 11 | 12 | I know, we have [Arrow](https://arrow-kt.io/) that is the best functional library around. Anyway if you only want to do simple tasks, like validating your domain classes, Arrow is a bit of an overkill. 13 | 14 | Also, Arrow is a real functional library, with a plenty of functional concepts that you need to digest before being productive. For the typical OOP developer, it has a quite steep learning curve. 15 | 16 | ## Konad to the OOP rescue 17 | 18 | Here it comes Konad. It has only three classes: 19 | - [**Result**](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/Result.kt): can be `Result.Ok` or `Result.Errors`. 20 | - [**Validation**](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/Validation.kt): can be `Validation.Success` or `Validation.Fail`. 21 | - [**Maybe**](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/Maybe.kt): you know this... yet another Optional/Option/Nullable whatever. (But read the [Maybe](#maybe) section below, it will get clear why we need it) 22 | 23 | These are **monads** and **applicative functors**, so they implement the usual `map`, `flatMap` and `ap` methods. 24 | 25 | Konad exists **with the only purpose** to let you easily compose these three classes. 26 | 27 | Advanced use-cases examples are described here: 28 | - [Nice Kotlin Nullables and Where to Find Them](https://medium.com/swlh/nice-kotlin-nullables-and-where-to-find-them-85d8de481e41?source=friends_link&sk=992c123a45421d26a6e21637e4ecdfcd) 29 | - [Type-safe Domain Modeling in Kotlin](https://betterprogramming.pub/type-safe-domain-modeling-in-kotlin-425ddbc73732?source=friends_link&sk=2fedd10125b31cf7ca378878de4b3491) 30 | 31 | ## Getting started 32 | 33 | Add the dependency 34 | 35 | #### Maven 36 | add in pom.xml 37 | ```xml 38 | 39 | io.github.lucapiccinelli 40 | konad 41 | 1.2.6 42 | 43 | ``` 44 | 45 | #### Gradle 46 | add in build.gradle 47 | ```groovy 48 | dependencies { 49 | implementation "io.github.lucapiccinelli:konad:1.2.6" 50 | } 51 | ``` 52 | 53 | ## Usage example 54 | 55 | *For an exaustive list of usage examples, please refer to test suite [CreateNewUserTests.kt](https://github.com/lucapiccinelli/konad/blob/master/src/test/kotlin/io/konad/usage/examples/CreateNewUserTests.kt) 56 | and to [ResultTests.kt](https://github.com/lucapiccinelli/konad/blob/master/src/test/kotlin/io/konad/ResultTests.kt)* 57 | 58 | Let's say you have a `User` class, that has an `Email` and a `PhoneNumber`. Email and PhoneNumber are built so that they can only be constructed using a factory method. It will return a `Result.Errors` type if the value passed is not valid. 59 | 60 | ```kotlin 61 | 62 | data class User(val username: String, val email: Email, val phoneNumber: PhoneNumber, val firstname: String) 63 | 64 | data class Email private constructor (val value: String) { 65 | companion object{ 66 | fun of(emailValue: String): Result = if (Regex(EMAIL_REGEX).matches(emailValue)) 67 | Email(emailValue).ok() 68 | else "$emailValue doesn't match an email format".error() 69 | } 70 | } 71 | 72 | data class PhoneNumber private constructor(val value: String){ 73 | companion object { 74 | fun of(phoneNumberValue: String): Result = if(Regex(PHONENUMBER_REGEX).matches(phoneNumberValue)) 75 | PhoneNumber(phoneNumberValue).ok() 76 | else "$phoneNumberValue should match a valid phone number, but it doesn't".error() 77 | } 78 | } 79 | 80 | ``` 81 | 82 | `Email` and `PhoneNumber` constructors are private, so that you can be sure that it can't exist a `User` with invalid contacts. However, the factory methods give you back a `Result/Result`. 83 | 84 | In order to compose them and get a `Result` you have to do the following 85 | 86 | ```kotlin 87 | 88 | val userResult: Result = ::User + 89 | "foo.bar" + 90 | Email.of("foo.bar") + // This email is invalid -> returns Result.Errors 91 | PhoneNumber.of("xxx") + // This phone number is invalid -> returns Result.Errors 92 | "Foo" 93 | 94 | when(userResult){ 95 | is Result.Ok -> userResult.toString() 96 | is Result.Errors -> userResult.toList().joinToString(" - ") 97 | }.run(::println) // This is going to print "foo.bar doesn't match an email format - xxx should match a valid phone number, but it doesn't 98 | 99 | // or 100 | 101 | userResult 102 | .map{ user -> user.toString() } 103 | .ifError { errors -> errors.description(errorDescriptionsSeparator = " - ") } 104 | .run(::println) 105 | 106 | ``` 107 | 108 | ## The pure functional style. 109 | 110 | Composition happens thanks to concepts named **functors** and **applicative Functors**. 111 | 112 | I chose to stay simple and practical, then all the methods that implement composition are called `on` (See [applicativeBuilders.kt](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/applicative/builders/applicativeBuilders.kt)). 113 | However, for those who love the functional naming, you can choose this other style. (See [applicativeBuildersPureStyle.kt](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/applicative/builders/applicativeBuildersPureStyle.kt)) 114 | 115 | ```kotlin 116 | 117 | val user: Result = ::User.curry() 118 | .apply("foo.bar") 119 | .map(Email.of("foo.bar")) 120 | .ap(PhoneNumber.of("xxx")) 121 | .pure("Foo") 122 | .result 123 | 124 | ``` 125 | 126 | 127 | ## Maybe 128 | 129 | `Maybe` is needed only to wrap Kotlin *nullables* and bring them to a **higher-kinded type** (see [unaryHigherKindedTypes.kt](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/hkt/unaryHigherKindedTypes.kt)). 130 | In this way `on`, can be used to compose nullables. 131 | 132 | Its constructor is private because **you should avoid using it** in order to express *optionality*. Kotlin nullability is perfect for that purpose. 133 | 134 | ### How to compose nullables 135 | If ever you tried to compose *nullables* in Kotlin, then probably you ended up having something like the following 136 | 137 | ```kotlin 138 | 139 | val foo: Int? = 1 140 | val bar: String? = "2" 141 | val baz: Float? = 3.0f 142 | 143 | fun useThem(x: Int, y: String, z: Float): Int = x + y.toInt() + z.toInt() 144 | 145 | val result1: Int? = foo 146 | ?.let { bar 147 | ?.let { baz 148 | ?.let { useThem(foo, bar, baz) } } } 149 | 150 | // or 151 | 152 | val result2: Int? = if(foo != null && bar != null && baz != null) 153 | useThem(foo, bar, baz) 154 | else null 155 | 156 | ``` 157 | 158 | This is not very clean. And it gets even worse if would like to give an error message when a `null` happens. 159 | 160 | Using Konad, nullables can be composed as follows 161 | 162 | ```kotlin 163 | 164 | val result: Int? = ::useThem + foo + bar + baz 165 | 166 | ``` 167 | 168 | or you can choose to give an explanatory message when something is `null` 169 | 170 | ```kotlin 171 | 172 | val result: Result = ::useThem + 173 | foo.ifNull("Foo should not be null") + 174 | bar.ifNull("Bar should not be null") + 175 | baz.ifNull("Baz should not be null") 176 | 177 | ``` 178 | 179 | 180 | ## Validation 181 | 182 | `Validation` is like an `Either` monad, but with the left case accumulation. It is similar to `Result` but instead of fixing the error case as a string description, it lets you 183 | decide how you represent the error. Example: 184 | 185 | ```kotlin 186 | 187 | sealed class ResourceError { 188 | data class BadInput(val description: String) : ResourceError() 189 | object NotFound : ResourceError() 190 | object Forbidden : ResourceError() 191 | } 192 | 193 | fun readUser(id: String): Validation = 194 | if (id.isBlank()) ResourceError.BadInput("id should not be blank").fail() 195 | else repository.findById(id)?.success() ?: ResourceError.NotFound.fail() 196 | 197 | readUser("xxx") 198 | .map { user: User -> println(user) } 199 | .ifFail { failures: Collection -> println(failures) } 200 | 201 | ``` 202 | 203 | ## Flatten 204 | 205 | What if you have a `List>` and you want a `Result>`? Then use `flatten` extension method. 206 | 207 | ```kotlin 208 | 209 | val r: Result> = listOf(Result.Ok(1), Result.Ok(2)).flatten() 210 | 211 | ``` 212 | 213 | Errors get cumulated as usual 214 | 215 | ```kotlin 216 | val r: Result> = listOf(Result.Errors("error1"), Result.Ok(1), Result.Errors("error2")) 217 | .flatten() 218 | 219 | when(r){ 220 | is Result.Ok -> r.value.toString() 221 | is Result.Errors -> r.description 222 | }.run(::println) // will print error1 - error2 223 | ``` 224 | 225 | Obviously it works also on nullables: `Collection -> Collection?` 226 | 227 | ```kotlin 228 | val flattened = setOf("a", null, "c").flatten() 229 | 230 | flattened shouldBe null 231 | ``` 232 | 233 | and on Validation 234 | 235 | ```kotlin 236 | val v: Validation> = listOf("error1".fail(), 1.success(), "error2".fail()).flatten() 237 | ``` 238 | 239 | ## Error enrichment 240 | 241 | Sometime you need to add some details on an error, or to transform it. `Result` and `Validation` monads have convenience method for this case. 242 | Examples: 243 | 244 | ```kotlin 245 | fun checkNotEmpty(value: String) = if(value.isBlank()) "value should not be blank".error() else value.ok() 246 | 247 | data class User private constructor(val firstName: String, val lastname: String){ 248 | companion object{ 249 | fun of(firstname: String, lastname: String): Result = ::User.curry() 250 | .on(checkNotEmpty(firstname)) 251 | .on(checkNotEmpty(lastname)) 252 | .result 253 | } 254 | } 255 | 256 | ``` 257 | 258 | in this example, if both `firstname` and `lastname` are blank, then you will get two errors. Unfortunately both of those errors will have the same description, and you will not be 259 | able to distinct which `value should not be empty`. To fix, there is the method `Result::errorTitle` 260 | 261 | ```kotlin 262 | 263 | fun of(firstname: String, lastname: String): Result = ::User.curry() 264 | .on(checkNotEmpty(firstname).errorTitle("firstname")) 265 | .on(checkNotEmpty(lastname).errorTitle("lastname")) 266 | .result 267 | 268 | ``` 269 | 270 | You can find a more detailed specification here: 271 | [ResultTests](https://github.com/lucapiccinelli/konad/blob/master/src/test/kotlin/io/konad/ResultTests.kt#L106) 272 | 273 | Similarly, `Validation` has the `mapFail` method, to apply a tranformation on the error case. Examples here 274 | [ValidationTests](https://github.com/lucapiccinelli/konad/blob/master/src/test/kotlin/io/konad/ValidationTests.kt#L101) 275 | 276 | In case of accumulated errors, both `errorTitle` and `mapFail` are applied to the entire list of errors. 277 | 278 | ### Result.field 279 | 280 | Since version 1.2.3, there exist an extension method `Result.field` that enables to add an error title in a type-safe manner. 281 | 282 | ```kotlin 283 | fun of(firstname: String, lastname: String): Result = ::User + 284 | checkNotEmpty(firstname).field(User::firstname) + 285 | checkNotEmpty(lastname).field(User::lastname) 286 | ``` 287 | 288 | In this example, `field` will add the name of the property as an error title, while also checking at compile time if the type 289 | of the property matches the type of the corresponding constructor parameter 290 | 291 | ## Extend with your own composable monads 292 | 293 | If you wish to implement your own monads and let them be composable through the `on` **Konad applicative builders**, then you need to implement the interfaces 294 | that are here: [Higher-kinded types](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/hkt/unaryHigherKindedTypes.kt) 295 | 296 | Actually, to let your type be composable, it is enough to implement the `ApplicativeFunctorKind` interface. 297 | 298 | Kotlin doesn't natively supports *Higher-kinded types*. To implement them, Konad is inspired on [how those are implemented in Arrow](https://arrow-kt.io/docs/patterns/glossary/#higher-kinds). 299 | That is why there is the need of `.result` and `.nullable` extension properties. 300 | --------------------------------------------------------------------------------