Liking cljdoc? Tell your friends :D

match-block

Pattern matching blocks as values.


![data all the things](http://i.imgur.com/6OupF6Q.jpg)

Turning abstractions into first-class values gives us an ability to abstract over and compose them, and often yields conceptually simpler models. First-class-ing things has been a common theme in Clojure, where it's fondly referred to as "data all the things".

Pattern matching is a lovely feature. It's an integral part of all major functional languages. Sadly, in most implementations, pattern matching blocks aren't first class values.

Scala innovates in this area by treating pattern-matching blocks as first-class values. It achieves this by providing a dedicated type (called PartialFunction) for them. Let's see how this can be useful.

PartialFunction in Scala

Consider the following piece of Scala code (pasted directly from a REPL session):

scala> def foo(a: String, b: String) = try {
     |   a.toInt / b.toInt
     | } catch {
     |   case ex: NumberFormatException => 'nfe
     |   case ex: ArithmeticException => 'ae
     | }
foo: (a: String, b: String)Any

scala> foo("2", "1")
res6: Any = 2

scala> foo("kl", "1")
res7: Any = 'nfe

scala> foo("9", "0")
res8: Any = 'ae

Syntactically, the try-catch here looks fairly similar to its Clojure counterpart. However there is one big difference. The bit that's passed to catch as argument is a first-class value! This lets us do things like:

scala> def foo(a: String, b: String, handler: PartialFunction[Throwable, Any]) = try {
     |   a.toInt / b.toInt
     | } catch handler
foo: (a: String, b: String, handler: PartialFunction[Throwable,Any])Any

scala> val h: PartialFunction[Throwable, Any] = {
     |   case ex: NumberFormatException => 'nfe
     | }
h: PartialFunction[Throwable,Any] = <function1>

scala> val i: PartialFunction[Throwable, Any] = {
     |   case ex: ArithmeticException => 'ae
     | }
i: PartialFunction[Throwable,Any] = <function1>

scala> foo("4", "0", h.orElse(i))
res9: Any = 'ae

And even:

scala> attempt {
     |   val s = Console.readLine
     |   s.toInt
     | } fallback {
     |   case ex: NumberFormatException => println("Invalid string. Try again."); restart
     | }

// "hobo"
Invalid string. Try again.

// "kucuk"
Invalid string. Try again.

// "9"
res12: Int = 9

scala>

(You can find the implementation for the attempt-fallback utility here.)

From these examples, we can deduce two major advantages of first-class pattern matching blocks:

  • One can compose pattern matching blocks using combinators such as orElse.
  • It's easy to create new constructs requiring case-based handling, without having to resort to ad hoc syntactic transformations (macros).

There are numerous examples in the Scala world where this has been put to a good use. Some of which are as follows:

  • scala.util.control.Exception - Compositional and functional goodness, atop traditional exception handling constructs.
  • Standard collection operations like collect.
  • Methods such as onSuccess for registering callbacks in futures library.
  • Error recovery combinators, such as recover in futures library.
  • react block in actors.
  • Request matchers in Lift.

How things look on the Clojure side

Clojure currently does not have a generic construct of this sort. Clojure's try-catch for example, is an ad hoc syntax, which maps almost directly to its Java counterpart.

As it happens, Clojure has everything you will need to implement this idea on your own:

  • First-class functions.
  • core.match - a great pattern matching library to piggyback on.
  • Support for building syntactic extensions (by virtue of being a Lisp).

This project uses the above to provide first-class pattern matching blocks implementation for Clojure.

The implementation is almost entirely based on Scala's PartialFunction - the core implementation, the optimizations, and the combinators.

What the project currently does

The REPL session below should demystify the crux of the library:

user=> (use '[clojure.core.match :only (match)]) (use 'match-block.core)
nil
nil
user=> (macroexpand-1 '(match-block [a b] [2 :two] :qux [3 :three] :guz))
(match-block.core/map->MatchBlock {:fun         (clojure.core/fn [a b]
                                                  (clojure.core.match/match [a b]
                                                                            [2 :two] :qux
                                                                            [3 :three] :guz))
                                   :defined-at? (clojure.core/fn [a b]
                                                  (clojure.core.match/match [a b]
                                                                            [2 :two] true
                                                                            [3 :three] true
                                                                            :else false))})

Future directions for the library

  • Combinators to compose, transform pattern matching blocks. Examples: or-else, comp, apply-or-else, lift, unlift, cond etc.
  • Scala's PartialFunctions have more special treatment in compiler, making the above-mentioned combinators very efficient. We could borrow some of those ideas in this port.
  • A variant of try-catch that accepts its handler as a pattern matching block.
  • The slingshot library has a concept of "selectors". I think "selectors" are simply a special case of "matching", and matching should belong in core.match. The selectors could likely be reimplemented with a bunch of custom core.match patterns, plus match-block.
  • The implementation could potentially make use of knowledge of core.match innards to provide faster implementations of :fun and :defined-at?.

Usage

(REPL session again.)

user=> (use '[clojure.core.match :only (match)]) (use 'match-block.core)
nil
nil
user=> (def foo
  #_=>   (match-block [a b]
  #_=>                [3 1] :nice
  #_=>                :else :aww-shucks))
#'user/foo
user=> (foo 3 1)
:nice
user=> (foo 3 2)
:aww-shucks
user=> (defined-at? foo 3 1)
true
user=> (defined-at? foo 3 3)
true
user=> (def bar
  #_=>   (match-block [a b]
  #_=>                [3 1] :nice))
#'user/bar
user=> (bar 3 3)

IllegalArgumentException No matching clause: 3 3  user/fn--2410 (NO_SOURCE_FILE:2)
user=> (defined-at? bar 3 3)
false
user=> Bye for now!%

Inputs welcome!

Any sort of feedback, code review, pull requests are most welcome!

License

Copyright © 2014 Rahul Goma Phulore.

Distributed under the Eclipse Public License, the same as Clojure.

Can you improve this documentation?Edit on GitHub

cljdoc is a website building & hosting documentation for Clojure/Script libraries

× close