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)
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