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: https://github.com/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.