{"id":41054059,"url":"https://github.com/lucapiccinelli/konad","last_synced_at":"2026-01-22T11:33:53.882Z","repository":{"id":45715958,"uuid":"319372160","full_name":"lucapiccinelli/konad","owner":"lucapiccinelli","description":"Monads composition API that just works. For OOP developers","archived":false,"fork":false,"pushed_at":"2022-04-14T05:36:12.000Z","size":607,"stargazers_count":64,"open_issues_count":7,"forks_count":5,"subscribers_count":3,"default_branch":"master","last_synced_at":"2025-08-03T22:10:46.854Z","etag":null,"topics":["applicative-functors","arrow","composition","functors","kotlin","kotlin-monads","monads","nullables"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/lucapiccinelli.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2020-12-07T16:05:18.000Z","updated_at":"2025-07-05T14:22:42.000Z","dependencies_parsed_at":"2022-08-04T23:00:40.245Z","dependency_job_id":null,"html_url":"https://github.com/lucapiccinelli/konad","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/lucapiccinelli/konad","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucapiccinelli%2Fkonad","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucapiccinelli%2Fkonad/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucapiccinelli%2Fkonad/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucapiccinelli%2Fkonad/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/lucapiccinelli","download_url":"https://codeload.github.com/lucapiccinelli/konad/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/lucapiccinelli%2Fkonad/sbom","scorecard":{"id":601769,"data":{"date":"2025-08-11","repo":{"name":"github.com/lucapiccinelli/konad","commit":"ad9564cc6767ca267f481339ab27dac3574cb096"},"scorecard":{"version":"v5.2.1-40-gf6ed084d","commit":"f6ed084d17c9236477efd66e5b258b9d4cc7b389"},"score":3.3,"checks":[{"name":"Code-Review","score":0,"reason":"Found 0/30 approved changesets -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project requires human code review before pull requests (aka merge requests) are merged.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#code-review"}},{"name":"Dangerous-Workflow","score":10,"reason":"no dangerous workflow patterns detected","details":null,"documentation":{"short":"Determines if the project's GitHub Action workflows avoid dangerous patterns.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#dangerous-workflow"}},{"name":"Token-Permissions","score":0,"reason":"detected GitHub workflow tokens with excessive permissions","details":["Warn: no topLevel permission defined: .github/workflows/build-and-test.yml:1","Warn: no topLevel permission defined: .github/workflows/maven-central-publish.yml:1","Info: no jobLevel write permissions found"],"documentation":{"short":"Determines if the project's workflows follow the principle of least privilege.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#token-permissions"}},{"name":"Binary-Artifacts","score":9,"reason":"binaries present in source code","details":["Warn: binary detected: .mvn/wrapper/maven-wrapper.jar:1"],"documentation":{"short":"Determines if the project has generated executable (binary) artifacts in the source repository.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#binary-artifacts"}},{"name":"Maintained","score":0,"reason":"0 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 0","details":null,"documentation":{"short":"Determines if the project is \"actively maintained\".","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#maintained"}},{"name":"SAST","score":0,"reason":"no SAST tool detected","details":["Warn: no pull requests merged into dev branch"],"documentation":{"short":"Determines if the project uses static code analysis.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#sast"}},{"name":"Pinned-Dependencies","score":0,"reason":"dependency not pinned by hash detected -- score normalized to 0","details":["Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build-and-test.yml:18: update your workflow using https://app.stepsecurity.io/secureworkflow/lucapiccinelli/konad/build-and-test.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build-and-test.yml:20: update your workflow using https://app.stepsecurity.io/secureworkflow/lucapiccinelli/konad/build-and-test.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/build-and-test.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/lucapiccinelli/konad/build-and-test.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/maven-central-publish.yml:22: update your workflow using https://app.stepsecurity.io/secureworkflow/lucapiccinelli/konad/maven-central-publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/maven-central-publish.yml:24: update your workflow using https://app.stepsecurity.io/secureworkflow/lucapiccinelli/konad/maven-central-publish.yml/master?enable=pin","Warn: GitHub-owned GitHubAction not pinned by hash: .github/workflows/maven-central-publish.yml:37: update your workflow using https://app.stepsecurity.io/secureworkflow/lucapiccinelli/konad/maven-central-publish.yml/master?enable=pin","Info:   0 out of   6 GitHub-owned GitHubAction dependencies pinned"],"documentation":{"short":"Determines if the project has declared and pinned the dependencies of its build process.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#pinned-dependencies"}},{"name":"CII-Best-Practices","score":0,"reason":"no effort to earn an OpenSSF best practices badge detected","details":null,"documentation":{"short":"Determines if the project has an OpenSSF (formerly CII) Best Practices Badge.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#cii-best-practices"}},{"name":"Security-Policy","score":0,"reason":"security policy file not detected","details":["Warn: no security policy file detected","Warn: no security file to analyze","Warn: no security file to analyze","Warn: no security file to analyze"],"documentation":{"short":"Determines if the project has published a security policy.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#security-policy"}},{"name":"Fuzzing","score":0,"reason":"project is not fuzzed","details":["Warn: no fuzzer integrations found"],"documentation":{"short":"Determines if the project uses fuzzing.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#fuzzing"}},{"name":"License","score":10,"reason":"license file detected","details":["Info: project has a license file: LICENSE:0","Info: FSF or OSI recognized license: MIT License: LICENSE:0"],"documentation":{"short":"Determines if the project has defined a license.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#license"}},{"name":"Packaging","score":-1,"reason":"packaging workflow not detected","details":["Warn: no GitHub/GitLab publishing workflow detected."],"documentation":{"short":"Determines if the project is published as a package that others can easily download, install, easily update, and uninstall.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#packaging"}},{"name":"Signed-Releases","score":-1,"reason":"no releases found","details":null,"documentation":{"short":"Determines if the project cryptographically signs release artifacts.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#signed-releases"}},{"name":"Branch-Protection","score":0,"reason":"branch protection not enabled on development/release branches","details":["Warn: branch protection not enabled for branch 'master'"],"documentation":{"short":"Determines if the default and release branches are protected with GitHub's branch protection settings.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#branch-protection"}},{"name":"Vulnerabilities","score":10,"reason":"0 existing vulnerabilities detected","details":null,"documentation":{"short":"Determines if the project has open, known unfixed vulnerabilities.","url":"https://github.com/ossf/scorecard/blob/f6ed084d17c9236477efd66e5b258b9d4cc7b389/docs/checks.md#vulnerabilities"}}]},"last_synced_at":"2025-08-21T00:37:13.534Z","repository_id":45715958,"created_at":"2025-08-21T00:37:13.534Z","updated_at":"2025-08-21T00:37:13.534Z"},"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":28662133,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-01-22T01:17:37.254Z","status":"online","status_checked_at":"2026-01-22T02:00:07.137Z","response_time":144,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["applicative-functors","arrow","composition","functors","kotlin","kotlin-monads","monads","nullables"],"created_at":"2026-01-22T11:33:53.258Z","updated_at":"2026-01-22T11:33:53.873Z","avatar_url":"https://github.com/lucapiccinelli.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"\u003cimg src=\"assets/konad-logo.png\" alt=\"Konad\" width=\"600\"/\u003e\n\n---\n\n[![Build and Test](https://github.com/lucapiccinelli/konad/workflows/build-and-test/badge.svg)](https://github.com/lucapiccinelli/konad/actions)\n[![Maven Central](http://img.shields.io/maven-central/v/io.github.lucapiccinelli/konad.svg)](https://search.maven.org/search?q=a:konad)\n\nMonads composition API that just works. For OOP developers. It is well suited to compose also Kotlin nullables.\n\n## Why another functional library for Kotlin?\n\nI 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.\n\nAlso, 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.\n\n## Konad to the OOP rescue\n\nHere it comes Konad. It has only three classes:\n - [**Result**](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/Result.kt): can be `Result.Ok` or `Result.Errors`.\n - [**Validation**](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/Validation.kt): can be `Validation.Success` or `Validation.Fail`.\n - [**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)\n \nThese are **monads** and **applicative functors**, so they implement the usual `map`, `flatMap` and `ap` methods. \n\nKonad exists **with the only purpose** to let you easily compose these three classes.\n\nAdvanced use-cases examples are described here:\n - [Nice Kotlin Nullables and Where to Find Them](https://medium.com/swlh/nice-kotlin-nullables-and-where-to-find-them-85d8de481e41?source=friends_link\u0026sk=992c123a45421d26a6e21637e4ecdfcd)\n - [Type-safe Domain Modeling in Kotlin](https://betterprogramming.pub/type-safe-domain-modeling-in-kotlin-425ddbc73732?source=friends_link\u0026sk=2fedd10125b31cf7ca378878de4b3491)\n\n## Getting started\n\nAdd the dependency\n\n#### Maven\nadd in pom.xml\n```xml\n\u003cdependency\u003e\n    \u003cgroupId\u003eio.github.lucapiccinelli\u003c/groupId\u003e\n    \u003cartifactId\u003ekonad\u003c/artifactId\u003e\n    \u003cversion\u003e1.2.6\u003c/version\u003e\n\u003c/dependency\u003e\n```\n\n#### Gradle\nadd in build.gradle\n```groovy\ndependencies {\n    implementation \"io.github.lucapiccinelli:konad:1.2.6\"\n}\n```\n\n## Usage example\n\n*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)\nand to [ResultTests.kt](https://github.com/lucapiccinelli/konad/blob/master/src/test/kotlin/io/konad/ResultTests.kt)*\n\nLet'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.\n\n```kotlin\n\ndata class User(val username: String, val email: Email, val phoneNumber: PhoneNumber, val firstname: String)\n\ndata class Email private constructor (val value: String) {\n    companion object{\n        fun of(emailValue: String): Result\u003cEmail\u003e = if (Regex(EMAIL_REGEX).matches(emailValue))\n            Email(emailValue).ok()\n            else \"$emailValue doesn't match an email format\".error()\n    }\n}\n\ndata class PhoneNumber private constructor(val value: String){\n    companion object {\n        fun of(phoneNumberValue: String): Result\u003cPhoneNumber\u003e = if(Regex(PHONENUMBER_REGEX).matches(phoneNumberValue))\n            PhoneNumber(phoneNumberValue).ok()\n            else \"$phoneNumberValue should match a valid phone number, but it doesn't\".error()\n    }\n}\n\n```\n\n`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\u003cEmail\u003e/Result\u003cPhoneNumber\u003e`. \n\nIn order to compose them and get a `Result\u003cUser\u003e` you have to do the following\n\n```kotlin\n\n    val userResult: Result\u003cUser\u003e = ::User +\n        \"foo.bar\" +\n        Email.of(\"foo.bar\") + // This email is invalid -\u003e returns Result.Errors\n        PhoneNumber.of(\"xxx\") + // This phone number is invalid -\u003e returns Result.Errors\n        \"Foo\"\n    \n    when(userResult){\n        is Result.Ok -\u003e userResult.toString()\n        is Result.Errors -\u003e userResult.toList().joinToString(\" - \")\n    }.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\n    \n    // or\n    \n    userResult\n       .map{ user -\u003e user.toString() }\n       .ifError { errors -\u003e errors.description(errorDescriptionsSeparator = \" - \") }\n       .run(::println)\n\n```\n\n## The pure functional style.\n\nComposition happens thanks to concepts named **functors** and **applicative Functors**.\n\nI 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)).\nHowever, 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))\n\n```kotlin\n\n    val user: Result\u003cUser\u003e = ::User.curry()\n        .apply(\"foo.bar\")\n        .map(Email.of(\"foo.bar\")) \n        .ap(PhoneNumber.of(\"xxx\"))\n        .pure(\"Foo\")\n        .result\n\n```\n\n\u003ca name=\"maybe\"\u003e\u003c/a\u003e\n## Maybe\n\n`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)). \nIn this way `on`, can be used to compose nullables. \n\nIts constructor is private because **you should avoid using it** in order to express *optionality*. Kotlin nullability is perfect for that purpose.\n\n### How to compose nullables\nIf ever you tried to compose *nullables* in Kotlin, then probably you ended up having something like the following\n\n```kotlin\n\nval foo: Int? = 1\nval bar: String? = \"2\"\nval baz: Float? = 3.0f\n\nfun useThem(x: Int, y: String, z: Float): Int = x + y.toInt() + z.toInt()\n\nval result1: Int? = foo\n    ?.let { bar\n    ?.let { baz\n    ?.let { useThem(foo, bar, baz) } } }\n\n// or\n\nval result2: Int? = if(foo != null \u0026\u0026 bar != null \u0026\u0026 baz != null) \n    useThem(foo, bar, baz) \n    else null\n\n```\n\nThis is not very clean. And it gets even worse if would like to give an error message when a `null` happens.\n\nUsing Konad, nullables can be composed as follows \n\n```kotlin\n\nval result: Int? = ::useThem + foo + bar + baz\n\n```\n\nor you can choose to give an explanatory message when something is `null`\n\n```kotlin\n\nval result: Result\u003cInt\u003e = ::useThem +\n    foo.ifNull(\"Foo should not be null\") +\n    bar.ifNull(\"Bar should not be null\") +\n    baz.ifNull(\"Baz should not be null\")\n\n```\n\n\u003ca name=\"validation\"\u003e\u003c/a\u003e\n## Validation\n\n`Validation\u003cA, B\u003e` is like an `Either` monad, but with the left case accumulation. It is similar to `Result\u003cT\u003e` but instead of fixing the error case as a string description, it lets you\ndecide how you represent the error. Example:\n\n```kotlin\n\nsealed class ResourceError {\n    data class BadInput(val description: String) : ResourceError()\n    object NotFound : ResourceError()\n    object Forbidden : ResourceError()\n}\n\nfun readUser(id: String): Validation\u003cResourceError, User\u003e =\n    if (id.isBlank()) ResourceError.BadInput(\"id should not be blank\").fail()\n    else repository.findById(id)?.success() ?: ResourceError.NotFound.fail()\n\nreadUser(\"xxx\")\n    .map { user: User -\u003e println(user) }\n    .ifFail { failures: Collection\u003cResourceError\u003e -\u003e println(failures) }\n\n```\n\n## Flatten\n\nWhat if you have a `List\u003cResult\u003cT\u003e\u003e` and you want a `Result\u003cList\u003cT\u003e\u003e`? Then use `flatten` extension method.\n\n```kotlin\n\nval r: Result\u003cCollection\u003cInt\u003e\u003e = listOf(Result.Ok(1), Result.Ok(2)).flatten()\n\n```\n\nErrors get cumulated as usual\n\n```kotlin\n val r: Result\u003cCollection\u003cInt\u003e\u003e = listOf(Result.Errors(\"error1\"), Result.Ok(1), Result.Errors(\"error2\"))\n    .flatten()\n\nwhen(r){\n    is Result.Ok -\u003e r.value.toString()\n    is Result.Errors -\u003e r.description\n}.run(::println) // will print error1 - error2\n```\n\nObviously it works also on nullables: `Collection\u003cT?\u003e -\u003e Collection\u003cT\u003e?`\n\n```kotlin\nval flattened = setOf(\"a\", null, \"c\").flatten()\n\nflattened shouldBe null\n```\n\nand on Validation\n\n```kotlin\nval v: Validation\u003cString, Collection\u003cInt\u003e\u003e = listOf(\"error1\".fail(), 1.success(), \"error2\".fail()).flatten()\n```\n\n## Error enrichment\n\nSometime you need to add some details on an error, or to transform it. `Result` and `Validation` monads have convenience method for this case.\nExamples:\n\n```kotlin\nfun checkNotEmpty(value: String) = if(value.isBlank()) \"value should not be blank\".error() else value.ok()\n\ndata class User private constructor(val firstName: String, val lastname: String){\n    companion object{\n        fun of(firstname: String, lastname: String): Result\u003cUser\u003e = ::User.curry()\n            .on(checkNotEmpty(firstname))\n            .on(checkNotEmpty(lastname))\n            .result\n    }\n}\n\n```\n\nin 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\nable to distinct which `value should not be empty`. To fix, there is the method `Result::errorTitle`\n\n```kotlin\n\nfun of(firstname: String, lastname: String): Result\u003cUser\u003e = ::User.curry()\n    .on(checkNotEmpty(firstname).errorTitle(\"firstname\"))\n    .on(checkNotEmpty(lastname).errorTitle(\"lastname\"))\n    .result\n\n```\n\nYou can find a more detailed specification here:\n[ResultTests](https://github.com/lucapiccinelli/konad/blob/master/src/test/kotlin/io/konad/ResultTests.kt#L106)\n\nSimilarly, `Validation` has the `mapFail` method, to apply a tranformation on the error case. Examples here\n[ValidationTests](https://github.com/lucapiccinelli/konad/blob/master/src/test/kotlin/io/konad/ValidationTests.kt#L101)\n\nIn case of accumulated errors, both `errorTitle` and `mapFail` are applied to the entire list of errors.\n\n### Result\u003cT\u003e.field\n\nSince version 1.2.3, there exist an extension method `Result\u003cT\u003e.field` that enables to add an error title in a type-safe manner.\n\n```kotlin\nfun of(firstname: String, lastname: String): Result\u003cUser\u003e = ::User +\n    checkNotEmpty(firstname).field(User::firstname) + \n    checkNotEmpty(lastname).field(User::lastname)\n```\n\nIn this example, `field` will add the name of the property as an error title, while also checking at compile time if the type \nof the property matches the type of the corresponding constructor parameter \n\n## Extend with your own composable monads\n\nIf 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\nthat are here: [Higher-kinded types](https://github.com/lucapiccinelli/konad/blob/master/src/main/kotlin/io/konad/hkt/unaryHigherKindedTypes.kt)\n\nActually, to let your type be composable, it is enough to implement the `ApplicativeFunctorKind` interface.\n\nKotlin 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).\nThat is why there is the need of `.result` and `.nullable` extension properties.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flucapiccinelli%2Fkonad","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flucapiccinelli%2Fkonad","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flucapiccinelli%2Fkonad/lists"}