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.
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:
orElse
.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.collect
.onSuccess
for registering callbacks in futures library.recover
in futures library.react
block in actors.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:
core.match
- a great pattern matching library to piggyback on.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.
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))})
or-else
, comp
, apply-or-else
, lift
, unlift
, cond
etc.PartialFunction
s have more special treatment in compiler, making the above-mentioned combinators very efficient. We could borrow some of those ideas in this port.try
-catch
that accepts its handler as a pattern matching block.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
.core.match
innards to provide faster implementations of :fun
and :defined-at?
.(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!%
Any sort of feedback, code review, pull requests are most welcome!
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