{"id":17036695,"url":"https://github.com/marick/structural-typing","last_synced_at":"2025-04-05T20:09:01.753Z","repository":{"id":32208672,"uuid":"35782446","full_name":"marick/structural-typing","owner":"marick","description":"Structural typing for Clojure, somewhat inspired by Elm. Tailored to \"flow-style\" programming, where complex structures flow through a series of functions, each of which makes a smallish change. Can also be used in testing tools and the like that need to describe how a nested structure differs from a description.","archived":false,"fork":false,"pushed_at":"2016-09-05T20:27:33.000Z","size":1229,"stargazers_count":245,"open_issues_count":1,"forks_count":9,"subscribers_count":12,"default_branch":"master","last_synced_at":"2025-03-29T19:07:47.320Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":"","language":"Clojure","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/marick.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","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":"2015-05-17T20:49:42.000Z","updated_at":"2025-03-24T07:59:02.000Z","dependencies_parsed_at":"2022-09-19T05:31:56.452Z","dependency_job_id":null,"html_url":"https://github.com/marick/structural-typing","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marick%2Fstructural-typing","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marick%2Fstructural-typing/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marick%2Fstructural-typing/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/marick%2Fstructural-typing/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/marick","download_url":"https://codeload.github.com/marick/structural-typing/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247393572,"owners_count":20931813,"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","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":[],"created_at":"2024-10-14T08:51:25.657Z","updated_at":"2025-04-05T20:09:01.731Z","avatar_url":"https://github.com/marick.png","language":"Clojure","readme":"Available via [clojars](https://clojars.org/marick/structural-typing) for Clojure 1.7+  \nFor lein: [marick/structural-typing \"2.0.5\"]        \nLicense: [MIT](http://opensource.org/licenses/MIT)        \n[API docs](http://marick.github.io/structural-typing/)       \n[Wiki docs](https://github.com/marick/structural-typing/wiki)\n\n[![Build Status](https://travis-ci.org/marick/structural-typing.png?branch=master)](https://travis-ci.org/marick/structural-typing)\n\n**[Semantic versioning](http://semver.org)**: Changes to the text of\nthe default error explanations will not trigger a major version\nbump. Changes in the number of messages won't either, provided the new\nset contains the same information. (In less-common cases, the library\nsays almost the same thing twice.)\n\n**Updating to 2.0**: See the [change log](https://github.com/marick/structural-typing/blob/master/CHANGELOG.md). It is extremely unlikely that you'll have to change client code, unless you used functions that were explicitly deprecated.\n\n# structural-typing\n\nThis library provides two services:\n\n1. Good error messages when checking the correctness of structures\n   (in, for example, tests).\n\n2. A way to define\n   [structural types](https://en.wikipedia.org/wiki/Structural_type_system)\n   that are checked at runtime. Structural typing differs from the\n   more common\n   [nominal typing](https://en.wikipedia.org/wiki/Nominal_type_system)\n   in that, for example, two records with different `defrecord` names\n   are of the same type if they have the same keys.\n\n\n## Good error messages are ever so helpful\n\nThe core function is `built-like`. When given a [type description](https://github.com/marick/structural-typing/wiki/Glossary#condensed-type-description) and a correct value, its return value is\nthe input value:\n\n```clojure\nuser=\u003e (use 'structural-typing.type)\nuser=\u003e (built-like string? \"foo\")\n\"foo\"\n```\n\nIf the value is incorrect, an error message is printed to standard output and `nil` is returned:\n\n```clojure\nuser=\u003e (built-like string? :keyword)\nValue should be `string?`; it is `:keyword`\n=\u003e nil\n```\n\n(A later section justifies the `nil` return. Also,\nprinting a message to standard output is almost never what you want in\nproduction, so it can be overridden. It's useful for showing things in\nthe repl, though.)\n\nNotice that I've declared the type to be \"string\" by using the Clojure\nfunction `string?`. You can use any function. For example:\n\n```clojure\n(defn long? [x] (\u003e (count x) 10))\n\nuser=\u003e (built-like [long? string?] \"foo\")\nValue should be `long?`; it is `\"foo\"`\n=\u003e nil\n```\n\nThe library goes to considerable trouble to print function names\nnicely. It also avoids unhelpful annoyances like `long?` blowing up\nwhen given an integer:\n\n```clojure\nuser=\u003e (built-like [long? string?] 5)\nValue should be `long?`; it is `5`\nValue should be `string?`; it is `5`\n=\u003e nil\n```\n\nFor better or worse, Clojure is one of those languages that treats `nil` as a member of\nevery type. By default, this library follows that convention:\n\n```clojure\nuser=\u003e (built-like string? nil)\n=\u003e nil\n```\n\n(It's the lack of an error message that distinguishes this `nil` result from a signal of an error.)\n\nYou can override this default if you like:\n\n```clojure\nuser=\u003e (built-like [string? reject-nil] nil)\nThe whole value should not be `nil`\n=\u003e nil\n```\n\n### Structures and paths\n\nAs shown above, `built-like` can be used with basic types like strings,\nbut that's unusual. The type more usually describes a collection. For example, here's a claim\nthat all values in a collection are even:\n\n```clojure\nuser=\u003e (built-like {[ALL] even?} [1 2 3 nil])\n[0] should be `even?`; it is `1`\n[2] should be `even?`; it is `3`\n```\n\nNotice that the `nil` was not rejected. To do that, you'd have to be explicit:\n\n```clojure\nuser=\u003e (built-like {[ALL] [even? reject-nil]} [1 2 3 nil])\n[0] should be `even?`; it is `1`\n[2] should be `even?`; it is `3`\n[3] has a `nil` value\n=\u003e nil\n```\n\nItems (like `[ALL]`) in the left-hand (key) positions of the map are\n[paths](https://github.com/marick/structural-typing/wiki/Glossary#path)\ninto the structure. For example, suppose each element of a collection\nis a map. In that map, `:a` should have an even value, and `:b` should\nhave an odd value. The type description would have two paths:\n\n```clojure\nuser=\u003e (def my-type {[ALL :a] even?\n                     [ALL :b] odd?})\nuser=\u003e (built-like my-type [{:a 0 :b 0} {:a 1 :b 1}])\n[0 :b] should be `odd?`; it is `0`\n[1 :a] should be `even?`; it is `1`\n=\u003e nil\n```\n\nAs before, `nil` values would be silently accepted. Moreover, missing\nvalues are, too. (The justification for that decision is below.) For example:\n\n```clojure\nuser=\u003e (built-like my-type [{:a 0}])\n=\u003e [{:a 0}]    ;; how is this ok?! There's no :b!\n```\n\n`reject-nil` can be used in such a case, relying on the fact that `(:a {})` is `nil`:\n\n```clojure\nuser=\u003e (def my-type {[ALL :a] [even? reject-nil]\n                     [ALL :b] [odd? reject-nil]})\nuser=\u003e (built-like my-type [{:a 0}])\n[0 :b] has a `nil` value\n=\u003e nil\n```\n\nHowever, the concept of \"missing\" is really different than \"present\nbut `nil`\", so you can more specifically reject missing values:\n\n```clojure\nuser=\u003e (def my-type {[ALL :a] [even? reject-missing]\n                     [ALL :b] [odd? reject-missing]})\nuser=\u003e (built-like my-type [{:a nil}]) \n[0 :b] does not exist\n=\u003e nil\n```\n\nNotice that `built-like` accepted the explicit `nil`. If you want to\nreject both `nil` and missing values, use `required-path`:\n\n```clojure\nuser=\u003e (def my-type {[ALL :a] [even? required-path]\n                     [ALL :b] [odd? required-path]})\nuser=\u003e (built-like my-type [{:a nil}])\n[0 :a] has a `nil` value\n[0 :b] does not exist\n=\u003e nil\n```\n\nNote: You'll often want all or most paths to be required, and it would\nbe tedious and error-prone to type `required-path` on every right-hand\nside in the type description. See [quickly requiring certain paths](https://github.com/marick/structural-typing#quickly-requiring-certain-paths) below, the wiki, and the API documentation.\n\n### Sometimes I want to be specific about expected values\n\nIn testing, you'll often want to say not that a\nvalue is a string, but that it's a *specific* string. That can be done\nlike this:\n\n```clojure\nuser=\u003e (require '[structural-typing.preds :as pred])\nuser=\u003e (built-like {[:a :b] (pred/exactly 3)}\n                   {:a {:b 4}})\n[:a :b] should be exactly `3`; it is `4`\n=\u003e nil\n```\n\nYou may not want exact comparison. There are a variety of useful\nrelated functions in the `structural-typing.preds` namespace.  For\nexample, you can constrain the possible inputs to an API by insisting\nthat the version number match a regular expression:\n\n```clojure\nuser=\u003e (def version-check {[:metadata :version] (pred/matches #\"1.?\")})\nuser=\u003e (built-like version-check {:metadata {:version \"2.3\"}\n                                  :real-data 5})\n[:metadata :version] should match #\"1.?\"; it is \"2.3\"\n=\u003e nil\n```\n\n\n#### A bit of path shorthand and a tad of terminology\n\nIn its\n[canonical](https://github.com/marick/structural-typing/wiki/Glossary#canonical-type-description)\nform, a type description is a map from paths to a vector of\n\"[checkers](https://github.com/marick/structural-typing/wiki/Glossary#checker)\".\nHere is a canonical description:\n\n```clojure\n{[:a] [even?]\n [:b] [odd?]\n [:c] [(pred/exactly 5)]}\n```\n\nHowever, you may have already noticed that you don't need the vector when you have a single checker:\n\n```clojure\n{[:a] even?\n [:b] odd?\n [:c] (pred/exactly 5)}\n```\n\nThe same is true when the path has only one element:\n\n```clojure\n{:a even?\n :b odd?\n :c (pred/exactly 5)}\n```\n\nThe above are two (possibly dubious) examples of my emphasis on\npleasing shorthand. Another example is motivated by the following:\n\n```clojure\nuser=\u003e (built-like {[] [string?]} \"foo\")\n=\u003e \"foo\"\n```\n\nThe `[]` represents a path that stops short of descending\ninto the given value (`\"foo\"` in this case). Instead, the\ncheckers are applied to `\"foo\"` itself (the [\"whole value\"](https://github.com/marick/structural-typing/wiki/Glossary#whole-value)).\n\nThat looks a bit ugly, but you've already seen the abbreviated form:\n\n```clojure\nuser=\u003e (built-like string? \"foo\")\n\"foo\"\n```\n\nI know which *I* prefer.\n\n## The justification of some seemingly odd design choices\n\nMany Clojure apps look something like this:\n\n![Flow through a pipeline](https://github.com/marick/structural-typing/blob/master/doc/flow.png)\n\n\nData flows into them from some external source.\nIt's converted into a Clojure data structure: maps or vectors of\nmaps (1). The data is perhaps augmented by requesting related data from\nother apps (2).\n\nThereafter, the data flows through a series of processing steps\n(3). Each of them transforms structures into other structures. Some\nsteps might reduce a sequential structure into a map, or generate a\nsequence from a map. But the most common transformation is to add,\nremove, or change key-value pairs. Whatever the case, each step is\n(should be) a relatively isolated, discrete, and\nindependently-understandable data transformation.\n\nFinally (4), the result is passed on to some other app. It might be\nthe app that sent the data in the first place, or it might be another\napp in a pipeline of microservices.\n\n*Or*... the pipeline needs to be interrupted at some point because\nsomething has gone wrong. Clojure's `some-\u003e` and `some-\u003e\u003e` are useful\nfor this purpose, as they will short-circuit a pipeline if any step\nproduces a nil:\n\n```clojure\nuser=\u003e (some-\u003e {:a {:b 1}}\n               :a           ; produces {:b 1}\n               :c           ; produces nil\n               inc)         ; this is never reached\n=\u003e nil\n```\n\nThis library adapts well to that style: check the result of each step\n(most often at the end of pipeline stages that are functions). If the\nvalue checks out, `built-like` will pass it along. Otherwise, it will\nissue an out-of-band error message and return `nil` so that `some-\u003e`\nwill stop processing.\n\n(If you prefer monads, you can [easily change](https://github.com/marick/structural-typing/wiki/Using-the-Either-monad) `built-like`'s behavior\nso that it returns an Either value.)\n\nBecause of the way structures are transformed as they flow through the\npipeline, you often do not want to say \"Structure X *must* contain\nsubstructure Y\". Instead, you want to say \"*If* substructure Y has\nbeen added, it should look like *this*\". That's the reason why\n`built-like` accepts missing or `nil` values unless otherwise instructed.\nIt's only at the\nend of the pipeline that you want to append a further qualification\n\"... and all of these substructures are required.\"\n\nAlthough not mentioned above, `built-like` will never complain about\nextra structure. If you declare that a point must have integer `:x` and `:y`\ncoordinates, `built-like` will happily accept a map that satisfies that constraint but also\nhas a `:color` key. (See\n[Elm's records](http://elm-lang.org/docs/records#record-types) for\nmore examples.)\n\nThat's it for paths, their traversals, and how oddly-shaped structures\nare handled. But another peculiarity of this library is that\nfundamental types are described with predicates (like `string?`)\ninstead of names (like [Schema's](https://github.com/Prismatic/schema)\n`s/Str`). Why?\n\nThe use of names (so-called [nominal typing](https://en.wikipedia.org/wiki/Nominal_type_system))\nrestricts the language you can use to describe data. You can speak of a `:color` that\nis a string, but not a string that specifically encodes an RGB-format\ncolor value (as might be tested by an `rgb-string?` predicate). That's\nbecause type systems have historically been targeted at *static*\n(compile-time) analysis, and that's intractable with arbitrary\npredicates. But it's perfectly fine for\nruntime checking, which is why this library leans so heavily on predicates.\nTo us, values type-check if they satisfy predicates. Building up new or composite types means adding new predicates that values must satisfy.\n\n\n## But names are kind of handy...\n\nIndeed they are. If you have a `Point` type, it might be nice to name\nit. One way would be to use `def`\n\n```clojure\n(def Point {:x [required-path integer?]\n            :y [required-path integer?]})\n```\n\nEssentially, you're using a map from type names to type\ndescriptions. The type names are globally accessible because they're\nvars stored in a namespace's `ns-publics` map.\n\nThat works. But, as it turns out, some kinds of shorthand type\ndescriptions are cleaner and clearer when we use a separate map of type names to\ndescriptions, one that isn't layered on top of a namespace. That's called a\n[type-repo](https://github.com/marick/structural-typing/wiki/Glossary#type-repo).\n\nA particular program can have many type repos, each using the\n[recommended setup](https://github.com/marick/structural-typing/wiki/Recommended-setup) for ease of use. For repl samples like those on this page, it's more convenient to use the *global type repo*. It lets you give keyword or string names to the sort of descriptions you've already seen:\n\n```clojure\nuser=\u003e (use 'structural-typing.global-type)\nuser=\u003e (type! :Point {:x [required-path integer?]\n                      :y [required-path integer?]})\nuser=\u003e (built-like :Point {:x 1.5})\n:x should be `integer?`; it is `1.5`\n:y does not exist\n=\u003e nil\n```\n\nOnce you have named types, you'll want to combine them. Suppose we\nhave a separate `:Colorful` type like this:\n\n```clojure\nuser=\u003e (type! :Colorful {:color string?})\n```\n\nYou can check to see whether a map is built like both a `:Point` and a `:Colorful`:\n\n```clojure\nuser=\u003e (built-like [:Colorful :Point] {:x 1, :color 1})\n:color should be `string?`; it is `1`\n:y does not exist\n=\u003e nil\n```\n\nOn the other hand, if all you want to say is that some specific\n`:Point` seen at one place in your code has a string `:color` value,\nnaming a new type might be excessive. So you can combine type names\nand \"anonymous\" snippets of description:\n\n```clojure\nuser=\u003e (built-like [:Point\n                    {:color string?}]   ; an anonymous addition to `:Point`\n                   {:x 1, :color 1})\n:color should be `string?`; it is `1`\n:y does not exist\n=\u003e nil\n```\n\n\nIf, though, the idea of a colorful point is an important part of your\ndomain, it's easy to create one:\n\n```clojure\nuser=\u003e (type! :ColorfulPoint (includes :Colorful)\n                             (includes :Point))\nuser=\u003e (built-like :ColorfulPoint {:x 1, :color 1})\n:color should be `string?`; it is `1`\n:y does not exist\n=\u003e nil\n```\n\n(The use of `includes` is intended to remind you that a\n`:ColorfulPoint` is not limited to the fields named by the types. If\nan incoming `:ColorfulPoint` includes a `:shape` key that your code is\nuninterested in, that's perfectly fine: it'll just be passed along.)\n\nAnother way to combine existing types is to build up aggregate types. Let's suppose that\na `:Line` contains two points. That can be done like this:\n\n```clojure\nuser=\u003e (type! :Line {:head [required-path (includes :Point)]\n                     :tail [required-path (includes :Point)]})\n               \nuser=\u003e (built-like :Line {:head {:x 1}})\n:tail does not exist\n[:head :y] does not exist\n=\u003e nil\n```\n\nThere are other variant representations that the library\nexpands into canonical form. Frankly, some of them were more fun to\nimplement than they are useful, so I'll refer you to\n[the wiki](https://github.com/marick/structural-typing/wiki) for\ndetails.\n\nBut there's one worth mentioning here.\n\n### Quickly requiring certain paths\n\nYou might be surprised (I was) by how often all you need to check at\nthe beginning of a pipeline is whether the input has all the required\nkeys. Inputs seem to either be right or wildly wrong. Either `:x` and\n`:y` are both present and both integers, or they're missing\ncompletely. A programmer who remembered to assign `x` and `y` values to points\nprobably did not suddenly decide strings would be a great way to represent them.\n\nIf you believe that, your type declarations might be no more than:\n\n```clojure\nuser=\u003e (type! :ColorfulPoint {:x required-path\n                              :y required-path\n                              :color required-path})\n```\n\nAwfully verbose for what you want to accomplish. This is better:\n\n```clojure\n(type! :ColorfulPoint (requires :x :y :color))\n```\n\nYou might be more suspicious. You'll believe that `:x` and `:y` are valid (if present), but you'd like to check that `:color` actually satisfies `rgb-color?`. That's done like this:\n\n```clojure\n(type! :ColorfulPoint (requires :x :y :color)\n                      {:color rgb-string?})\n```\n\n## Changing what happens when the checked value is incorrect\n\nYou can [log messages using a logging library](https://github.com/marick/structural-typing/wiki/Using-a-logging-library) instead of printing to standard output.\n\nYou can use a [monadic](https://github.com/marick/structural-typing/wiki/Using-the-Either-monad) (inline) style of error reporting.\n\nYou can easily [throw an exception](http://marick.github.io/structural-typing/structural-typing.type.html#var-throwing-error-handler) when an error is encountered.\n\n## Similar libraries\n\n[Schema](https://github.com/Prismatic/schema) and [Truss](https://github.com/ptaoussanis/truss). *At some point, there will be a link to a compare-and-contrast page.*\n\n## For more details\n\nSee the [wiki](https://github.com/marick/structural-typing/wiki) for more methodical documentation, recommended setup, use with logging libraries and monads, and details on semantics. There is [API](http://marick.github.io/structural-typing/) documentation. It includes descriptions of [predefined predicates](http://marick.github.io/structural-typing/structural-typing.preds.html).\n\n-------------------\n\n# Back Matter\n\n## Credits\n\nI was inspired by Elm's typing for its [records](http://elm-lang.org/learn/Records.elm).\n\nI first used type checking of this sort at\n[GetSet](http://getset.com), though this version is way\nbetter. (Sorry, GetSet!)\n\n[Specter](https://github.com/nathanmarz/specter) does the work of\ntraversing structures, and Nathan Marz provided invaluable help with\nSpecter subtleties. [Potemkin](https://github.com/ztellman/potemkin)\ngave me a function I couldn't write correctly myself. [Defprecated](https://github.com/alexander-yakushev/defprecated) came in handy as I flailed around in search of an API.\n\n## Contributors\n\n* Alessandro Andrioni\n* Bahadir Cambel\n* Brian Marick\n* Devin Walters\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarick%2Fstructural-typing","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fmarick%2Fstructural-typing","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fmarick%2Fstructural-typing/lists"}