{"id":23675111,"url":"https://github.com/exoquery/decomat","last_synced_at":"2025-09-02T03:32:49.525Z","repository":{"id":167334828,"uuid":"642945613","full_name":"ExoQuery/DecoMat","owner":"ExoQuery","description":"Deconstructive Pattern-Matching for Kotlin","archived":false,"fork":false,"pushed_at":"2025-06-29T05:35:29.000Z","size":368,"stargazers_count":44,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-06-29T06:34:23.310Z","etag":null,"topics":["extraction","kotlin","kotlin-library","pattern-matching","scala"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ExoQuery.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,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null}},"created_at":"2023-05-19T17:58:12.000Z","updated_at":"2025-06-29T05:35:33.000Z","dependencies_parsed_at":"2023-12-26T05:29:21.448Z","dependency_job_id":"de635d68-293f-4e9d-abcc-9288609034e9","html_url":"https://github.com/ExoQuery/DecoMat","commit_stats":null,"previous_names":["exoquery/decomat"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/ExoQuery/DecoMat","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FDecoMat","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FDecoMat/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FDecoMat/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FDecoMat/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ExoQuery","download_url":"https://codeload.github.com/ExoQuery/DecoMat/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ExoQuery%2FDecoMat/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":273225171,"owners_count":25067236,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-09-02T02:00:09.530Z","response_time":77,"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":["extraction","kotlin","kotlin-library","pattern-matching","scala"],"created_at":"2024-12-29T13:56:46.117Z","updated_at":"2025-09-02T03:32:49.128Z","avatar_url":"https://github.com/ExoQuery.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Decomat\n\nScala-Style Deconstructive Pattern-Matching for Kotlin.\n\nDecomat is available on Maven Central. To use it, add the following to your `build.gradle.kts`:\n```\nimplementation(\"io.exoquery:decomat-core:0.0.2\")\nksp(\"io.exoquery:decomat-ksp:0.0.2\")\n```\n\n## Introduction\n\nDecomat is a library that gives Kotlin a way to do pattern-matching on ADTs (Algebraic Data Types) in a way \nthat is similar to Scala's pattern-matching. For example:\n\n```scala\ncase class Customer(name: Name, affiliate: Affiliate)\ncase class Name(first: String, last: String)\nsealed trait Affiliate\ncase class Partner(id: Int) extends Affiliate\ncase class Organization(name: String) extends Affiliate\n\nsomeone match {\n  case Customer(Name(first @ \"Joe\", last), Partner(id)) =\u003e func(first, last, id)\n  case Customer(Name(first @ \"Jack\", last), Organization(\"BigOrg\")) =\u003e func(first, last)\n}\n```\n\nSimilarly, in Kotlin with Decomat you can do this:\n```kotlin\n\n  bigListOfPeople.mapNotNull { p -\u003e\n    p.match(\n      case( Customer[FullName[Is(\"Joe\"), Is()], Partner[Is()]] )\n        .then { first, last, id -\u003e func(first, last, id) },\n    )\n  }\n\n```\n\n\n\n```kotlin\non(someone).match(\n  case( Customer[Name[Is(\"Joe\"), Is()], Partner[Is()]] )\n    .then { first, last, id -\u003e func(first, last, id) },\n  case( Customer[Name[Is(\"Jack\"), Is()], Organization[Is(\"BigOrg\")]] )\n    .then { first, last -\u003e func(first, last) }\n)\n```\n\nWhereas normally the following would be needed:\n```kotlin\nwhen(someone) {\n  is Customer -\u003e\n    if (someone.name.first == \"Joe\") {\n      when (val aff = someone.affiliate) {\n        is Partner -\u003e {\n          func(someone.name.first, someone.name.last, aff.id)\n        }\n        else -\u003e fail()\n      }\n    } else if (someone.name.first == \"Jack\") {\n      when (val aff = someone.affiliate) {\n        is Organization -\u003e {\n          if (aff.name == \"BigOrg\") {\n            func(someone.name.first, someone.name.last)\n          } else fail()\n        }\n        else -\u003e fail()\n      }\n    } else fail()\n}\n```\n\nDecomat is not a full replacement of Scala's pattern-matching, but it does have some of the same \nfeatures and usage patterns in a limited scope. The primary insight behind Decomat is that for in most of \nthe cases where Scala ADT pattern matching is used:\n\n* No more than two components need to be deconstructed (3 will be partially supported soon)\n* The deconstruction itself does not need to be more than two levels deep\n* The components that need to be deconstructed are usually known as the ADT case-classes are being written.\n* Frequently, other parts of the main object need to be checked during the pattern matching but they do not \n  need to be deconstructed. This can typically be done with a simple `if` statement (see `thenIfThis` below). \n\n## Tutorial\n\n#### 1. Build\nIn order to get started with Decomat, add the needed dependencies to your `build.gradle.kts`\nfile and enable KSP. Decomat relies on various extension methods that are generated by KSP.\n```\n// build.gradle.kts\nplugins {\n  ...\n  id(\"com.google.devtools.ksp\") version \"\u003cksp-version\u003e\"\n}\n\nimplementation(\"io.exoquery:decomat-core:\u003cversion\u003e\")\nksp(\"io.exoquery:decomat-ksp:\u003cversion\u003e\")\n```\n\n#### 2. Annotate\n\nThen:\n* Add the `@Matchable` annotation your class and `@Component` annotation to (up to two) constructor parameters.\n* Make the Data Class extend `HasProductClass\u003cYourDataClass\u003e`.\n* Add the `productComponents` field to your class and pass `this` and the component-fields into it.\n* Add an empty companion-object\n```kotlin\n@Matchable\ndata class Customer(@Component val name: Name, @Component val affiliate: Affiliate) {\n  override val productComponents = productComponentsOf(this, name, affiliate)\n  companion object {}\n}\n```\n\nFollow these steps for all other classes that you want to pattern match on, in the case above to `Name` and `Partner` as follows:\n\n```kotlin\n@Matchable\ndata class Name(@Component val first: String, @Component val last: String) {\n  override val productComponents = productComponentsOf(this, first, last)\n  companion object {}\n}\n\n@Matchable\ndata class Partner(@Component val id: Int) {\n  override val productComponents = productComponentsOf(this, id)\n  companion object {}\n}\n```\n\nThen use KSP to generate the needed extension methods, in IntelliJ this typically just means\nrunning the 'Rebuild Project' command. The extension-methods will be generated inside of your \nproject under `projectDir/build/generated/ksp/main/kotlin/`. They will\nbe placed in the same package as the `@Matchable` data classes.\n\n\u003e Note that ONLY the parameters that you actually want to deconstruct shuold be annotated with `@Component`\n\u003e and only 2 are supported. You can use the `thenIfThis` and `thenThis` methods to conveniently interact\n\u003e with non-component methods during filtration.\n\u003e There can be other non-component parameters in the constructor before, after, and \n\u003e in-between them:\n\u003e ```kotlin\n\u003e @Matchable\n\u003e data class Customer(\n\u003e   val something: String, \n\u003e   @Component val name: Name, \n\u003e   val somethingElse: String, \n\u003e   @Component val affiliate: Affiliate,\n\u003e   val yetSomethingElse: String\n\u003e ) { ... }\n\u003e ```\n\n#### 3. Use!\n\nThen you can use the `on` and `case` functions to pattern match on the ADTs and the `then` function to\nperform transformations.\n```kotlin\non(someone).match(\n  case( Customer[Name[Is(\"Joe\"), Is()], Partner(Is())] )\n    .then { first, last, id -\u003e func(first, last, id) },\n  // Other cases...\n}\n```\nNote that Scala also allows you to match a variable based on just a type. For example:\n```scala\nsomeone match {\n  case Customer(Name(first, last), partner: Partner) =\u003e func(first, last, part)\n}\n```\nIn Decomat, you can do using the the `Is` function with a type and empty parameter-list.\n```kotlin\non(someone).match(\n  case( Customer[Name[Is(), Is()], Is\u003cPartner\u003e()] )\n    // Note how since we are not deconstructing the Partner class anymore, the 3rd parameter\n    // switches from the `id: Int` type to the `partner: Partner` type.\n    .then { first, last, partner /*: Partner*/ -\u003e func(first, last, partner) },\n  // Other cases...\n)\n```\n\nThere are several other methods provided for pattern-matching convenience.\n\n##### thenIf\n\nThe `thenIf` method allows you to perform a transformation only if the predicate is true. This is similar\nto adding a `if` clause to a Scala pattern-match case. For example:\n\n```scala\nsomeone match {\n  case Customer(Name(first, last), Partner(id)) if (first == \"Joe\") =\u003e func(first, last, id)\n  ...\n}\n```\nIn Decomat, this would be done as follows:\n```kotlin\non(someone).match(\n  case( Customer[Name[Is(), Is()], Partner(Is())] )\n    .thenIf { first, last, id -\u003e first == \"Joe\" }\n    .then { first, last, id -\u003e func(first, last, id) },\n  // Other cases...\n)\n```\n\n##### thenIfThis\n\nIf you want to filter by a non `@Component` annoated field, you can use the `thenIfThis` method.\nThis method allows you to use the pattern-matched object as a reciever. For example:\n\n```kotlin\n@Matchable\ndata class Customer(val something: String, @Component val name: Name, @Component val affiliate: Affiliate) { ... }\n\non(something).match(\n  case( Customer[Name[Is(), Is()], Partner(Is())] )\n    .thenIfThis {{ first, last, id -\u003e\n      // Note that the first, last, and id properties are available here but you do not necessarily need to use them,\n      // since you can use the `this` keyword to refer to the `Customer` instance (also `this` can be omitted entirely).\n      something == \"something\"\n    }}\n    .then { name, affiliate -\u003e func(name, affiliate) },\n  // Other cases...\n)\n```\nHere we are using the `Customer` class as a reciever in the `thenIfThis` class above. The properties\n`first`, `last`, and `id` are also available to use if you need them.\n\n\u003e The actual signature of `thenIfThis` is: \n\u003e ```kotlin\n\u003e R.() -\u003e (component1, component2, ...) -\u003e Boolean\n\u003e ```\n\u003e That is why the double braches `{{ ... }}` are needed.\n\n##### thenThis\n\nIf you want to use any fields of the pattern-matched object that are not one of the components, you can use the `thenThis` method.\nThis method allows you to use the pattern-matched object as a reciever. For example:\n\n```kotlin\n@Matchable\ndata class Customer(val something: String, @Component val name: Name, @Component val affiliate: Affiliate) {\n  val somethingElse = \"somethingElse\"\n  ...\n}\n\non(something).match(\n  case( Customer[Name[Is(), Is()], Partner(Is())] )\n    .thenThis { first, last, id -\u003e\n      // You can use the `this` keyword to refer to the `Customer` instance (also `this` can be omitted entirely).\n      // (the components first, last, id are also available here for convenience)\n      this.something + this.somethingElse\n    }\n  // Other cases...\n)\n```\n\n##### Is\n\nThe `Is(...)` pattern is use to match the innermost patterns. It can be use to match by value, or by type.\n\nMatching by value:\n```kotlin\n```kotlin\n// Match only when affiliate is a Partner with id 123\non(something).match(\n  case( Customer[..., Is(Partner(123))] ).then { ... },\n  // Other cases...\n)\n```\n\nMatching by type:\n```kotlin\n// Match only when affiliate is of the type: Partner\non(something).match(\n  case( Customer[..., Is\u003cPartner\u003e] ).then { ... },\n  // Other cases...\n)\n```\n\nIt is also possible to use the `Is` pattern to match by a custom predicate. Use this for more complex pattern\nmatching but be warned that it may be less performant than the other methods because the predicate does not inline.\n```kotlin\non(something).match(\n  case( Customer[..., Is\u003cPartner\u003e { p -\u003e (p.id == 123 || p.id == 456) }] ).then { ... },\n  // Other cases...\n)\n```\n\nYou can also define custom Is-variants based on predicates for example:\n```kotlin\ndef isPartnerWithIds(vararg ids: Int) = IsIs\u003cPartner\u003e { p -\u003e p is Partner \u0026\u0026 ids.contains(p.id) }\non(something).match(\n  case( Customer[..., isPartnerWithIds(123, 456)] ).then { ... },\n  // Other cases...\n)\n```\n\nNote that in many cases, you can use the `thenIf` method instead of the `Is { ... }` predicate function which\ndoes inline leading to better performance.\n```kotlin\non(something).match(\n  case( Customer[..., Is\u003cPartner\u003e() )\n    // This will be inlined\n    .thenIf { _, aff -\u003e aff is Partner \u0026\u0026 (aff.id == 123 || aff.id == 456) }\n    .then { ... },\n  // Other cases...\n)\n```\n\n## Custom Patterns\n\nOne extremely powerful feature of Scala pattern-matching is that one can use custom patterns in a composable manner.\nFor example:\n```scala\n// Create a Data model\ncase class Person(name: Name, age: Int)\nsealed trait Name\ncase class SimpleName(first: String, last: String) extends Name\ncase class FullName(first: String, middle: String, last: String) extends Name\n\n// Create a custom pattern\nobject FirstLast {\n  def unapply(name: Name): Option[(String, String)] = name match {\n    case SimpleName(first, last) =\u003e Some(first, last)\n    case FullName(first, _, last) =\u003e Some(first, last)\n    case _ =\u003e None\n  }\n}\n\n// Now we can use the pattern to match and extract custom data\nval p: Person = ...\np match {\n  case Person(FirstLast(\"Joe\", last), age) =\u003e ...\n}\n```\nSimilarly, Decomat allows you to create custom patterns. For example:\n```kotlin\n// First Create our data model\n@Matchable\ndata class Person(@Component val name: Name, @Component val age: Int): HasProductClass\u003cPerson\u003e {\n  override val productComponents = ProductClass2(this, name, age)\n  companion object { }\n}\nsealed interface Name\ndata class SimpleName(val first: String, val last: String): Name\ndata class FullName(val first: String, val middle: String, val last: String): Name\n\n// Then create our custom pattern matcher. Use the customPattern1 or customPattern2 functions to create the custom pattern.\nobject FirstLast {\n  operator fun get(first: Pattern0\u003cString\u003e, last: Pattern0\u003cString\u003e) =\n    customPattern2(first, last) { it: Name -\u003e\n      when(it) {\n        is SimpleName -\u003e first.matches(it.first) \u0026\u0026 last.matches(it.last)\n        is FullName -\u003e first.matches(it.first) \u0026\u0026 last.matches(it.last)\n        else -\u003e false\n      }\n    }\n}\n\n// Then use the `FirstLast` custom pattern to match and extract data\nval p: Person = ...\nval out =\n  on(p).match(\n    case(Person[FirstLast[Is(\"Joe\"), Is()], Is()]).then { (first, last), age -\u003e ... }\n  )\n```\n\n\n\nNote that when Scala pattern matching clauses get complex, it is common to use pattern matching itself in order to deconstruct\npatterns into smaller patterns. That means that if we make `SimpleName` and `FullName` matchable, we can\nuse them with Decomat's matching instead of Kotlin `when` statement. This gives us more versatility.\nFor example:\n```kotlin\n// Annotate SimpleName and FullName as @Matchable in additionl to `Person`\n@Matchable\ndata class Person(@Component val name: Name, @Component val age: Int): HasProductClass\u003cPerson\u003e {\n  override val productComponents = ProductClass2(this, name, age)\n  companion object { }\n}\nsealed interface Name\n@Matchable\ndata class SimpleName(@Component val first: String, @Component val last: String): Name, HasProductClass\u003cSimpleName\u003e {\n  override val productComponents = ProductClass2(this, first, last)\n  companion object { }\n}\n@Matchable\ndata class FullName(@Component val first: String, val middle: String, @Component val last: String): Name, HasProductClass\u003cFullName\u003e {\n  override val productComponents = ProductClass3(this, first, middle, last)\n  companion object { }\n}\n\n// Then create our custom pattern matcher which itself uses on/match functions:\nobject FirstLast {\n  operator fun get(first: Pattern0\u003cString\u003e, last: Pattern0\u003cString\u003e) =\n    customPattern2(first, last) { it: Name -\u003e\n      on(it).match(\n        case(FullName[Is(), Is()]).then { first, last -\u003e Components2(first, last) },\n        case(SimpleName[Is(), Is()]).then { first, last -\u003e Components2(first, last) }\n      )\n    }\n}\n\n// Then use the `FirstLast` custom pattern to match and extract data the same as before...\nval p: Person = ...\nval out =\n  on(p).match(\n    case(Person[FirstLast[Is(\"Joe\"), Is()], Is()]).then { (first, last), age -\u003e ... }\n  )\n```\n\nThis latter approach is particularly useful when you want the custom pattern matching function itself to have\ncomplex nested conditional logic. For example:\n```kotlin\n// Match all full-names where the first-name is \"Joe\" or \"Jack\"\n// Match all simple-names where the last-name is \"Bloggs\" and \"Roogs\"\nobject FirstLast {\n  operator fun get(first: Pattern0\u003cString\u003e, last: Pattern0\u003cString\u003e) =\n    customPattern2(first, last) { it: Name -\u003e\n      on(it).match(\n        case(FullName[Is { it == \"Joe\" || it == \"Jack\" }, Is()])\n          .then { first, last -\u003e Components2(first, last) },\n        case(SimpleName[Is(), Is { it == \"Bloggs\" || it == \"Roogs\" }])\n          .then { first, last -\u003e Components2(first, last) }\n      )\n    }\n}\n```\n\n## ADTs with Type Parameters (i.e. GADTs)\n\nDecomat supports ADTs with type parameters but they are not used in the Pattern-components. Instead,\nthey are converted into start-projections. This is because typing all of the parameters would make the\nmatching highly restrictive. (Also, type-parameters cannot be used as part of the pattern-matching\ndue to type-erasure.)\n\nFor example:\n```kotlin\n@Metadata\nsealed interface Query\u003cT\u003e\ndata class Map\u003cT, R\u003e(@Component val head: Query\u003cT\u003e, @Component val body: Query\u003cR\u003e): Query\u003cR\u003e {\n  // ...\n}\ndata class Entity\u003cT\u003e(@Component val value: T): Query\u003cT\u003e {\n  fun \u003cR\u003e someField(getter: () -\u003e R): Query\u003cR\u003e = Property(this, getter())\n  // ...\n}\n```\n\nThe `Query` interface must be up-casted into into a star-projection when it is used in a match.\n```kotlin\nval query: Query\u003cSomething\u003e = ...\non(query as Query\u003c*\u003e).match(\n  case( Map[Is(), Is()] )\n    .then { head: Query\u003c*\u003e, body: Query\u003c*\u003e -\u003e func(head, body) },\n  case( Entity[Is()] )\n    .then { value: Entity\u003c*\u003e -\u003e func(value) },\n  // Other cases...\n)\n```\nNote how the `head` and `body` elements are star projections instead of the origin types?\nThis is done so that the `Map` case can match any `Query` type, otherwise the matching logic would be too restrictive.\n(E.g. it would be difficult to deduce the type of the `head` and `body` elements causing the generated code to be incorrect)\n\nIf you want to experiment with fully-typed ADT-components nonetheless, use `@Matchable(simplifyTypes = false)`.\n\n## Changing the Annotation Name\n\nKotlin allows changing an import name using the `import ... as ...` syntax. This can be used to change the\n`@Matchable` annotation name to something else, however due to issue [#783](https://github.com/google/ksp/issues/783) it is not possible to genenerically\ndetect this change inside of a KSP processor. Therefore, if you change the annotation name, you must also\nadd the following setting to your `build.gradle.kts` file:\n```kotlin\n// build.gradle.kts\nksp {\n    arg(\"matchableName\", \"Mat\")\n    arg(\"componentName\", \"Slot\")\n}\n```\nThen rename the `@Matchable` annotation to `@Mat` and the `@Component` annotation to `@Slot`\nin the import:\n```kotlin\nimport io.decomat.Matchable as Mat\nimport io.decomat.Component as Slot\n\n// Then use the annotations as follows:\n@Mat\ndata class Person(@Slot val firstName: String, @Slot val lastName: String) {\n  override val productComponents = productComponentsOf(this, firstName, lastName)\n  companion object {}\n}\n```\n\n\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoquery%2Fdecomat","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fexoquery%2Fdecomat","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fexoquery%2Fdecomat/lists"}