Pattern Matching

At the very heart of Atomy is pattern-matching, and its behavior with dispatch. Patterns are used to both check for and deconstruct a value in one fell swoop. Depending on the scenario, a pattern-match may error if it doesn't match (=, =!, and sometimes methods), or just silently fail (match).

When defining a method, a few things occur. First, the target of the definition is determined based on the message receiver pattern. See A Pattern's Target for a more detailed account, but you don't have to memorize any of this.

Once the target is determined, Atomy will check to see if the method is already defined on the target, perhaps with different patterns. If it doesn't exist, the method is simply inserted. If it does, the method is inserted based on its "precision" in relation to the others.

This sorting is the magic dust. Pattern A is said to be "more precise" than pattern B if it matches fewer cases, or if pattern B will always match what pattern A will match, but not the other way around.

For example, the pattern 1 is much more precise than the pattern x - the former only matches the integer 1, while the latter will match any value! In addition, with two Constant patterns, one specifying a subclass of the other, the subclass is more precise.

Care is taken to maintain proper precision-based ordering so that an object may be extended after the fact with method definitions that handle more specific cases. For example, let's say in your original code you define this:

1 foo := -1
2 foo := -2
_ foo := .umm

Without precision sorting, not only would the order of definitions matter greatly, but no one would be able to handle any other cases! If someone came along and tried defining 3 foo := -3, it would be unreachable, because the wildcard _ matches everything. They'd always get back .umm for everything but 1 and 2. Likewise, if you defined that catch-all case first, it would override the others.

With precision sorting, things "just work." You can define your methods in whatever order you want - maybe writing the catch-all first and then drilling into the details afterward. It doesn't matter.

Types of Patterns

There are a ton of different pattern-matchers that come with Atomy, owing to the fact that they are so easy to define and can provide for immense expressive power.

_, foo, @foo, @@foo, $foo, ...

Possibly-named wildcard matches. Global, instance, and class variables behave the same, but bind as their respective types rather than as locals.

Example:

> _ = 1
1
> a = 2
2
> a
2
1, 4.0, true, false, "foo", .foo, ...

Primitive literal values that will match only themselves.

Example:

> 1 = 1
1
> 1 = 2
Atomy::PatternMismatch: pattern of type `Match' did not match value `2'
> 4.0 = 4.0
4.0
> 4.0 = 4.000000000001
Atomy::PatternMismatch: pattern of type `Literal' did not match value `4.000000000001'
> "foo" = "foo"
"foo"
foo: pattern, foo { pattern }

A named pattern-match. Matches pattern, binding the value it matches to a local variable, foo.

Example:

> (foo: 2) = 2
2
> foo
2
> (foo: 2) = 1
Atomy::PatternMismatch: pattern of type `Named' did not match value `1'
head . tail

Matches a non-empty list, pattern-matching its first value on head and the rest of it on tail.

Example:

> (x . xs) = [1, 2, 3]
[1, 2, 3]
> x
1
> xs
[2, 3]
[], [pattern, pattern-2], ...

Matches a list of fixed length, recursively matching each pattern on each of its values.

Example:

> [] = []
[]
> [a, b] = [1, 2]
[1, 2]
> [c, 4] = [3, 4]
[3, 4]
> [a, b, c]
[1, 2, 3]
.foo(x), .foo(x, _), ...

Matches a particle value, recursively pattern-matching the particle's values. A wildcard in a particle pattern matches placeholders, but no other pattern will.

Example:

> .foo(x) = .foo(2)
.foo(2)
> x
2
> .foo(_) = .foo(_)
.foo(_)
> .foo(_) = .foo(2)
.foo(2)
> .foo(2) = .foo(_)
Atomy::PatternMismatch: pattern of type `Particle' did not match value `.foo(_)'
'a, `a, `(1 + ~b), ...

Matches expression values recursively. Unquotes serve as nested patterns, with the same recursive semantics as quasiquotation. Splice unquotes are similar, but act as "splats", matching the rest of its container.

Example:

> `a = `a
a
> `(1 + ~b) = '(1 + 2)
1 + 2
> `(1 + ~'2) = '(1 + 2)
1 + 2
> ``(1 + ~~c) = ``(1 + ~3)
`(1 + ~3)
> [b, c]
[2, 3]
> `[1, 2, ~*['3, '4]] = '[1, 2, 3, 4]
[1, 2, 3, 4]
> `[1, 2, ~*xs] = '[1, 2, 1 + 2, 1 + 3]
[1, 2, 1 + 2, 1 + 3]
> xs
[1 + 2, 1 + 3]
{ expression }

Used to target a value's singleton class; expression is evaluated and its singleton class is the target for a definition. This pattern acts as a wildcard otherwise.

Example:

> a = "foo"
"foo"
> { a } fizz := 42
#<Rubinius::CompiledMethod fizz file=wrapper>
> a fizz
42
> "foo" fizz
NoMethodError: undefined method `fizz' on an instance of String.
&pattern

Ruby-style proc-arg pattern, used to capture a block passed to a method as a Proc.

Example:

> x my-block(&y) := y [x]
#<Rubinius::CompiledMethod my_block file=wrapper>
> 1 my-block: 42
Atomy::MethodFail: method 'my_block' does not understand the given arguments: []
> 1 my-block [x]: x + 2
Atomy::MethodFail: method 'my_block' does not understand the given arguments: []
*pattern

Ruby-style "splat" pattern, typically used to match the rest of the arguments in a method definition.

Example:

> x my-splat(y, *zs) := [x, y, zs]
#<Rubinius::CompiledMethod my_splat file=wrapper>
> 1 my-splat(2, 3, 4, 5)
[1, 2, [3, 4, 5]]
> x my-splat(y, *[3, zs]) := [x, y, zs]
#<Rubinius::CompiledMethod my_splat file=wrapper>
> 1 my-splat(2, 3, 4)
[1, 2, 4]
pattern = default

Ruby-style default argument pattern. Will match pattern on the argument, or on default's value if the argument is not given.

Example:

> x my-default(y = x + 1) := [x, y]
#<Rubinius::CompiledMethod my_default file=wrapper>
> 1 my-default
[1, 2]
> 1 my-default(42)
[1, 42]
pattern ? predicate

Matches pattern, and then evaluates predicate with the value as self, only succeeding if both the match and the predicate succeed.

If pattern is not given, _ is assumed.

Example:

> (Integer ? odd?) = 41
41
> (Integer ? odd?) = 42
Atomy::PatternMismatch: pattern of type `Predicate' did not match value `42'
> (Integer ? odd?) = "foo"
Atomy::PatternMismatch: pattern of type `Predicate' did not match value `"foo"'
> (? odd?) = 41
41
> (? odd?) = 42
Atomy::PatternMismatch: pattern of type `Predicate' did not match value `42'
> (? odd?) = "foo"
NoMethodError: undefined method `odd?' on an instance of String.
pattern-1 & pattern-2

Succeeds only if both patterns match the value. This is similar to foo: pattern-2, but you can specify a pattern rather than a named wildcard in place of foo.

Example:

> (Integer & 41) = 41
41
> (Integer & 42) = 41
Atomy::PatternMismatch: pattern of type `And' did not match value `41'
> (a & b) = x
NoMethodError: undefined method `x' on #<Atomy::Module:0x3f4b8> (Atomy::Module)
> [a, b]
NoMethodError: undefined method `a' on #<Atomy::Module:0x3f4b8> (Atomy::Module)
pattern-1 | pattern-2

Succeeds if either patterns match the value. This pattern is short-circuiting; if one matches, the other won't be attempted. Therefore, don't rely on bindings in the second pattern being set.

Example:

> (one | two) = 41
41
> [one, two]
NoMethodError: undefined method `two' on #<Atomy::Module:0x3fc80> (Atomy::Module)
> (1 | 2) = 2
2
> (1 | x) = 2
2
> x
2
pattern with(expression, sub-pattern)

Matches pattern on the value. If the match succeeds, matches sub-pattern on the result of evaluating expression with the value as self. Useful for matching things like instance variables. An omitted pattern implies _.

Example:

> Integer with(odd?, true) = 1
1
> data(MyPoint(@x, @y))
nil
> with(@x, 1) = MyPoint new(1, 2)
#<Rubinius::BlockEnvironment:0x411ac>

A Pattern's Target

When defining a method, the receiver in the message-pattern determines where the method is inserted.

_, foo, @foo, @@foo, $foo, ...

Object

{ foo bar }

foo bar's singleton class

Foo, Foo::Bar, ::Bar, ...

the class named by the constant

1, 2, ...

Integer

true

TrueClass

false

FalseClass

1.0, 2.0, ...

Float

"", "foo", ...

String

head . tail, [], [pattern, pattern-2], ...

List

'x, `x, `(1 + ~y), ...

Expression

.foo

Symbol

.foo(1)

Particle

foo: pattern, foo { pattern }

target of pattern

pattern = default

target of pattern

pattern ? predicate

target of pattern

? predicate

Object

pattern-1 & pattern-2

target of pattern-1

pattern-1 | pattern-2

target of pattern-1

pattern-1 with(expression, pattern-2)

target of pattern-1

&foo, *foo

Object

Thus, 1 foo := x is a definition placed on Integer, while { self } foo is inserted on self's singleton class, similar to def self.foo; ...; end in Ruby.