structural-typing.type

Structural types, loosely inspired by Elm’s way of looking at records.

<>all-built-like

(<>all-built-like candidates type-repo type-shorthand)(<>all-built-like candidates type-shorthand)

The same as all-built-like but intended to be used in -> pipelines. Consequently, the candidates argument comes first.

  (-> emr-patients
      augment           (<>all-built-like [:Decidable Patient])
      audit
      decide
      schedule)

(The <> is intended to remind you of swiss arrows.)

<>built-like

(<>built-like candidate type-repo type-shorthand)(<>built-like candidate type-shorthand)

The same as built-like but intended to be used in -> pipelines. Consequently, the candidate argument comes first.

  (-> emr-patient
      augment           (<>built-like [:Decidable Patient])
      audit
      decide
      schedule)

(The <> is intended to remind you of swiss arrows.)

ALL

When included in a path, expects a collection and applies the rest of the path to each element.

all-built-like

(all-built-like type-repo type-shorthand candidates)(all-built-like type-shorthand candidates)

Check each of the candidates. Perform the type-repo’s error behavior if any of the candidates fail. Otherwise, return the original candidates.

(some->> (all-built-like :Point [{:x 1, :y 2}
                                 {:why "so serious?"}])
         (map process-points))

Error messages will include the index of the structure that failed.

built-like

(built-like type-repo type-shorthand candidate)(built-like type-shorthand candidate)

type-shorthand is either a type-signifier (typically a keyword like :Point), a condensed type description (like (requires :x :y)), or a vector containing either or both. built-like checks the candidate against the shorthand.

By default, built-like will either return the candidate or, if the candidate doesn’t match the shorthand, print an error message and return nil. If the type-repo is omitted, the global one is used.

(type/built-like :Point {:x 1 :y 2})
(type/built-like [:Colorful :Point] {:x 1, :y 2, :color "red"})
(type/built-like [:Colorful (requires :x :y)] {:x 1, :y 2, :color "red"})

Types are defined with named or type!. Default behavior is changed with replace-success-handler, replace-error-handler, on-success!, and on-error!.

built-like?

(built-like? type-repo type-shorthand candidate)(built-like? type-shorthand candidate)

type-shorthand is either a type-signifier (typically a keyword like :Point), a condensed type description (like (requires :x :y)), or a vector containing either or both.

Returns true iff the candidate structure matches everything in the shorthand.

With three arguments, the check is against the type-repo. If type-repo is omitted, the global repo is used.

(type/built-like? :Point candidate)
(type/built-like? [:Colorful :Point] candidate)

default-error-handler

This error handler takes the output of type checking (a sequence of oopsies) and prints each one’s explanation to standard output. It returns nil, allowing constructs like this:

 (some-> (type/built-like :Patient p)
         (assoc :handled true)
         ...)

default-success-handler

The default success handler just returns the original candidate structure passed to built-like.

description

(description type-repo type-signifier)(description type-signifier)

Returns the canonical (expanded) description of the type-signifier. Uses the global type repo if none is given.

The result is not a string, but rather a structure tweaked to look nice either at the repl or as the output from pprint. However, that means it’s not a real type description; you can’t feed it back to named or type!.

each-of

Use each-of to describe a “forking” path. This is convenient when two parts of a bigger data structure should be built the same way.

(type! :Plat {[:corners (each-of :nw :ne :sw :se)] (includes :GeoPoint)})

through-each is a synonym. I tend to use each-of for the end of the path, through-each for a fork earlier than that.

empty-type-repo

A type repo that contains no types and uses the default success and error handlers.

ensure-standard-functions

macro

(ensure-standard-functions type-repo-sym)

Suppose you are creating a type repo inside a namespace, as is done in the logging example. You’d like that namespace to provide functions that use that type repo without having to constantly refer to it:

 (my.types/built-like? :Point xy)
 ;; instead of:
 (my.types/built-like? my.types/type-repo :Point xy)

This function takes a type repo and creates type-repo-specific functions for you.

 (in-ns 'my.types)
 (type/ensure-standard-functions type-repo)

See the examples directory more details.

explain-with

(explain-with explainer predicate)

After the predicate fails, the failure will need to be explained. Arrange for explainer to be called with the oopsie that results from the failure.

 (explain-with #(format "Yo! %s has %s characters, which is WAY too long."
                        (:leaf-value %)
                        (count (:leaf-value %)))
               #(< (count %) 8)))

includes

(includes type-signifier)

During creation of a type by named or type!, a call to includes is replaced with the type the type-signifier refers to.

named

(named type-repo type-signifier & condensed-type-descriptions)

Define type-signifier inside the type-repo in terms of the condensed-type-descriptions.

Returns the augmented type-repo. See also named!.

not-nil

(not-nil & args)

WARNING: #’structural-typing.guts.preds.pseudopreds/not-nil is deprecated. Deprecated in favor of required-path, reject-nil, or reject-missing.

origin

(origin type-repo type-signifier)(origin type-signifier)

Returns the original condensed type description associated with the type-signifier. Uses the global type repo if none is given.

The result is not a string, but rather a structure tweaked to look nice either at the repl or as the output from pprint. However, that means it’s not a real type description; you can’t feed it back to named or type!.

paths-of

(paths-of type-signifier-or-map)

Include all the paths of a type (or a literal map) within a path.

 (type! :StrictX (includes :X)
                 (requires (paths-of :X)))

The above example constructs a stricter version of :X by insisting all of its paths are required.

When the argument is a map, it is flattened before the paths are extracted, so that {:a {:b even?}} and {[:a :b] even?} have the same effect. (Included types are already flat.)

RANGE

(RANGE inclusive-start exclusive-end)

Use this in a path to select a range of values in a collection. The first argument is inclusive; the second exclusive.

(type! :ELEMENTS-1-AND-2-ARE-EVEN {[(RANGE 1 3)] even?})

reject-missing

This appears in a predicate list, but it is never called directly. Its appearance means that cases like the following are rejected:

user=> (type! :X {:a [string? reject-missing]})
user=> (built-like :X {})
:a does not exist

user=>  (type! :X {[(RANGE 0 3)] [reject-missing even?]})
user=> (built-like :X [])
[0] does not exist
[1] does not exist
[2] does not exist

See also reject-nil and required-path.

reject-nil

False iff the value given is nil. By default, type descriptions allow nil values, following Clojure’s lead. To reject nils, use type descriptions like this:

(type! :X {:a [reject-nil string?]})

… or, when checking types directly:

(built-like [string? reject-nil] nil)

See also reject-missing and required-path.

replace-error-handler

(replace-error-handler type-repo handler)

For this type-repo, pass oopsies generated by type failures to handler as the last step in built-like. Thus, built-like will return the handler’s result.

replace-success-handler

(replace-success-handler type-repo handler)

For this type-repo, handle candidates that typecheck successfully by passing them to handler as the last step in built-like. Thus, built-like will return the handler’s result.

required-path

False iff a key/path does not exist or has value nil. See also reject-missing and reject-nil.

requires

(requires & args)

Often, all you want to say about some parts of a type is that they’re required. requires is a shorthand way to do that.

(type! :Point (requires :x :y))
(type! :Line (requires [:start :x] [:start :y]))
(type! :Line (requires [(through-each :start :end) (:each-of :x :y)]))

requires-mentioned-paths

(requires-mentioned-paths & condensed-type-descriptions)

Canonicalizes the type descriptions into a single path->pred map and adds required-path to each path’s predicates.

 (type! :X (requires-mentioned-paths (includes :Point)
                                     {:color rgb-string?}))

Note: It can’t require paths you don’t mention. The easiest way to mention a path is to name it in a requires - which may be either an argument to this function or outside it:

 (type! :X (requires-mentioned-paths (requires :name)
                                     (includes :Point)))
 (type! :X (requires :name)
           (requires-mentioned-paths (includes :Point)))

show-as

(show-as name predicate)

Associate the given name string with the predicate for use when predicate failures are explained.

  (show-as "less than 3" (partial >= 3))

through-each

(through-each & alternatives)

Use through-each to describe a “forking” path. This is convenient when two parts of a bigger data structure should be built the same way.

(type! :Line {[(through-each :start :end) :location] (includes :Point)})

each-of is a synonym. I tend to use each-of for the end of the path, through-each for a fork earlier than that.

throwing-error-handler

(throwing-error-handler oopsies)

In contrast to the default error handler, this one throws a java.lang.Exception whose message is the concatenation of the explanations of the oopsies.

To make all type mismatches throw failures, do this:

   (global-type/on-error! type/throwing-error-handler) ; for the global type repo
   (type/replace-error-handler type-repo type/throwing-error-handler) ; local repo