haskell_match: Haskell's pattern matching (and a chunk of Haskell itself) in Ruby, compiled to decision trees in Rust
I just published haskell_match, a gem that brings Haskell's pattern matching to Ruby with the part Ruby has never had: every logical path has to be accounted for, or the definition fails.
HaskellMatch.data "Shape = Circle Double | Rect Double Double | Tri Double Double Double"
include Shape
area = HaskellMatch.fn(:area) do
on("Circle r") { |r| Math::PI * r * r }
on("Rect w h") { |w, h| w * h }
end
# HaskellMatch::NonExhaustiveError: Pattern match(es) are non-exhaustive
# In an equation for 'area':
# Patterns not matched:
# Tri _ _ _
That is GHC's -Wincomplete-patterns and -Woverlapping-patterns, as errors, at definition time. Add a Tri clause and it compiles. Add a clause that can never be reached and it tells you that too.
How it works
Clauses are compiled once into a Maranget-style decision tree by a Rust extension (Rutie). Matching runs in Rust over raw Ruby VALUEs with no allocation until a clause is chosen; list tails are shared slices rather than copies. The exhaustiveness and redundancy analysis is the usefulness algorithm from Maranget's "Warnings for pattern matching", with witnesses rendered the way GHC prints them.
Two dialects, mixed freely
Quoted Haskell syntax, or the same patterns written in place as plain Ruby:
on("Just (x:xs)") { |x, xs| ... }
on(Just([x, *xs])) { |x, xs| ... }
on(Person(name: n, **_)) { |n| ... }
on({ name: n, title: t }) { |n, t| ... } # Hash patterns, same checker
Strings are lists of characters (on("(c:cs)") matches "hello"), wildcards match anything, and a value no pattern can accept raises a type error, exactly like Haskell.
Recursion without "stack level too deep"
A recursive function over a million elements works. Plain recursion runs on chained Fiber stacks, tail calls run in constant space, and a depth guard turns a runaway loop into an exception instead of a memory leak. The README has measurements (bytes per level, GC pause times) for anyone who wants the numbers.
Lazy lists
naturals = HaskellMatch.lazy(1..)
take.(5, naturals) # => [1, 2, 3, 4, 5]
Infinite lists match list patterns directly; Enumerators are wrapped automatically.
Or just write Haskell
This is the part I did not expect to end up building. You can write actual Haskell, inline or in .hs files, and it compiles to Ruby methods:
Nums = HaskellMatch.haskell(<<~HS)
primes :: [Int]
primes = sieve [2..]
where
sieve [] = []
sieve (p:xs) = p : sieve [x | x <- xs, x mod p /= 0]
fibs :: [Integer]
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
HS
Nums.primes.take(10) # => [2, 3, 5, 7, 11, 13, 17, 19, 23, 29]
Nums.fibs.take(8) # => [0, 1, 1, 2, 3, 5, 8, 13]
Supported: data declarations (including infix constructors and records), equations, guards, pattern guards, where/let, case, lambdas, sections, user-defined operators with fixity declarations, list comprehensions, ranges, imports and exports between modules, and a lazy Prelude. Haskell functions come back to Ruby as Procs, Ruby lambdas go in as Haskell functions, and partial application works from the Ruby side (Mod.add3(1).(2).(3)).
Deliberately not supported: type classes, do notation and monads, or anything that needs type inference. Those are rejected with a clear message rather than half-working.
Other things people might care about
- Thread-, fiber- and Ractor-safe; ractor: true produces shareable functions.
- deriving (Ord, Enum, Bounded), optional field type checks, where helpers inside fn.
- Existing Ruby Data and Struct classes work as constructors without registration; HaskellMatch.sealed closes a class hierarchy for exhaustiveness checking.
- Errors show the pattern with a caret under the problem.
- Ruby 3.2+, needs a Rust toolchain to build the extension. MIT / Apache-2.0.
​
gem install haskell_match
GitHub: danielpclark/haskell_match
Happy to answer questions about the implementation. The decision-tree compiler and the fiber-based recursion were the two interesting problems; the README has an "expert notes" section on the latter if you want to argue about memory.
8 points
Mentions
Six gems, one version, MIT. 480+ specs, six example apps driven end to end by the bot and a headless browser, interop-tested against the Rust reference server. Stores for ActiveRecord, Redis and ActiveSupport::Cache. PRs welcome. 6/6 danielpclark/sqrl-ruby #Ruby #SQRL
SQRL for Ruby just shipped. Steve Gibson’s password-less login (per-site Ed25519 keys, nothing to phish, nothing to leak from your database) is now one gem and one line away for Rails, Devise, OmniAuth, Sinatra, Roda or plain Rack. Thread: 1/6 danielpclark/sqrl-ruby
Coming soon to a browser near you: “SQRL Login” 🐿️ An open-source browser extension for @SGgrc's SQRL, with a #Rust/#WebAssembly core. danielpclark/SQRL-extension #SQRL #security
Built a desktop dashboard for live Claude Code sessions: every session on your machine, other machines on your LAN/Tailscale, and claude.ai cloud sessions — all on one screen. #Ruby + a small #Rust (egui) window. #AI #BotOps #BotOperations danielpclark/bot-ops-blue t.co
If anyone wants to write games in Ruby I wrote my own rbgame, inspired by pygame, and it's on top of a SDL rewrite in Rust. #Ruby #Rust #Python #GameDev #gamedesign danielpclark/rbgame Includes a Gorillas clone game as an example. t.co
haskell_match: Haskell's pattern matching (and a chunk of Haskell itself) in Ruby, compiled to decision trees in Rust I just published haskell_match, a gem that brings Haskell's pattern matching to Ruby with the part Ruby has never had: every logical path has to be accounted for, or the definition fails. HaskellMatch.data "Shape = Circle Double | Rect Double Double | Tri Double Double Double" include Shape area = HaskellMatch.fn(:area) do on("Circle r") { |r| Math::PI * r * r } on("Rect w h") { |w, h| w * h } end # HaskellMatch::NonExhaustiveError: Pattern match(es) are non-exhaustive # In an equation for 'area': # Patterns not matched: # Tri _ _ _ That is GHC's -Wincomplete-patterns and -Woverlapping-patterns, as errors, at definition time. Add a Tri clause and it compiles. Add a clause that can never be reached and it tells you that too. How it works Clauses are compiled once into a Maranget-style decision tree by a Rust extension (Rutie). Matching runs in Rust over raw Ruby VALUEs with no allocation until a clause is chosen; list tails are shared slices rather than copies. The exhaustiveness and redundancy analysis is the usefulness algorithm from Maranget's "Warnings for pattern matching", with witnesses rendered the way GHC prints them. Two dialects, mixed freely Quoted Haskell syntax, or the same patterns written in place as plain Ruby: on("Just (x:xs)") { |x, xs| ... } on(Just([x, *xs])) { |x, xs| ... } on(Person(name: n, **_)) { |n| ... } on({ name: n, title: t }) { |n, t| ... } # Hash patterns, same checker Strings are lists of characters (on("(c:cs)") matches "hello"), wildcards match anything, and a value no pattern can accept raises a type error, exactly like Haskell. Recursion without "stack level too deep" A recursive function over a million elements works. Plain recursion runs on chained Fiber stacks, tail calls run in constant space, and a depth guard turns a runaway loop into an exception instead of a memory leak. The README has measurements (bytes per level, GC pause times) for anyone who wants the numbers. Lazy lists naturals = HaskellMatch.lazy(1..) take.(5, naturals) # => [1, 2, 3, 4, 5] Infinite lists match list patterns directly; Enumerators are wrapped automatically. Or just write Haskell This is the part I did not expect to end up building. You can write actual Haskell, inline or in .hs files, and it compiles to Ruby methods: Nums = HaskellMatch.haskell(<<~HS) primes :: [Int] primes = sieve [2..] where sieve [] = [] sieve (p:xs) = p : sieve [x | x <- xs, x mod p /= 0] fibs :: [Integer] fibs = 0 : 1 : zipWith (+) fibs (tail fibs) HS Nums.primes.take(10) # => [2, 3, 5, 7, 11, 13, 17, 19, 23, 29] Nums.fibs.take(8) # => [0, 1, 1, 2, 3, 5, 8, 13] Supported: data declarations (including infix constructors and records), equations, guards, pattern guards, where/let, case, lambdas, sections, user-defined operators with fixity declarations, list comprehensions, ranges, imports and exports between modules, and a lazy Prelude. Haskell functions come back to Ruby as Procs, Ruby lambdas go in as Haskell functions, and partial application works from the Ruby side (Mod.add3(1).(2).(3)). Deliberately not supported: type classes, do notation and monads, or anything that needs type inference. Those are rejected with a clear message rather than half-working. Other things people might care about - Thread-, fiber- and Ractor-safe; ractor: true produces shareable functions. - deriving (Ord, Enum, Bounded), optional field type checks, where helpers inside fn. - Existing Ruby Data and Struct classes work as constructors without registration; HaskellMatch.sealed closes a class hierarchy for exhaustiveness checking. - Errors show the pattern with a caret under the problem. - Ruby 3.2+, needs a Rust toolchain to build the extension. MIT / Apache-2.0. ​ gem install haskell_match GitHub: danielpclark/haskell_match Happy to answer questions about the implementation. The decision-tree compiler and the fiber-based recursion were the two interesting problems; the README has an "expert notes" section on the latter if you want to argue about memory.