Skip to content

ianthehenry/pat

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

24 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

pat/match

A supercharged match macro for Janet. Install it with jpm:

# project.janet
(declare-project
  :dependencies [
    {:url "https://github.com/ianthehenry/pat.git"
     :tag "v2.0.1"}
  ])

Here's a quick diff between pat/match and Janet's built-in match:

  • [x y z] patterns match exactly, instead of matching prefixes of their input
  • pat/match supports pattern alternatives with or
  • pat/match supports field punning in dictionary patterns, with {:foo &}
  • pat/match supports pattern aliases and refinements with and
  • pat/match supports optional fields in dictionary patterns, with {:x (? x)}
  • pat/match supports "view patterns" with map
  • pat/match raises an error when no patterns match (unless you specify an explicit default)
  • there's a different syntax for attaching conditions to patterns (see "predicate and expression patterns" below)

Symbol patterns

Symbol patterns are the same as the native match, except that & is not a valid symbol in pat/match, while @ always is.

Symbols match any values, and bind that value.

(pat/match 10
  x (* 2 x))
# 20

There are two exceptions:

  • _ is a pattern that matches any value, but creates no binding.
  • & is not a legal symbol pattern, as it has special meaning in struct and tuple patterns.

Literal patterns

The same as the native match.

Numbers, strings, keywords, and booleans match values exactly. All quoted values -- including symbols -- match exactly as well.

(pat/match (type [1 2 3])
  :tuple "yep")

(pat/match operator
  '+ "plus"
  '- "minus")

Predicate and expression patterns

Quite a bit different than the native match.

Use | to evaluate arbitrary predicates or expressions. For example:

(def x 5)
(pat/match x
  |even? "it's even"
  |odd? "it's odd")
# "it's odd"

Which is the same as:

(def x 5)
(pat/match x
  |(even? $) "it's even"
  |(odd? $) "it's odd")
# "it's odd"

You can also write arbitrary expressions that don't refer to the value being matched at all:

(def x 5)
(pat/match x
  |(< 1 2) :trivial)
# :trivial

A mental model for how this works: short-fns of zero arguments are invoked, and if they return a function or cfunction, then their result is invoked again with the value being matched. Otherwise, if they don't return a function or cfunction, their result is interpreted as a normal truthy or falsey value.

But in practice, pat/match will optimize away the short-fn allocation in all practical cases where your pattern is a constant expression or predicate.

Tuple and array patterns

Unlike the native match, tuple patterns without a & clause must match exactly with their input, instead of a prefix of their input. pat/match also supports arbitrary patterns after the &, while the native match only supports a symbol.

(def values [1 2])
(pat/match values
  [x y] (+ x y))

Matching prefixes

(def values [1 2 3])
(pat/match values
  [x y &] (+ x y))
(def values [1 2 3])
(pat/match values
  [car cadr & rest] rest)
# [3]

& rest patterns match a sliced value of the same type as their input:

(def values @[1 2 3])
(pat/match values
  [car cadr & rest] rest)
# @[3]

You can put any pattern after the &, not just a symbol. For example, this pattern will only match tuples of length 2, 3, or 4:

(def values [1 2 3])
(pat/match values
  [car cadr & |(<= (length $) 2)]
    (+ car cadr))

Struct and table patterns

Basically the same as the native match, but supports optional keys and field punning.

(def point {:x 1 :y 2})
(pat/match point
  {:x x :y y} (+ x y))

Optional matching

Because structs and tables cannot contain nil values, the following can never match:

(pat/match {:foo nil}
  {:foo _} ...)

Because {:foo nil} is actually {}, and the pattern {:foo _} needs to match against the key :foo, which does not exist.

You can fix this by making an optional match like this:

(pat/match {:foo nil}
  {:foo (? x)} x)

This will bind x to nil if the keyword :foo does not exist in the input.

Keyword punning

Instead of:

(def person {:name "ian"})
(pat/match person
  {:name name} (print name))

You can write:

(pat/match person
  {:name &} (print name))

Evaluation order

Note that, due to the way Janet abstract syntax trees work, there is no way to guarantee the order of the match in a struct pattern. This means you cannot refer to variables bound by other keys. That is, don't write code like this:

(pat/match foo
  {:a a :b |(> $ a)} ...)

Such a construct is allowed using [] patterns or and patterns, but not {} patterns: the order that the keys in a struct appear is not part of the parsed abstract syntax tree that pat operates on. This might work, sometimes, but it's fragile, and working code could break in a future version of Janet. pat/match will not prevent you from doing this, because I don't know how to do so without incurring a runtime cost.

If you really need to do this, you can use and to sequence each step of the match:

(pat/match foo
  (and {:a a} {:b |(> $ a)}) ...)

Similarly, you cannot write duplicate keys in a struct pattern:

(pat/match foo
  {:a a :a 10} ...)

Janet erases the first instance of :a at parse time, so pat can't even warn you if you make this mistake. If you want to match multiple patterns against the same key, use an (and) pattern instead:

(pat/match foo
  {:a (and a 10)} ...)

Operator patterns

Operator patterns replace the native match's equality patterns, like (@ foo) and condition patterns, like (foo (> foo 0)).

(and patterns...)

You can use and to match multiple patterns against a single value. You can use this to alias values, like "as patterns" in other languages:

(pat/match [1 2]
  (and [x y] p)
    (printf "%q = <%q %q>" p x y))

Or to check conditions, like "when patterns" in other languages:

(pat/match point
  (and [x y] |(< x y))
    (print "ascending order"))

(or patterns...)

or allows you to try multiple patterns and match if any one of them succeeds:

(pat/match (type value)
  (or :tuple :array) "indexed")

Every subpattern in an or pattern must bind exactly the same set of symbols. For example, this is allowed:

(pat/match value
  (or [x] x) (* x 2))

But this will fail to compile:

(pat/match value
  (or [x] y) (* x 2))

You can use _ to perform structural matching without binding any new symbols.

(= value)

Check a value for equality:

(def origin [0 0])
(pat/match point
  (= origin) :origin)

This is equivalent to:

(pat/match point
  |(= origin $) :origin)

But a little more convenient to write.

(unquote value)

You can use unquote in order to create first-class patterns.

unquote uses eval in order to evaluate the pattern, so it will not work if you want to reference a variable that is defined in a lexical scope (i.e., a variable that is not an environment entry). So this will work:

(def my-pattern ~[x y])
(pat/match [1 2]
  ,my-pattern (+ x y))

But this will not:

(let [my-pattern ~[x y]]
  (pat/match [1 2]
    ,my-pattern (+ x y)))

Another way to say this is that you cannot create dynamic patterns at runtime. The pattern you're splicing in must be known at compile time.

(not pat)

Invert a pattern:

(def point [0 0 0])
(pat/match point
  (not [_ _]) :not-2d)

The pattern inside not cannot create any bindings.

(not= pat)

Slightly more efficient shorthand for (not (= x)).

(pat/match value
  (and x (not= nil)) (print x))

(map f pat)

Call f with the value being matched, and match the pattern against the result, like "view patterns" in other languages.

(match numbers
  (map max-of (and big |prime?)) (print big)
  (error "largest number must be prime"))

Changelog

v2.0.1 2024-08-04

  • Macro expansion is more hygienic. In particular it works even in contexts that have shadowed length or =.

v2.0.0 2024-07-18

  • Breaking change: unquote now splices first-class patterns, instead of checking for equality. Previously, the patterns (= foo) and ,foo were identical. Now the latter assumes foo evaluates to a pattern.

v1.0.1 2024-05-15

  • Fix a bug where non-optional dictionary patterns wouldn't work.

v1.0.0 2023-08-27

  • Initial release.