haskell_match 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +98 -0
- data/LICENSE-APACHE +202 -0
- data/LICENSE-MIT +21 -0
- data/README.md +1484 -0
- data/ext/haskell_match/Cargo.lock +33 -0
- data/ext/haskell_match/Cargo.toml +22 -0
- data/ext/haskell_match/extconf.rb +41 -0
- data/ext/haskell_match/src/core/ast.rs +190 -0
- data/ext/haskell_match/src/core/error.rs +52 -0
- data/ext/haskell_match/src/core/exhaust.rs +699 -0
- data/ext/haskell_match/src/core/hs/ast.rs +256 -0
- data/ext/haskell_match/src/core/hs/json.rs +225 -0
- data/ext/haskell_match/src/core/hs/layout.rs +346 -0
- data/ext/haskell_match/src/core/hs/lexer.rs +688 -0
- data/ext/haskell_match/src/core/hs/mod.rs +14 -0
- data/ext/haskell_match/src/core/hs/parser.rs +1945 -0
- data/ext/haskell_match/src/core/lexer.rs +590 -0
- data/ext/haskell_match/src/core/mod.rs +19 -0
- data/ext/haskell_match/src/core/parser.rs +1116 -0
- data/ext/haskell_match/src/core/pretty.rs +373 -0
- data/ext/haskell_match/src/core/resolve.rs +336 -0
- data/ext/haskell_match/src/core/tree.rs +921 -0
- data/ext/haskell_match/src/core/typecheck.rs +226 -0
- data/ext/haskell_match/src/core/types.rs +404 -0
- data/ext/haskell_match/src/lib.rs +19 -0
- data/ext/haskell_match/src/ruby/mod.rs +1195 -0
- data/ext/haskell_match/src/ruby/runtime.rs +1045 -0
- data/lib/haskell_match/binding_plan.rb +84 -0
- data/lib/haskell_match/case_of.rb +71 -0
- data/lib/haskell_match/clauses.rb +354 -0
- data/lib/haskell_match/data.rb +417 -0
- data/lib/haskell_match/deep_call.rb +98 -0
- data/lib/haskell_match/deriving.rb +130 -0
- data/lib/haskell_match/dsl.rb +71 -0
- data/lib/haskell_match/errors.rb +85 -0
- data/lib/haskell_match/field_types.rb +140 -0
- data/lib/haskell_match/function.rb +240 -0
- data/lib/haskell_match/haskell/compiler.rb +961 -0
- data/lib/haskell_match/haskell.rb +326 -0
- data/lib/haskell_match/inspect.rb +45 -0
- data/lib/haskell_match/lazy_list.rb +210 -0
- data/lib/haskell_match/native_loader.rb +64 -0
- data/lib/haskell_match/pattern.rb +75 -0
- data/lib/haskell_match/pattern_ast.rb +394 -0
- data/lib/haskell_match/prelude.rb +448 -0
- data/lib/haskell_match/scope.rb +44 -0
- data/lib/haskell_match/version.rb +5 -0
- data/lib/haskell_match.rb +41 -0
- metadata +124 -0
data/README.md
ADDED
|
@@ -0,0 +1,1484 @@
|
|
|
1
|
+
# haskell_match
|
|
2
|
+
|
|
3
|
+
Haskell's pattern matching, brought to Ruby in full: algebraic data types,
|
|
4
|
+
clauses that destructure their arguments, and a compiler that refuses to build
|
|
5
|
+
a function with a hole in it. The matcher is written in Rust (via
|
|
6
|
+
[Rutie](https://github.com/danielpclark/rutie)), compiles each function once
|
|
7
|
+
into a decision tree, and runs it directly over Ruby values.
|
|
8
|
+
|
|
9
|
+
```ruby
|
|
10
|
+
require "haskell_match"
|
|
11
|
+
|
|
12
|
+
HaskellMatch.data "Shape = Circle Double | Rect Double Double | Triangle Double Double Double"
|
|
13
|
+
include Shape
|
|
14
|
+
|
|
15
|
+
area = HaskellMatch.fn(:area) do
|
|
16
|
+
on("Circle r") { |r| 3.14159 * r * r }
|
|
17
|
+
on("Rect w h") { |w, h| w * h }
|
|
18
|
+
on(Triangle(a, b, c)) do |a, b, c|
|
|
19
|
+
s = (a + b + c) / 2.0
|
|
20
|
+
Math.sqrt(s * (s - a) * (s - b) * (s - c))
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
area.(Rect.new(2, 3)) # => 6
|
|
25
|
+
area.(Triangle[3, 4, 5]) # => 6.0
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Two of those clauses are Haskell written in a string; the third is the same
|
|
29
|
+
pattern written as Ruby. Both forms build the same decision tree, and you can
|
|
30
|
+
use whichever reads better, line by line.
|
|
31
|
+
|
|
32
|
+
## Why every path must be accounted for
|
|
33
|
+
|
|
34
|
+
In most Ruby code, the case you forgot is found by a user. A `case` with no
|
|
35
|
+
`else` silently returns `nil`; an `if` chain that misses a branch falls
|
|
36
|
+
through; a `Hash#fetch` without a default raises in production at three in the
|
|
37
|
+
morning. The fix is always the same, and always late: add the branch you did
|
|
38
|
+
not think of.
|
|
39
|
+
|
|
40
|
+
Haskell inverts this. A function defined by patterns is a *claim* about the
|
|
41
|
+
shape of its input, and the compiler checks the claim: if the clauses do not
|
|
42
|
+
cover every value the type allows, the program does not compile, and the
|
|
43
|
+
message tells you exactly which values fell through. haskell_match brings that
|
|
44
|
+
discipline to Ruby at definition time:
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
HaskellMatch.fn(:area) do
|
|
48
|
+
on("Circle r") { |r| 3.14159 * r * r }
|
|
49
|
+
on("Rect w h") { |w, h| w * h }
|
|
50
|
+
end
|
|
51
|
+
# HaskellMatch::NonExhaustiveError:
|
|
52
|
+
# Pattern match(es) are non-exhaustive
|
|
53
|
+
# In an equation for 'area':
|
|
54
|
+
# Patterns not matched:
|
|
55
|
+
# Triangle _ _ _
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
This changes how code evolves. Add a constructor to a type (`| Hexagon
|
|
59
|
+
Double`) and every function that matches on that type fails to load, each one
|
|
60
|
+
pointing at the exact case to write. There is no grep for call sites, no
|
|
61
|
+
"should be fine", no test suite hoping to cover the new branch: the compiler
|
|
62
|
+
has already enumerated the paths and found the missing one. Teams that adopt
|
|
63
|
+
this stop writing defensive `else raise "unreachable"` branches, because
|
|
64
|
+
unreachable is now something the compiler proves rather than something a
|
|
65
|
+
comment asserts.
|
|
66
|
+
|
|
67
|
+
The same analysis catches the opposite mistake, a clause that can never run:
|
|
68
|
+
|
|
69
|
+
```ruby
|
|
70
|
+
HaskellMatch.data "Maybe a = Nothing | Just a"
|
|
71
|
+
include Maybe
|
|
72
|
+
|
|
73
|
+
HaskellMatch.fn(:describe) do
|
|
74
|
+
on("_") { "something" }
|
|
75
|
+
on("Just x") { |x| "just #{x}" }
|
|
76
|
+
end
|
|
77
|
+
# HaskellMatch::RedundantClauseError:
|
|
78
|
+
# Pattern match is redundant
|
|
79
|
+
# In an equation for 'describe':
|
|
80
|
+
# describe Just x = ... (example.rb:3)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Redundant clauses are dead code with a story: usually an earlier clause grew
|
|
84
|
+
broader than intended, or two people each handled a case. Either way the
|
|
85
|
+
compiler found the contradiction in the reasoning before it became a bug.
|
|
86
|
+
|
|
87
|
+
The check is precise, not merely cautious. Nested patterns, literals, lists,
|
|
88
|
+
tuples and records are all enumerated, and the witnesses are concrete:
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
HaskellMatch.data "Tree a = Leaf | Node (Tree a) a (Tree a)"
|
|
92
|
+
include Tree
|
|
93
|
+
|
|
94
|
+
HaskellMatch.fn(:depth) do
|
|
95
|
+
on("Leaf") { 0 }
|
|
96
|
+
on("Node Leaf _ Leaf") { 1 }
|
|
97
|
+
on("Node (Node _ _ _) _ _") { |*| :deep }
|
|
98
|
+
end
|
|
99
|
+
# Patterns not matched:
|
|
100
|
+
# Node Leaf _ (Node _ _ _)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Haskell's type checker also rejects a function that matches a `Maybe` in one
|
|
104
|
+
clause and a list in another. Ruby has no static types, so haskell_match
|
|
105
|
+
checks what it can at definition time (all clauses must agree on the shape of
|
|
106
|
+
each position) and defers the rest to the call: a value that *no* clause could
|
|
107
|
+
accept raises `TypeMismatchError`, while `_` and variables accept anything,
|
|
108
|
+
exactly as Haskell's wildcard does.
|
|
109
|
+
|
|
110
|
+
## A tour, in both dialects
|
|
111
|
+
|
|
112
|
+
Everything below mixes the two ways of writing a pattern. A String given to
|
|
113
|
+
`on` is Haskell syntax; anything else is the pattern written in place, where
|
|
114
|
+
bare names are variables, `_` is the wildcard, `[x, *xs]` is `(x:xs)`, and
|
|
115
|
+
`Just(x)` or `Just[x]` applies a constructor.
|
|
116
|
+
|
|
117
|
+
### Data types and constructors
|
|
118
|
+
|
|
119
|
+
```ruby
|
|
120
|
+
HaskellMatch.data "Either a b = Left a | Right b"
|
|
121
|
+
HaskellMatch.data "Contact = Person { name :: String, age :: Int }"
|
|
122
|
+
include Either
|
|
123
|
+
include Contact
|
|
124
|
+
|
|
125
|
+
Just.new(1) # => Just 1
|
|
126
|
+
Just[Just[Nothing]] # => Just (Just Nothing)
|
|
127
|
+
Person.new(name: "Ann", age: 30) # => Person {name = "Ann", age = 30}
|
|
128
|
+
[1, 2].map(&Just) # => [Just 1, Just 2]
|
|
129
|
+
Nothing.frozen? # => true
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Constructors are frozen `Data` values: they compare by value, hash, print as
|
|
133
|
+
Haskell would, and work with Ruby's own `case/in` too.
|
|
134
|
+
|
|
135
|
+
### Maybe and Either, the everyday cases
|
|
136
|
+
|
|
137
|
+
```ruby
|
|
138
|
+
from_maybe = HaskellMatch.fn(:from_maybe) do
|
|
139
|
+
on("d", "Nothing") { |d| d }
|
|
140
|
+
on(_, Just(x)) { |x| x }
|
|
141
|
+
end
|
|
142
|
+
from_maybe.(0, Just.new(5)) # => 5
|
|
143
|
+
from_maybe.(0, Nothing) # => 0
|
|
144
|
+
|
|
145
|
+
either = HaskellMatch.fn(:either) do
|
|
146
|
+
on("Left e") { |e| "error: #{e}" }
|
|
147
|
+
on(Right(v)) { |v| "ok: #{v}" }
|
|
148
|
+
end
|
|
149
|
+
either.(Left.new("boom")) # => "error: boom"
|
|
150
|
+
either.(Right[42]) # => "ok: 42"
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
### Lists, strings and recursion
|
|
154
|
+
|
|
155
|
+
A Ruby Array is a Haskell list, and so is a Ruby String (`String = [Char]`):
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
length = HaskellMatch.fn(:length) do
|
|
159
|
+
on("[]") { 0 }
|
|
160
|
+
on([_, *xs]) { |xs| 1 + length.(xs) }
|
|
161
|
+
end
|
|
162
|
+
length.([1, 2, 3]) # => 3
|
|
163
|
+
length.("haskell") # => 7
|
|
164
|
+
|
|
165
|
+
zip = HaskellMatch.fn(:zip) do
|
|
166
|
+
on("(x:xs)", [y, *ys]) { |x, xs, y, ys| [[x, y]] + zip.(xs, ys) }
|
|
167
|
+
on("_", "_") { [] }
|
|
168
|
+
end
|
|
169
|
+
zip.([1, 2, 3], %w[a b]) # => [[1, "a"], [2, "b"]]
|
|
170
|
+
|
|
171
|
+
greeting = HaskellMatch.fn(:greeting) do
|
|
172
|
+
on('""') { "Hello, stranger" }
|
|
173
|
+
on("('A':_)") { "Hello, A-person" }
|
|
174
|
+
on([c, *_]) { |c| "Hello, #{c.upcase}-person" }
|
|
175
|
+
end
|
|
176
|
+
greeting.("") # => "Hello, stranger"
|
|
177
|
+
greeting.("Ann") # => "Hello, A-person"
|
|
178
|
+
greeting.("bob") # => "Hello, B-person"
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
Bound tails share storage with the original (a copy-on-write slice), so
|
|
182
|
+
recursing down a list does not copy it.
|
|
183
|
+
|
|
184
|
+
### Guards, as-patterns and nesting
|
|
185
|
+
|
|
186
|
+
```ruby
|
|
187
|
+
classify = HaskellMatch.fn(:classify) do
|
|
188
|
+
on(Just(x), guard: ->(x) { x.negative? }) { :negative }
|
|
189
|
+
on("Just 0") { :zero }
|
|
190
|
+
on(Just(x), where: ->(x) { x.even? }) { :even }
|
|
191
|
+
on("Just _") { :odd }
|
|
192
|
+
on(Nothing) { :none }
|
|
193
|
+
end
|
|
194
|
+
[Just[-1], Just[0], Just[2], Just[3], Nothing].map(&classify)
|
|
195
|
+
# => [:negative, :zero, :even, :odd, :none]
|
|
196
|
+
|
|
197
|
+
dedupe = HaskellMatch.fn(:dedupe) do
|
|
198
|
+
on("(x:rest@(y:_))", guard: ->(x, y) { x == y }) { |rest| dedupe.(rest) }
|
|
199
|
+
on([x, *rest]) { |x, rest| [x] + dedupe.(rest) }
|
|
200
|
+
on([]) { [] }
|
|
201
|
+
end
|
|
202
|
+
dedupe.([1, 1, 2, 3, 3, 3, 4]) # => [1, 2, 3, 4]
|
|
203
|
+
|
|
204
|
+
flatten_maybe = HaskellMatch.fn(:flatten_maybe) do
|
|
205
|
+
on("Just (Just x)") { |x| Just[x] }
|
|
206
|
+
on(Just(Nothing)) { Nothing }
|
|
207
|
+
on(Nothing) { Nothing }
|
|
208
|
+
end
|
|
209
|
+
flatten_maybe.(Just[Just[7]]) # => Just 7
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
A guarded clause may fall through, so (as in GHC) it does not count towards
|
|
213
|
+
coverage; `otherwise` does.
|
|
214
|
+
|
|
215
|
+
### Records and tuples
|
|
216
|
+
|
|
217
|
+
```ruby
|
|
218
|
+
can_vote = HaskellMatch.fn(:can_vote) do
|
|
219
|
+
on("Person { age = a }", guard: ->(a) { a >= 18 }) { true }
|
|
220
|
+
on(Person(**_)) { false }
|
|
221
|
+
end
|
|
222
|
+
can_vote.(Person.new("Ann", 30)) # => true
|
|
223
|
+
|
|
224
|
+
introduce = HaskellMatch.fn(:introduce) do
|
|
225
|
+
on(Person(name: n, age: a)) { |n, a| "#{n} is #{a}" }
|
|
226
|
+
end
|
|
227
|
+
introduce.(Person.new("Bob", 7)) # => "Bob is 7"
|
|
228
|
+
|
|
229
|
+
swap = HaskellMatch.fn(:swap) { on("(a, b)") { |a, b| [b, a] } }
|
|
230
|
+
swap.([1, 2]) # => [2, 1]
|
|
231
|
+
|
|
232
|
+
dist = HaskellMatch.fn(:dist) { on(tuple(x1, y1), tuple(x2, y2)) { |x1, y1, x2, y2| Math.hypot(x2 - x1, y2 - y1) } }
|
|
233
|
+
dist.([0, 0], [3, 4]) # => 5.0
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Tuples are Arrays of a fixed length, lists are Arrays of any length; the
|
|
237
|
+
compiler keeps the two apart just as Haskell does.
|
|
238
|
+
|
|
239
|
+
### Expressions, patterns as objects, and methods
|
|
240
|
+
|
|
241
|
+
```ruby
|
|
242
|
+
# case ... of
|
|
243
|
+
HaskellMatch.case_of(Just[3]) do
|
|
244
|
+
on("Just x", guard: ->(x) { x > 10 }) { |x| "big #{x}" }
|
|
245
|
+
on(Just(x)) { |x| "just #{x}" }
|
|
246
|
+
on(Nothing) { "nothing" }
|
|
247
|
+
end # => "just 3"
|
|
248
|
+
|
|
249
|
+
# a pattern on its own, usable in case/when
|
|
250
|
+
head = HaskellMatch.pattern { Just([x, *_]) }
|
|
251
|
+
head.match(Just[[9, 8]]) # => {:x=>9}
|
|
252
|
+
head === Just[[]] # => false
|
|
253
|
+
|
|
254
|
+
# methods defined by clauses, with access to self
|
|
255
|
+
class Account
|
|
256
|
+
extend HaskellMatch::DSL
|
|
257
|
+
include Maybe
|
|
258
|
+
|
|
259
|
+
attr_reader :balance
|
|
260
|
+
def initialize(balance) = @balance = balance
|
|
261
|
+
def limit = 100
|
|
262
|
+
|
|
263
|
+
hdef :deposit do
|
|
264
|
+
on(Nothing) { self }
|
|
265
|
+
on(Just(amt), guard: ->(amt) { amt <= limit }) { |amt| Account.new(balance + amt) }
|
|
266
|
+
on("Just amt") { |amt| raise ArgumentError, "over limit: #{amt}" }
|
|
267
|
+
end
|
|
268
|
+
end
|
|
269
|
+
Account.new(10).deposit(Just[50]).balance # => 60
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
### Bindings your way
|
|
273
|
+
|
|
274
|
+
Name your block parameters after the pattern's variables and take any subset
|
|
275
|
+
in any order; keywords work too:
|
|
276
|
+
|
|
277
|
+
```ruby
|
|
278
|
+
f = HaskellMatch.fn(:f) do
|
|
279
|
+
on("(x:xs)") { |xs| xs } # by name
|
|
280
|
+
on("[]") { [] }
|
|
281
|
+
end
|
|
282
|
+
f.([1, 2, 3]) # => [2, 3]
|
|
283
|
+
|
|
284
|
+
g = HaskellMatch.fn(:g) { on([x, *xs]) { |x:, xs:| { x: x, xs: xs } }; on([]) { {} } }
|
|
285
|
+
g.([1, 2]) # => {:x=>1, :xs=>[2]}
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
## Recursion without limits, and laziness
|
|
289
|
+
|
|
290
|
+
Haskell programs loop by recursing and process infinite data by being lazy.
|
|
291
|
+
Ruby's VM gives a fixed stack to each thread and evaluates eagerly, so a
|
|
292
|
+
faithful port has to supply both. haskell_match does, in three complementary
|
|
293
|
+
ways.
|
|
294
|
+
|
|
295
|
+
### Plain recursion goes as deep as memory allows
|
|
296
|
+
|
|
297
|
+
```ruby
|
|
298
|
+
count = HaskellMatch.fn(:count) do
|
|
299
|
+
on([]) { 0 }
|
|
300
|
+
on([_, *xs]) { |xs| 1 + count.(xs) }
|
|
301
|
+
end
|
|
302
|
+
count.((1..200_000).to_a) # => 200000
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
A Ruby lambda written the same way dies with `SystemStackError` around 10,000
|
|
306
|
+
levels. Here the native call runs every hundredth nested body on a fresh
|
|
307
|
+
Fiber, chaining their stacks the way GHC grows its own, so the depth limit
|
|
308
|
+
becomes memory rather than a fixed buffer. You write the obvious code and it
|
|
309
|
+
works; the cost is about 1.6 KB per level while the recursion is pending, and
|
|
310
|
+
`HaskellMatch.max_depth` (250,000 by default) turns a runaway recursion into
|
|
311
|
+
a clear `StackOverflowError` instead of a swallowed machine. A function that
|
|
312
|
+
is *meant* to recurse deep can be defined with `deep: true`: its body is then
|
|
313
|
+
invoked from Ruby rather than from the native matcher, which costs about one
|
|
314
|
+
extra Ruby frame per call but brings a level down to about 100 bytes and
|
|
315
|
+
makes garbage collection at depth six times cheaper.
|
|
316
|
+
|
|
317
|
+
### Tail calls run in constant space
|
|
318
|
+
|
|
319
|
+
```ruby
|
|
320
|
+
sum = HaskellMatch.fn(:sum) do
|
|
321
|
+
on(acc, []) { |acc| acc }
|
|
322
|
+
on(acc, [x, *xs]) { |acc, x, xs| sum.tail(acc + x, xs) }
|
|
323
|
+
end
|
|
324
|
+
sum.(0, (1..1_000_000).to_a) # => 500000500000
|
|
325
|
+
|
|
326
|
+
collatz_steps = HaskellMatch.fn(:collatz_steps) do
|
|
327
|
+
on(1, n) { |n| n }
|
|
328
|
+
on(k, n, guard: ->(k) { k.even? }) { |k, n| collatz_steps.tail(k / 2, n + 1) }
|
|
329
|
+
on(k, n) { |k, n| collatz_steps.tail(3 * k + 1, n + 1) }
|
|
330
|
+
end
|
|
331
|
+
collatz_steps.(27, 0) # => 111
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`f.tail(args)` is Haskell's tail call made explicit: the native loop replaces
|
|
335
|
+
the arguments and matches again on the same frame. Any loop a Haskell program
|
|
336
|
+
would write as an accumulator, a fold, a state machine or a server loop runs
|
|
337
|
+
this way in O(1) space, including mutual recursion between functions, with
|
|
338
|
+
nothing counted against the depth limit.
|
|
339
|
+
|
|
340
|
+
### Deferred calls keep deep recursion cheap
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
length = HaskellMatch.fn(:length) do
|
|
344
|
+
on([]) { 0 }
|
|
345
|
+
on([_, *xs]) { |xs| length.defer(xs) { |n| 1 + n } } # 1 + length xs
|
|
346
|
+
end
|
|
347
|
+
length.((1..2_000_000).to_a) # => 2000000
|
|
348
|
+
```
|
|
349
|
+
|
|
350
|
+
`defer` names the continuation that plain recursion leaves implicit. The
|
|
351
|
+
pending blocks live on a heap stack owned by the native call, about 200 bytes
|
|
352
|
+
each, chunked so Ruby's generational GC never rescans the whole stack. Use it
|
|
353
|
+
when a non-tail recursion is known to go very deep and memory matters.
|
|
354
|
+
|
|
355
|
+
### Lazy lists make infinite data ordinary
|
|
356
|
+
|
|
357
|
+
```ruby
|
|
358
|
+
take = HaskellMatch.fn(:take) do
|
|
359
|
+
on(0, _) { [] }
|
|
360
|
+
on(_, []) { [] }
|
|
361
|
+
on(n, [x, *xs]) { |n, x, xs| [x] + take.(n - 1, xs) }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
naturals = HaskellMatch.lazy(1..)
|
|
365
|
+
take.(5, naturals) # => [1, 2, 3, 4, 5]
|
|
366
|
+
|
|
367
|
+
powers = HaskellMatch::LazyList.iterate(1) { |x| x * 2 }
|
|
368
|
+
take.(8, powers) # => [1, 2, 4, 8, 16, 32, 64, 128]
|
|
369
|
+
|
|
370
|
+
fibs = HaskellMatch.lazy(Enumerator.new { |y| a, b = 0, 1; loop { y << a; a, b = b, a + b } })
|
|
371
|
+
take.(10, fibs) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
|
|
372
|
+
|
|
373
|
+
# any Enumerator is a list; Ruby's lazy pipelines compose with the patterns
|
|
374
|
+
take.(3, (1..).lazy.select(&:even?).map { |x| x * x }) # => [4, 16, 36]
|
|
375
|
+
```
|
|
376
|
+
|
|
377
|
+
A `LazyList` is a memoised cons list: `(x:xs)` forces one cell, binds `xs` to
|
|
378
|
+
the rest still unevaluated, and every forced cell is computed once and shared
|
|
379
|
+
by all consumers, which is exactly Haskell's evaluation model for lists. The
|
|
380
|
+
advantages carry over intact. Producers and consumers are written separately
|
|
381
|
+
and composed; a generator never needs to know how much of it will be used;
|
|
382
|
+
`take.(n, expensive_stream)` does `n` units of work and no more; and the same
|
|
383
|
+
`take` serves finite Arrays, Strings, lazy lists and Ruby Enumerators without
|
|
384
|
+
a line changing, because they are all one type to the pattern compiler.
|
|
385
|
+
|
|
386
|
+
Together these give Ruby the two things Haskell relies on for "infinite"
|
|
387
|
+
programs: loops that do not consume stack, and data that does not have to
|
|
388
|
+
exist before it is asked for.
|
|
389
|
+
|
|
390
|
+
## Or simply write Haskell
|
|
391
|
+
|
|
392
|
+
Everything above is Haskell's pattern matching with Ruby expressions in the
|
|
393
|
+
clause bodies. When a function is clearer in Haskell itself, write it in
|
|
394
|
+
Haskell. `HaskellMatch.haskell` compiles a Haskell 2010 subset (data
|
|
395
|
+
declarations, equations, guards, `where`, `let`, `case`, lambdas, sections,
|
|
396
|
+
list comprehensions, ranges, and a lazy Prelude) into methods on a Ruby
|
|
397
|
+
module, with the same exhaustiveness and redundancy checks as everything
|
|
398
|
+
else in this library:
|
|
399
|
+
|
|
400
|
+
```ruby
|
|
401
|
+
Shapes = HaskellMatch.haskell(<<~HS)
|
|
402
|
+
data Shape = Circle Double | Rect Double Double
|
|
403
|
+
|
|
404
|
+
area :: Shape -> Double
|
|
405
|
+
area (Circle r) = 3 * r * r
|
|
406
|
+
area (Rect w h) = w * h
|
|
407
|
+
|
|
408
|
+
describe :: Shape -> String
|
|
409
|
+
describe s
|
|
410
|
+
| a > 10 = "big " ++ kind
|
|
411
|
+
| otherwise = "small " ++ kind
|
|
412
|
+
where
|
|
413
|
+
a = area s
|
|
414
|
+
kind = case s of
|
|
415
|
+
Circle _ -> "circle"
|
|
416
|
+
Rect w h | w == h -> "square"
|
|
417
|
+
| otherwise -> "rectangle"
|
|
418
|
+
|
|
419
|
+
totalArea :: [Shape] -> Double
|
|
420
|
+
totalArea = sum . map area
|
|
421
|
+
HS
|
|
422
|
+
|
|
423
|
+
shapes = [Shapes::Circle.new(1.0), Shapes::Rect.new(2.0, 2.0), Shapes::Rect.new(3.0, 5.0)]
|
|
424
|
+
shapes.map { |s| Shapes.describe(s) } # => ["small circle", "small square", "big rectangle"]
|
|
425
|
+
Shapes.total_area(shapes) # => 22.0
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
Haskell names arrive as Ruby methods (`totalArea` is also `total_area`),
|
|
429
|
+
constructors as constants, and the recursion and laziness machinery is the
|
|
430
|
+
one described above: `where` helpers that call themselves in tail position
|
|
431
|
+
run in constant space, and a list built with `:` is as lazy as Haskell's, so
|
|
432
|
+
the classic infinite definitions work unchanged.
|
|
433
|
+
|
|
434
|
+
```ruby
|
|
435
|
+
Nums = HaskellMatch.haskell(<<~HS)
|
|
436
|
+
primes :: [Int]
|
|
437
|
+
primes = sieve [2..]
|
|
438
|
+
where
|
|
439
|
+
sieve [] = []
|
|
440
|
+
sieve (p:xs) = p : sieve [x | x <- xs, x `mod` p /= 0]
|
|
441
|
+
|
|
442
|
+
fibs :: [Integer]
|
|
443
|
+
fibs = 0 : 1 : zipWith (+) fibs (tail fibs)
|
|
444
|
+
|
|
445
|
+
sumTo :: Int -> Int
|
|
446
|
+
sumTo n = go n 0
|
|
447
|
+
where
|
|
448
|
+
go 0 acc = acc
|
|
449
|
+
go k acc = go (k - 1) (acc + k)
|
|
450
|
+
HS
|
|
451
|
+
|
|
452
|
+
Nums.primes.take(8) # => [2, 3, 5, 7, 11, 13, 17, 19]
|
|
453
|
+
Nums.fibs.take(10) # => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
|
|
454
|
+
Nums.sum_to(1_000_000) # => 500000500000
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
The two languages call each other freely. A name the Haskell does not define
|
|
458
|
+
is a Ruby method of the host module, so Haskell can lean on Ruby for
|
|
459
|
+
formatting, I/O or anything else; and Ruby can match on the module's types
|
|
460
|
+
with the module's own `fn`, `case_of` and `pattern`, in either pattern
|
|
461
|
+
dialect:
|
|
462
|
+
|
|
463
|
+
```ruby
|
|
464
|
+
module Report
|
|
465
|
+
def self.money(x)
|
|
466
|
+
format("$%.2f", x)
|
|
467
|
+
end
|
|
468
|
+
|
|
469
|
+
extend HaskellMatch::Haskell
|
|
470
|
+
haskell <<~HS
|
|
471
|
+
data Line = Line String Double Int
|
|
472
|
+
|
|
473
|
+
total :: [Line] -> Double
|
|
474
|
+
total ls = sum [price * fromIntegral qty | Line _ price qty <- ls]
|
|
475
|
+
|
|
476
|
+
summary :: [Line] -> String
|
|
477
|
+
summary [] = "nothing ordered"
|
|
478
|
+
summary ls = show (length ls) ++ " lines, " ++ money (total ls)
|
|
479
|
+
HS
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
order = [Report::Line.new("tea", 2.5, 2), Report::Line.new("cake", 4.0, 1)]
|
|
483
|
+
Report.summary(order) # => "2 lines, $9.00"
|
|
484
|
+
Report.summary([]) # => "nothing ordered"
|
|
485
|
+
|
|
486
|
+
label = Report.fn(:label) do
|
|
487
|
+
on(Line(name, _, 1)) { |name| name }
|
|
488
|
+
on("Line name _ qty") { |name, qty| "#{qty} x #{name}" }
|
|
489
|
+
end
|
|
490
|
+
order.map { |l| label.(l) } # => ["2 x tea", "cake"]
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
And a Haskell file is just a Haskell file. `HaskellMatch.require` finds
|
|
494
|
+
`name.hs` on the load path (or takes a path) and defines a constant named
|
|
495
|
+
after its `module` header; `HaskellMatch.load` returns an anonymous module.
|
|
496
|
+
No templating and no interpolation: the file is the Haskell 2010 subset
|
|
497
|
+
described in [Inline Haskell and `.hs` files](#inline-haskell-and-hs-files).
|
|
498
|
+
|
|
499
|
+
```ruby
|
|
500
|
+
HaskellMatch.require "examples/geometry" # examples/geometry.hs: `module Geometry where ...`
|
|
501
|
+
Geometry.describe(Geometry::Triangle.new(3.0, 4.0, 5.0)) # => "small triangle"
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
## At a glance
|
|
505
|
+
|
|
506
|
+
* **Haskell pattern syntax**, quoted or written in place: constructors,
|
|
507
|
+
literals, variables, wildcards, lists, tuples, as-patterns `all@(x:_)`,
|
|
508
|
+
lazy patterns `~p`, bang patterns `!x`, record patterns `Person { name = n, .. }`,
|
|
509
|
+
guards and `otherwise`, with both forms freely mixed.
|
|
510
|
+
* **Algebraic data types** declared with Haskell `data` syntax; constructors
|
|
511
|
+
are frozen `Data` values with Haskell-style `inspect`.
|
|
512
|
+
* **Every logical path accounted for**: non-exhaustive and redundant clauses
|
|
513
|
+
are definition-time errors with GHC-style messages; positions whose
|
|
514
|
+
patterns disagree in type are a compile error; `_` and variables match
|
|
515
|
+
anything, and a value no pattern can accept raises `TypeMismatchError`.
|
|
516
|
+
* **Strings are lists of characters**, as in Haskell; lazy lists and
|
|
517
|
+
Enumerators are lists too.
|
|
518
|
+
* **Recursion without Ruby's stack limit**: chained Fiber stacks, constant-
|
|
519
|
+
space tail calls, deferred continuations, and a depth guard.
|
|
520
|
+
* **Fast**: one Maranget-style decision tree per function, matching in Rust
|
|
521
|
+
over Ruby `VALUE`s with no allocation until a clause is chosen, and list
|
|
522
|
+
tails as shared slices.
|
|
523
|
+
* **Thread-, fiber- and Ractor-safe**, with `ractor: true` producing
|
|
524
|
+
shareable functions.
|
|
525
|
+
* **Haskell itself**, inline or from `.hs` files: a Haskell 2010 subset
|
|
526
|
+
compiled to Ruby methods, with a lazy Prelude and two-way interop.
|
|
527
|
+
|
|
528
|
+
## Installation
|
|
529
|
+
|
|
530
|
+
You need Ruby ≥ 3.2 and a Rust toolchain (`cargo`). The gem builds the
|
|
531
|
+
extension on install:
|
|
532
|
+
|
|
533
|
+
```sh
|
|
534
|
+
gem install haskell_match # or add it to your Gemfile
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
From a checkout:
|
|
538
|
+
|
|
539
|
+
```sh
|
|
540
|
+
bundle install
|
|
541
|
+
bundle exec rake compile # cargo build --release, copies the library into lib/
|
|
542
|
+
bundle exec rake test # Ruby test suite (compiles first)
|
|
543
|
+
bundle exec rake cargo:test # Rust unit tests for the pure core
|
|
544
|
+
bundle exec rake bench
|
|
545
|
+
```
|
|
546
|
+
|
|
547
|
+
The library is loaded with `Fiddle`; set `HASKELL_MATCH_NATIVE` to point at a
|
|
548
|
+
specific build if needed.
|
|
549
|
+
|
|
550
|
+
## Declaring data types
|
|
551
|
+
|
|
552
|
+
```ruby
|
|
553
|
+
HaskellMatch.data "Maybe a = Nothing | Just a"
|
|
554
|
+
HaskellMatch.data "Either a b = Left a | Right b"
|
|
555
|
+
HaskellMatch.data "Tree a = Leaf | Node (Tree a) a (Tree a)"
|
|
556
|
+
HaskellMatch.data "Person = Person { name :: String, age :: Int }"
|
|
557
|
+
HaskellMatch.data "Color = Red | Green | Blue deriving (Show, Eq)" # deriving is accepted and ignored
|
|
558
|
+
|
|
559
|
+
# the same, without the declaration syntax
|
|
560
|
+
HaskellMatch.data :Maybe, Nothing: 0, Just: 1
|
|
561
|
+
HaskellMatch.data :Person, Person: { name: :String, age: :Int }
|
|
562
|
+
```
|
|
563
|
+
|
|
564
|
+
`HaskellMatch.data` defines a module named after the type (as a top-level
|
|
565
|
+
constant by default; pass `under: SomeModule`, or `under: nil` to get it back
|
|
566
|
+
without defining a constant). The module holds one constant per constructor,
|
|
567
|
+
so `include Maybe` brings `Just` and `Nothing` into scope.
|
|
568
|
+
|
|
569
|
+
* Constructors with fields are `Data` classes: `Just.new(1)`, `Just[1]`,
|
|
570
|
+
`Just.(1)`, `[1, 2].map(&Just)`. Positional fields are `_1`, `_2`, ...;
|
|
571
|
+
record fields have their own names. Values are frozen, compare by value,
|
|
572
|
+
hash, and print Haskell-style: `Just (Just 1)`, `Person {name = "Ann", age = 30}`.
|
|
573
|
+
They also work with Ruby's own `case/in` (`deconstruct_keys`).
|
|
574
|
+
* Nullary constructors are singleton values: `Nothing`, `Red`.
|
|
575
|
+
* `Maybe === value` tests membership; `Maybe.constructors` lists them.
|
|
576
|
+
* Types are closed: declaring `data Pet = Dog | Cat` later with a different
|
|
577
|
+
set of constructors replaces the type; existing compiled functions keep
|
|
578
|
+
working against the old values.
|
|
579
|
+
|
|
580
|
+
Haskell keeps types and constructors in different namespaces; Ruby does not.
|
|
581
|
+
For `data Person = Person {...}`, after `include Person` the name `Person`
|
|
582
|
+
refers to the constructor; the type module is `::Person` or
|
|
583
|
+
`Person.data_type`. The type module forwards `new`, `[]` and `call` to a
|
|
584
|
+
same-named constructor, so `Person.new("Al", 3)` builds a person either way.
|
|
585
|
+
|
|
586
|
+
## Patterns
|
|
587
|
+
|
|
588
|
+
| Haskell | Matches |
|
|
589
|
+
|----------------------------------|------------------------------------------------------------|
|
|
590
|
+
| `x`, `_`, `_name` | anything (variables bind, `_` does not) |
|
|
591
|
+
| `Just x`, `Nothing`, `Node l v r`| a constructor and its fields |
|
|
592
|
+
| `Person { name = n, age }` | record fields by name (`age` is a pun); `Person { .. }` binds all |
|
|
593
|
+
| `[]`, `[a, b]`, `(x:xs)`, `x:y:rest` | Ruby Arrays viewed as lists |
|
|
594
|
+
| `(a, b)`, `(a, b, c)`, `()` | Ruby Arrays of exactly that length |
|
|
595
|
+
| `True`, `False` | `true`, `false` |
|
|
596
|
+
| `0`, `-1`, `1.5`, `0xFF`, `12345678901234567890` | numbers (`0` also matches `0.0`, as in Haskell) |
|
|
597
|
+
| `'c'` | a character: a one-character Ruby String, or one character of a String matched as a list |
|
|
598
|
+
| `"text"` | the list `['t', 'e', 'x', 't']`: `String = [Char]`, so `f "" = ...; f (c:cs) = ...` works on Ruby Strings |
|
|
599
|
+
| `:sym`, `:"quoted"` | Ruby Symbols (an extension) |
|
|
600
|
+
| `all@(x:_)` | as-pattern |
|
|
601
|
+
| `~(a, b)` | lazy (irrefutable) pattern: always matches; destructured only when the clause runs |
|
|
602
|
+
| `!x` | bang pattern (accepted; Ruby is strict anyway) |
|
|
603
|
+
| `Data.Maybe.Just x` | qualification is ignored |
|
|
604
|
+
|
|
605
|
+
Comments (`-- ...`, `{- ... -}`) are allowed inside patterns.
|
|
606
|
+
|
|
607
|
+
Ruby Strings are lists of characters wherever a list pattern appears: `[]`
|
|
608
|
+
matches `""`, `(c:cs)` binds `c` to a one-character String and `cs` to the
|
|
609
|
+
rest (a String), and `['y', _]` matches any two-character String starting
|
|
610
|
+
with `y`. The same patterns match Arrays of one-character Strings. `Char` and
|
|
611
|
+
`String` are different types, as in Haskell: `'a'` and `"a"` cannot appear in
|
|
612
|
+
the same position.
|
|
613
|
+
|
|
614
|
+
### Wildcards and run-time types
|
|
615
|
+
|
|
616
|
+
`_` and variables match any value and never fail. Constructor and literal
|
|
617
|
+
patterns fail on values of another type, which simply moves matching on to
|
|
618
|
+
the next clause. Only when no pattern of the function can accept a value is
|
|
619
|
+
`HaskellMatch::TypeMismatchError` raised, the run-time counterpart of the
|
|
620
|
+
compile error Haskell would give:
|
|
621
|
+
|
|
622
|
+
```ruby
|
|
623
|
+
f = HaskellMatch.fn(:f) { on("Just x") { |x| x }; on("_") { :other } }
|
|
624
|
+
f.(5) # => :other `_` accepts anything
|
|
625
|
+
g = HaskellMatch.fn(:g) { on("Just x") { |x| x }; on("Nothing") { 0 } }
|
|
626
|
+
g.(5) # TypeMismatchError: expected a value of type Maybe but got 5 (Integer)
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
### Patterns without quotes
|
|
630
|
+
|
|
631
|
+
Inside a definition block the pattern can be written as a Ruby expression.
|
|
632
|
+
Bare lower-case names are variables, `_` is the wildcard, and the expression
|
|
633
|
+
is rendered to the Haskell syntax above and compiled identically, so
|
|
634
|
+
exhaustiveness checks, errors and speed are the same. Quoted and in-place
|
|
635
|
+
patterns can be mixed, even within one clause.
|
|
636
|
+
|
|
637
|
+
| In place | Haskell |
|
|
638
|
+
|--------------------------------|-----------------------------|
|
|
639
|
+
| `x`, `_`, `var(:name)` | `x`, `_`, `name` (`var` for a name taken by a method or local) |
|
|
640
|
+
| `Just(x)`, `Just[x]`, `Nothing`| `Just x`, `Nothing` |
|
|
641
|
+
| `Node(Leaf, v, Node(_, _, _))` | `Node Leaf v (Node _ _ _)` |
|
|
642
|
+
| `Person(name: n, age: _)` | `Person { name = n, age = _ }` |
|
|
643
|
+
| `Person(name: n, **_)` | `Person { name = n, .. }` |
|
|
644
|
+
| `[]`, `[a, b]` | `[]`, `[a, b]` |
|
|
645
|
+
| `[x, *xs]`, `cons(x, xs)` | `(x:xs)` |
|
|
646
|
+
| `[x, y, *_]` | `(x:y:_)` |
|
|
647
|
+
| `tuple(a, b)`, `unit` | `(a, b)`, `()` |
|
|
648
|
+
| `as(all, [x, *_])` | `all@(x:_)` |
|
|
649
|
+
| `lazy(tuple(a, b))`, `bang(x)` | `~(a, b)`, `!x` |
|
|
650
|
+
| `0`, `-1`, `1.5`, `true`, `:ok`| `0`, `-1`, `1.5`, `True`, `:ok` |
|
|
651
|
+
| `str("abc")`, `char("c")` | `"abc"`, `'c'` |
|
|
652
|
+
|
|
653
|
+
A String handed directly to `on` is quoted pattern syntax (`on("Just x")`);
|
|
654
|
+
inside a pattern a Ruby String is a string literal (`Just("abc")`), and
|
|
655
|
+
`str("abc")` makes one at the top level. A bare name that is also a method
|
|
656
|
+
of the enclosing object (or a local variable) is not a pattern variable;
|
|
657
|
+
use `var(:name)` for it. `HaskellMatch.pattern { Just([x, *_]) }` builds a
|
|
658
|
+
standalone pattern the same way.
|
|
659
|
+
|
|
660
|
+
### Guards
|
|
661
|
+
|
|
662
|
+
```ruby
|
|
663
|
+
sign = HaskellMatch.fn(:sign) do
|
|
664
|
+
on("n", guard: ->(n) { n > 0 }) { 1 }
|
|
665
|
+
on("n", guard: ->(n) { n < 0 }) { -1 }
|
|
666
|
+
on("_", guard: otherwise) { 0 } # or simply on("_") { 0 }
|
|
667
|
+
end
|
|
668
|
+
```
|
|
669
|
+
|
|
670
|
+
A guarded clause does not count towards exhaustiveness (a guard may fail),
|
|
671
|
+
exactly as in GHC; `otherwise` does.
|
|
672
|
+
|
|
673
|
+
### Bindings
|
|
674
|
+
|
|
675
|
+
Bound variables are handed to the body in the order they appear in the
|
|
676
|
+
pattern. Name your block parameters after the variables and you may take any
|
|
677
|
+
subset in any order; keyword parameters work too:
|
|
678
|
+
|
|
679
|
+
```ruby
|
|
680
|
+
on("(x:xs)") { |xs| ... } # by name
|
|
681
|
+
on("(x:xs)") { |xs, x| ... } # reordered
|
|
682
|
+
on("(x:xs)") { |x:, xs:| ... } # keywords
|
|
683
|
+
on("(x:xs)") { |**all| all } # => { x: 1, xs: [2, 3] }
|
|
684
|
+
on("(x:xs)") { |*vals| vals } # positional, pattern order
|
|
685
|
+
```
|
|
686
|
+
|
|
687
|
+
Naming a parameter that the pattern does not bind is a `DefinitionError`.
|
|
688
|
+
|
|
689
|
+
### Recursion
|
|
690
|
+
|
|
691
|
+
A function is in scope inside its own clauses under its name, and as `recur`:
|
|
692
|
+
|
|
693
|
+
```ruby
|
|
694
|
+
fact = HaskellMatch.fn(:fact) do
|
|
695
|
+
on(0) { 1 }
|
|
696
|
+
on(n) { |n| n * fact.(n - 1) } # or recur.(n - 1)
|
|
697
|
+
end
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
Ruby's VM stack is fixed in size, so plain recursion written in Ruby dies at
|
|
701
|
+
roughly 10,000 levels. haskell_match removes that limit the way GHC's growable
|
|
702
|
+
stack does, with three mechanisms:
|
|
703
|
+
|
|
704
|
+
* **Stack segments (automatic).** Every `HaskellMatch.stack_segment` nested
|
|
705
|
+
calls (default 100) the next clause body runs in a fresh Fiber, which brings
|
|
706
|
+
its own VM and machine stacks. `1 + length.(xs)` therefore recurses as deep
|
|
707
|
+
as memory allows with no change to the code. The cost is about 1.6 KB per
|
|
708
|
+
level (each segment commits a 128 KB fiber VM stack), reclaimed when the
|
|
709
|
+
call returns. Non-local exits (`throw`, `break`) do not cross segment
|
|
710
|
+
boundaries.
|
|
711
|
+
* **Tail calls (constant space).** Return `function.tail(args...)` from a
|
|
712
|
+
body and the call continues with those arguments on the same frame, like a
|
|
713
|
+
Haskell loop:
|
|
714
|
+
|
|
715
|
+
```ruby
|
|
716
|
+
sum = HaskellMatch.fn(:sum) do
|
|
717
|
+
on(acc, []) { |acc| acc }
|
|
718
|
+
on(acc, [x, *xs]) { |acc, x, xs| sum.tail(acc + x, xs) }
|
|
719
|
+
end
|
|
720
|
+
sum.(0, (1..1_000_000).to_a) # => 500000500000
|
|
721
|
+
```
|
|
722
|
+
* **Deferred calls (cheap depth).** `function.defer(args...) { |result| ... }`
|
|
723
|
+
stands for a non-tail call whose result the block receives; the pending
|
|
724
|
+
blocks are kept on a heap-backed stack managed natively, at about 200 bytes
|
|
725
|
+
per level:
|
|
726
|
+
|
|
727
|
+
```ruby
|
|
728
|
+
length = HaskellMatch.fn(:length) do
|
|
729
|
+
on([]) { 0 }
|
|
730
|
+
on([_, *xs]) { |xs| length.defer(xs) { |n| 1 + n } } # 1 + length xs
|
|
731
|
+
end
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
`HaskellMatch.max_depth` (default 250,000 nested calls, about 400 MB at the
|
|
735
|
+
segment cost) raises `HaskellMatch::StackOverflowError` beyond that depth, so
|
|
736
|
+
a runaway recursion fails instead of taking the machine's memory; set it
|
|
737
|
+
higher, or to 0 for no limit, when a computation legitimately needs more.
|
|
738
|
+
Tail calls do not count towards the depth. The section "Deep and infinite
|
|
739
|
+
recursion: expert notes" below gives the measured costs and the semantics at
|
|
740
|
+
segment boundaries.
|
|
741
|
+
|
|
742
|
+
### Lazy lists
|
|
743
|
+
|
|
744
|
+
`HaskellMatch.lazy(enumerable)` builds a memoised lazy list; list patterns
|
|
745
|
+
match it element by element, forcing only what they inspect, so infinite
|
|
746
|
+
lists work as in Haskell. Ruby Enumerators (including `Enumerator::Lazy`)
|
|
747
|
+
given to a function are wrapped automatically, iterating from the start each
|
|
748
|
+
time.
|
|
749
|
+
|
|
750
|
+
```ruby
|
|
751
|
+
take = HaskellMatch.fn(:take) do
|
|
752
|
+
on(0, _) { [] }
|
|
753
|
+
on(_, []) { [] }
|
|
754
|
+
on(n, [x, *xs]) { |n, x, xs| [x] + take.(n - 1, xs) }
|
|
755
|
+
end
|
|
756
|
+
take.(5, HaskellMatch.lazy(1..)) # => [1, 2, 3, 4, 5]
|
|
757
|
+
take.(4, HaskellMatch::LazyList.iterate(1) { |x| x * 2 }) # => [1, 2, 4, 8]
|
|
758
|
+
take.(3, (1..).lazy.map { |x| x * x }) # => [1, 4, 9]
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
`LazyList` is Enumerable and offers `head`, `tail`, `take(n)`, `empty?`, plus
|
|
762
|
+
constructors `iterate`, `repeat`, `range`, `generate` and `empty`.
|
|
763
|
+
|
|
764
|
+
### Clause bodies and `self`
|
|
765
|
+
|
|
766
|
+
The definition block runs with `self` set to the clause builder. Method calls
|
|
767
|
+
the builder does not understand are forwarded to the object that owns the
|
|
768
|
+
block, so helper methods remain callable. Instance variables are not visible;
|
|
769
|
+
take the builder as a parameter when you need them:
|
|
770
|
+
|
|
771
|
+
```ruby
|
|
772
|
+
HaskellMatch.fn(:f) { |m| m.on("Just x") { |x| x + @offset }; m.on("Nothing") { @offset } }
|
|
773
|
+
```
|
|
774
|
+
|
|
775
|
+
Methods defined with `hdef` run their bodies with `self` set to the receiver
|
|
776
|
+
(see below).
|
|
777
|
+
|
|
778
|
+
## Three ways to match
|
|
779
|
+
|
|
780
|
+
### `HaskellMatch.fn` — compiled functions
|
|
781
|
+
|
|
782
|
+
```ruby
|
|
783
|
+
zip = HaskellMatch.fn(:zip) do
|
|
784
|
+
on("(x:xs)", "(y:ys)") { |x, xs, y, ys| [[x, y]] + zip.(xs, ys) }
|
|
785
|
+
on("_", "_") { [] }
|
|
786
|
+
end
|
|
787
|
+
zip.([1, 2, 3], %i[a b]) # => [[1, :a], [2, :b]]
|
|
788
|
+
zip.arity # => 2
|
|
789
|
+
zip.bindings # => [["x", "xs", "y", "ys"], []]
|
|
790
|
+
zip.to_proc, zip.curry, zip[a, b], zip === a # Proc-like protocol
|
|
791
|
+
zip.select(args...) # => [clause_index, [bound values]] or nil
|
|
792
|
+
zip.decision_tree # dump of the compiled tree
|
|
793
|
+
```
|
|
794
|
+
|
|
795
|
+
Options: `exhaustive:` and `overlapping:` accept `:error` (default), `:warn`
|
|
796
|
+
or `:ignore` (`true`/`false` work too), per function or globally through
|
|
797
|
+
`HaskellMatch.exhaustive = :warn`; `deep: true` selects deep mode (see the
|
|
798
|
+
recursion notes); `ractor: true` makes the function Ractor-shareable. A non-exhaustive function compiled with
|
|
799
|
+
`exhaustive: false` raises `HaskellMatch::MatchError` when no clause matches.
|
|
800
|
+
|
|
801
|
+
### `HaskellMatch.case_of` — `case ... of` expressions
|
|
802
|
+
|
|
803
|
+
```ruby
|
|
804
|
+
HaskellMatch.case_of(value) do
|
|
805
|
+
on("Just x", guard: ->(x) { x > 10 }) { |x| "big #{x}" }
|
|
806
|
+
on("Just x") { |x| "just #{x}" }
|
|
807
|
+
on("Nothing") { "nothing" }
|
|
808
|
+
end
|
|
809
|
+
```
|
|
810
|
+
|
|
811
|
+
Several scrutinees may be given: `case_of(a, b) { on("(x:_)", "True") { ... } }`.
|
|
812
|
+
The decision tree for each distinct set of patterns is compiled once and
|
|
813
|
+
cached; the blocks are collected on each evaluation, so `fn` is the fast path
|
|
814
|
+
for hot code.
|
|
815
|
+
|
|
816
|
+
### `HaskellMatch.pattern` — single patterns
|
|
817
|
+
|
|
818
|
+
```ruby
|
|
819
|
+
p = HaskellMatch.pattern("Just (x:rest)") # also HaskellMatch["..."]
|
|
820
|
+
p.match(Just.new([1, 2, 3])) # => { x: 1, rest: [2, 3] }
|
|
821
|
+
p.match(Nothing) # => nil
|
|
822
|
+
p.match!(Nothing) # raises MatchError
|
|
823
|
+
p === Just.new([1]) # => true, so it works in case/when
|
|
824
|
+
p.irrefutable? # => false
|
|
825
|
+
```
|
|
826
|
+
|
|
827
|
+
### Methods: `hdef`
|
|
828
|
+
|
|
829
|
+
```ruby
|
|
830
|
+
class Account
|
|
831
|
+
extend HaskellMatch::DSL # fn, case_of, pattern, data, hdef
|
|
832
|
+
include Maybe
|
|
833
|
+
|
|
834
|
+
hdef :apply do
|
|
835
|
+
on("Nothing") { self }
|
|
836
|
+
on("Just amt", guard: ->(amt) { amt <= limit }) { |amt| Account.new(balance + amt) }
|
|
837
|
+
on("Just amt") { |amt| raise ArgumentError, "over limit: #{amt}" }
|
|
838
|
+
end
|
|
839
|
+
end
|
|
840
|
+
```
|
|
841
|
+
|
|
842
|
+
## Ruby values, Haskell discipline
|
|
843
|
+
|
|
844
|
+
Everything above works on values you declare with `HaskellMatch.data`. This
|
|
845
|
+
section is about the rest of Ruby: Hashes, the `Data` and `Struct` classes
|
|
846
|
+
you already have, and the conveniences Haskell programmers expect.
|
|
847
|
+
|
|
848
|
+
### Hash patterns
|
|
849
|
+
|
|
850
|
+
A Ruby Hash literal is a Hash pattern (quoted: `{name = n, "key" = p}`).
|
|
851
|
+
It matches a Hash that has every listed key (Symbol or String), with each
|
|
852
|
+
value matched by its sub-pattern; other keys are ignored, like fields left
|
|
853
|
+
out of a record pattern. A key whose value is `nil` counts as present.
|
|
854
|
+
|
|
855
|
+
```ruby
|
|
856
|
+
greet = HaskellMatch.fn(:greet) do
|
|
857
|
+
on({ name: n, title: t }) { |n, t| "#{t} #{n}" }
|
|
858
|
+
on({ name: n }) { |n| "hi #{n}" }
|
|
859
|
+
on({}) { "anonymous" } # {} matches every Hash
|
|
860
|
+
end
|
|
861
|
+
greet.(name: "Al", title: "Dr") # => "Dr Al"
|
|
862
|
+
greet.(name: "Al", age: 3) # => "hi Al"
|
|
863
|
+
```
|
|
864
|
+
|
|
865
|
+
Exhaustiveness and redundancy are checked as for everything else: without
|
|
866
|
+
a `{}` or `_` clause the checker reports `p1 where p1 is a Hash without the
|
|
867
|
+
key name`; a clause needing a superset of an earlier clause's keys is
|
|
868
|
+
redundant; a Hash column cannot be mixed with constructors or literals.
|
|
869
|
+
Values nest (`{ user: { id: i } }`, `{ k: Just(x) }`), and a non-Hash
|
|
870
|
+
argument with no wildcard clause is a `TypeMismatchError`.
|
|
871
|
+
|
|
872
|
+
### Ruby classes as constructors
|
|
873
|
+
|
|
874
|
+
A `Data` or `Struct` class visible from the definition needs no
|
|
875
|
+
declaration: naming it in a pattern makes it a type with that single
|
|
876
|
+
constructor, so one clause covers it and its members are its fields (record
|
|
877
|
+
patterns included).
|
|
878
|
+
|
|
879
|
+
```ruby
|
|
880
|
+
Point = Data.define(:x, :y)
|
|
881
|
+
norm = HaskellMatch.fn(:norm) { on(Point(x, y)) { |x, y| Math.hypot(x, y) } }
|
|
882
|
+
norm.(Point.new(3, 4)) # => 5.0
|
|
883
|
+
HaskellMatch.pattern("Point { x = a }").match(Point.new(1, 2)) # => {a: 1}
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
For several classes that together form one closed type (a sealed
|
|
887
|
+
hierarchy), `HaskellMatch.sealed` lists them. `Data` and `Struct` classes
|
|
888
|
+
bring their own field names; for any other class, name the reader methods
|
|
889
|
+
that are its fields.
|
|
890
|
+
|
|
891
|
+
```ruby
|
|
892
|
+
Circle = Data.define(:r)
|
|
893
|
+
Rect = Data.define(:w, :h)
|
|
894
|
+
class Tri
|
|
895
|
+
attr_reader :a, :b, :c
|
|
896
|
+
def initialize(a, b, c) = (@a, @b, @c = a, b, c)
|
|
897
|
+
end
|
|
898
|
+
|
|
899
|
+
Shape = HaskellMatch.sealed(:Shape, Circle, Rect, Tri => %i[a b c])
|
|
900
|
+
|
|
901
|
+
area = HaskellMatch.fn(:area) do
|
|
902
|
+
on(Circle(r)) { |r| Math::PI * r * r }
|
|
903
|
+
on("Rect w h") { |w, h| w * h }
|
|
904
|
+
# on("Tri a b c") missing:
|
|
905
|
+
end
|
|
906
|
+
# HaskellMatch::NonExhaustiveError: Patterns not matched: Tri _ _ _
|
|
907
|
+
```
|
|
908
|
+
|
|
909
|
+
The classes themselves are untouched apart from gaining a few readers
|
|
910
|
+
(`constructor_name`, `field_names`, `data_type`). Instances are identified
|
|
911
|
+
by exact class, so list the leaf classes of a hierarchy. The returned type
|
|
912
|
+
module works like any other (`Shape === value`, `Shape.constructors`) and
|
|
913
|
+
is defined as a constant only when you pass `under:`.
|
|
914
|
+
|
|
915
|
+
### `deriving (Ord, Enum, Bounded)`
|
|
916
|
+
|
|
917
|
+
`Eq` and `Show` always hold: constructor values compare structurally and
|
|
918
|
+
`inspect` prints Haskell. Deriving the other standard classes adds:
|
|
919
|
+
|
|
920
|
+
```ruby
|
|
921
|
+
HaskellMatch.data "Color = Red | Green | Blue deriving (Eq, Ord, Enum, Bounded, Show)"
|
|
922
|
+
include Color
|
|
923
|
+
|
|
924
|
+
Red < Blue # => true (Ord: constructor order, then fields)
|
|
925
|
+
[Blue, Red, Green].sort # => [Red, Green, Blue]
|
|
926
|
+
Red.succ # => Green (Enum; Blue.succ raises, as in GHC)
|
|
927
|
+
(Red..Blue).to_a # => [Red, Green, Blue]
|
|
928
|
+
Color.enum_from(Green) # => [Green, Blue]
|
|
929
|
+
Color.enum_from_then_to(Red, Blue, Blue) # => [Red, Blue]
|
|
930
|
+
Color.min_bound # => Red (Bounded)
|
|
931
|
+
Green.from_enum # => 1
|
|
932
|
+
```
|
|
933
|
+
|
|
934
|
+
`Ord` works for any type (`Pt 1 2 < Pt 1 3`); `Enum` and `Bounded` need an
|
|
935
|
+
enumeration (every constructor nullary), as GHC requires. The Hash form
|
|
936
|
+
takes `deriving: %i[Ord Enum]`. Unsupported class names are an error, and
|
|
937
|
+
nothing is registered when a derivation fails.
|
|
938
|
+
|
|
939
|
+
### Checked field types
|
|
940
|
+
|
|
941
|
+
The types written in a declaration are documentation by default. With
|
|
942
|
+
`check_types: true` (or `HaskellMatch.check_field_types = true` for all
|
|
943
|
+
declarations) the constructor verifies each argument and raises
|
|
944
|
+
`FieldTypeError` otherwise:
|
|
945
|
+
|
|
946
|
+
```ruby
|
|
947
|
+
HaskellMatch.data "Person = Person { name :: String, age :: Int, boss :: Maybe Person }",
|
|
948
|
+
check_types: true
|
|
949
|
+
Person.new("Al", "3", Nothing)
|
|
950
|
+
# HaskellMatch::FieldTypeError: Person: field 'age' expects Int, got "3" (String)
|
|
951
|
+
```
|
|
952
|
+
|
|
953
|
+
`Int`/`Integer` take Integers; `Double`/`Float` any Numeric; `String` a
|
|
954
|
+
String or list of characters; `Char` a one-character String; `Bool`
|
|
955
|
+
true/false; `[a]` any list (Array, String, LazyList, Enumerator);
|
|
956
|
+
`(a, b)` an Array of that size, elementwise; `a -> b` anything callable; a
|
|
957
|
+
declared type (`Maybe Person`, `Shape`) a value of that type; any other
|
|
958
|
+
capitalised name a Ruby class or module of that name when one exists
|
|
959
|
+
(`Time`, `Hash`, `MyApp::Money`); type variables anything.
|
|
960
|
+
|
|
961
|
+
### `where` helpers
|
|
962
|
+
|
|
963
|
+
A local function inside a definition, like a Haskell `where` binding. It is
|
|
964
|
+
a full `Function` (checked, with `tail` and `defer`), compiled with the
|
|
965
|
+
enclosing function's options unless you override them, and reachable by
|
|
966
|
+
name from every clause body and from the other helpers.
|
|
967
|
+
|
|
968
|
+
```ruby
|
|
969
|
+
sum_to = HaskellMatch.fn(:sum_to) do
|
|
970
|
+
on(n) { |n| go.(n, 0) }
|
|
971
|
+
where :go do
|
|
972
|
+
on(0, acc) { |acc| acc }
|
|
973
|
+
on(k, acc) { |k, acc| go.tail(k - 1, acc + k) }
|
|
974
|
+
end
|
|
975
|
+
end
|
|
976
|
+
sum_to.(1_000_000) # => 500000500000
|
|
977
|
+
sum_to.helpers # => {"go" => #<HaskellMatch::Function go/2>}
|
|
978
|
+
```
|
|
979
|
+
|
|
980
|
+
Helpers do not see the enclosing clause's variables (pass them as
|
|
981
|
+
arguments, as the example does), and in `ractor: true` mode they cannot
|
|
982
|
+
call back into the enclosing function by name.
|
|
983
|
+
|
|
984
|
+
### Composition
|
|
985
|
+
|
|
986
|
+
`Function#>>` and `#<<` compose like `Proc`'s: `(f >> g).(x)` is
|
|
987
|
+
`g.(f.(x))` (Haskell's `g . f`) and `(f << g).(x)` is `f.(g.(x))`.
|
|
988
|
+
|
|
989
|
+
### Errors point at the problem
|
|
990
|
+
|
|
991
|
+
Pattern syntax errors show the pattern with a caret under the offending
|
|
992
|
+
column, and Haskell syntax errors show the offending line of the Ruby or
|
|
993
|
+
`.hs` file:
|
|
994
|
+
|
|
995
|
+
```
|
|
996
|
+
HaskellMatch::PatternSyntaxError: clause 1: expected ')' but reached end of pattern
|
|
997
|
+
Just (x
|
|
998
|
+
^
|
|
999
|
+
```
|
|
1000
|
+
|
|
1001
|
+
## Inline Haskell and `.hs` files
|
|
1002
|
+
|
|
1003
|
+
`HaskellMatch.haskell(source)` compiles Haskell source into a module and
|
|
1004
|
+
returns it; `into: SomeModule` compiles into an existing one, and inside a
|
|
1005
|
+
module body `extend HaskellMatch::Haskell` gives a `haskell` method that
|
|
1006
|
+
does the same. `HaskellMatch.load(path)` compiles a file into a fresh
|
|
1007
|
+
module; `HaskellMatch.require(name)` finds `name.hs` on `$LOAD_PATH` (or
|
|
1008
|
+
takes a path), compiles it once, and defines a constant for it named after
|
|
1009
|
+
the file's `module` header (`module Data.Tree where` becomes `Data::Tree`)
|
|
1010
|
+
or, without one, the camel-cased file name.
|
|
1011
|
+
|
|
1012
|
+
```ruby
|
|
1013
|
+
Geometry = HaskellMatch.load("examples/geometry.hs")
|
|
1014
|
+
HaskellMatch.require "lists" # lists.hs somewhere on $LOAD_PATH -> Lists
|
|
1015
|
+
HaskellMatch.require "lib/hs/tree", under: MyApp # -> MyApp::Tree (or its module header)
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
Write the source in a heredoc; use a quoted heredoc (`<<~'HS'`) when the
|
|
1019
|
+
Haskell contains backslashes (lambdas) so Ruby leaves them alone.
|
|
1020
|
+
|
|
1021
|
+
### The supported language
|
|
1022
|
+
|
|
1023
|
+
The front end is a Haskell 2010 parser with the layout rule, so ordinary
|
|
1024
|
+
Haskell formatting works. Supported:
|
|
1025
|
+
|
|
1026
|
+
* `data` and `newtype` declarations, positional, infix (`data V = Double
|
|
1027
|
+
:| Double`) or with record fields (whose names are selector functions,
|
|
1028
|
+
with `P { f = e }` construction and `p { f = e }` update), with
|
|
1029
|
+
`deriving` (`Eq` and `Show` always hold; `Ord`, `Enum` and `Bounded` work
|
|
1030
|
+
as described under [deriving](#deriving-ord-enum-bounded), so `succ c`,
|
|
1031
|
+
`[Red ..]`, `minBound`-style code runs); `type` synonyms and signatures
|
|
1032
|
+
(accepted and ignored: Ruby is the type system here).
|
|
1033
|
+
* `module M (exports) where` headers and `import` declarations: see
|
|
1034
|
+
[Modules](#modules-imports-and-exports) below.
|
|
1035
|
+
* User-defined operators with `infixl`/`infixr`/`infix` fixity
|
|
1036
|
+
declarations, in prefix form (`(<+>) a b = ...`), infix form
|
|
1037
|
+
(`a <+> b = ...`) or with backticks (``x `cons` xs = ...``), at top level
|
|
1038
|
+
or in `where`/`let`; operators and backticked functions in sections, as
|
|
1039
|
+
values (`(<+>)`) and from Ruby (`Mod.send(:"<+>", a, b)`,
|
|
1040
|
+
`Mod.haskell_function("<+>")`).
|
|
1041
|
+
* Function equations with any patterns this library supports (constructors,
|
|
1042
|
+
infix constructors, literals, negative literals, characters, strings,
|
|
1043
|
+
lists, tuples, `_`, as-patterns, lazy and bang patterns, records), guards
|
|
1044
|
+
with `otherwise`, pattern guards (`| Just v <- lookup k m, v > 0 = ...`)
|
|
1045
|
+
and `let` guards, `where` bindings (functions, values and pattern
|
|
1046
|
+
bindings, nested arbitrarily), `let ... in`, `case ... of` with guards,
|
|
1047
|
+
`if/then/else`.
|
|
1048
|
+
* Expressions: application and partial application, operators with the
|
|
1049
|
+
Prelude's fixities, backtick operators, sections (`(*2)`, `` (`div` 2) ``,
|
|
1050
|
+
`subtract 1`), operator values (`(+)`), `$`, `.`, lambdas (including
|
|
1051
|
+
pattern lambdas), tuples, list literals, ranges (`[1..n]`, `[1,3..]`,
|
|
1052
|
+
`[0..]`), list comprehensions with generators, guards and `let`.
|
|
1053
|
+
* The small syntax extensions GHC users reach for without thinking:
|
|
1054
|
+
`MultiWayIf` (`if | c1 -> e1 | c2 -> e2`), `LambdaCase` (`\case`),
|
|
1055
|
+
`TupleSections` (`(,x)`, `(1,,3)`), `NamedFieldPuns` in construction.
|
|
1056
|
+
* Literals: decimal, hexadecimal, octal and binary integers, exponent
|
|
1057
|
+
floats, `_` digit separators, and every Haskell character escape
|
|
1058
|
+
(`\n`, `\x41`, `\o101`, `\65`, `\SOH`, `\^A`, `\&`, string gaps).
|
|
1059
|
+
* Names: `camelCase` functions get a `snake_case` alias; a trailing prime
|
|
1060
|
+
becomes `_prime` (`foldl'` is callable as `foldl_prime`).
|
|
1061
|
+
* Top-level values (`primes = ...`) become memoised methods; a value of
|
|
1062
|
+
function type can still be called with arguments from Ruby
|
|
1063
|
+
(`Mod.from_list([1, 2])` when `fromList = foldr insert Leaf`).
|
|
1064
|
+
|
|
1065
|
+
Out of scope, by design: type classes (`class`/`instance`), `do` notation
|
|
1066
|
+
and monads, and anything needing type inference. They are rejected with a
|
|
1067
|
+
clear message.
|
|
1068
|
+
|
|
1069
|
+
### Modules, imports and exports
|
|
1070
|
+
|
|
1071
|
+
A compiled module can import another. Functions and values of the imported
|
|
1072
|
+
module become callable (the compiler uses them with their real arity, tail
|
|
1073
|
+
calls included), its data types join the importing module's type scope so
|
|
1074
|
+
their constructors work in patterns with full exhaustiveness checking, and
|
|
1075
|
+
the constructors become constants of the importing module.
|
|
1076
|
+
|
|
1077
|
+
```haskell
|
|
1078
|
+
module Physics where
|
|
1079
|
+
import Vectors -- Vectors.hs on $LOAD_PATH, or the constant Vectors
|
|
1080
|
+
import Vectors (Vec(..), norm) -- only these
|
|
1081
|
+
import Vectors hiding (hidden) -- all but these
|
|
1082
|
+
import qualified Data.Map as M -- qualification is accepted and ignored
|
|
1083
|
+
import Math (hypot) -- a plain Ruby module: its singleton methods
|
|
1084
|
+
```
|
|
1085
|
+
|
|
1086
|
+
A module name resolves to an existing constant (`Data.Tree` is
|
|
1087
|
+
`Data::Tree`) or to a file on `$LOAD_PATH` (`Data/Tree.hs`, `Data.Tree.hs`
|
|
1088
|
+
or `data/tree.hs`), loaded once with `HaskellMatch.require`. The standard
|
|
1089
|
+
library modules (`Data.List`, `Data.Char`, `Data.Maybe`, `Control.Monad`,
|
|
1090
|
+
...) are the Prelude: importing one only checks that the named items exist.
|
|
1091
|
+
|
|
1092
|
+
An export list (`module Vectors (Vec(..), (<+>), norm) where`) limits what
|
|
1093
|
+
importers see; `Mod.haskell_exports` returns it. Everything stays callable
|
|
1094
|
+
from Ruby regardless. Compiled code refers to names unqualified, so two
|
|
1095
|
+
qualifications of the same name are not distinguished.
|
|
1096
|
+
|
|
1097
|
+
### Semantics worth knowing
|
|
1098
|
+
|
|
1099
|
+
* **Strictness.** Compiled code is strict (it is Ruby), with one deliberate
|
|
1100
|
+
exception: the tail of `x : e` is deferred when `e` is a computation
|
|
1101
|
+
rather than a variable or literal. That is exactly what makes
|
|
1102
|
+
`p : sieve xs` and `fibs = 0 : 1 : zipWith (+) fibs (tail fibs)` work. A
|
|
1103
|
+
list built that way is a `LazyList` (`to_a` materialises it, `==`
|
|
1104
|
+
compares elements); a list built from a variable tail (`toUpper c : cs`)
|
|
1105
|
+
keeps its input's type, so Strings stay Strings and Arrays stay Arrays.
|
|
1106
|
+
* **Lists are Arrays, Strings or LazyLists**, exactly as for patterns.
|
|
1107
|
+
Prelude functions accept all three and stay lazy when their input is.
|
|
1108
|
+
Ranges are Arrays when bounded and lazy when not.
|
|
1109
|
+
* **Recursion** compiles to the same machinery as `HaskellMatch.fn`: calls
|
|
1110
|
+
in tail position use `tail` (constant space), everything else uses the
|
|
1111
|
+
segmented stack, and `HaskellMatch.max_depth` applies.
|
|
1112
|
+
* **Exhaustiveness and redundancy** are enforced for every function,
|
|
1113
|
+
including `where`/`let` helpers, `case` expressions and pattern lambdas,
|
|
1114
|
+
with the policy you pass as `exhaustive:` (default
|
|
1115
|
+
`HaskellMatch.exhaustive`). Errors name the Haskell function and the line
|
|
1116
|
+
of the Ruby file (or `.hs` file) it came from.
|
|
1117
|
+
* **Type scopes.** Each compiled module gets its own type scope: a snapshot
|
|
1118
|
+
of the global `HaskellMatch.data` registry when the module is first
|
|
1119
|
+
compiled plus its own `data` declarations, so two modules can both declare
|
|
1120
|
+
a `Shape`. Ruby code matches on a module's types through the module's
|
|
1121
|
+
`fn`, `case_of`, `pattern` and `data` methods, which take the same
|
|
1122
|
+
options as `HaskellMatch`'s. Types you want to share between Ruby and
|
|
1123
|
+
Haskell are simplest declared globally with `HaskellMatch.data` before
|
|
1124
|
+
the module is compiled.
|
|
1125
|
+
* **Interop.** A name the Haskell does not define (and the Prelude does not
|
|
1126
|
+
provide) is called as a method of the host module, so a module can mix
|
|
1127
|
+
`def self.helper` with Haskell that calls `helper`. Ruby lambdas, Procs
|
|
1128
|
+
and Methods are Haskell functions (`Mod.my_map(->(x) { x * 2 }, [1, 2])`),
|
|
1129
|
+
a function returned from Haskell is a Ruby `Proc`, and calling an exported
|
|
1130
|
+
function applies like Haskell: `Mod.add3(1, 2, 3)`, `Mod.add3(1).(2).(3)`
|
|
1131
|
+
(a partial application) and `Mod.adder(5, 10)` (extra arguments go to the
|
|
1132
|
+
returned function) all work. The compiled `Function` objects are available
|
|
1133
|
+
as `Mod.haskell_functions` / `Mod.haskell_function(:name)` for `tail`,
|
|
1134
|
+
`to_proc`, `curried`, `===` and `decision_tree`.
|
|
1135
|
+
* **The Prelude** lives in `HaskellMatch::Prelude` and is callable from
|
|
1136
|
+
Ruby too (`HaskellMatch::Prelude.take(3, xs)`). It provides the standard
|
|
1137
|
+
types `Maybe`, `Either` and `Ordering` (`HaskellMatch::Prelude::Maybe::Just`)
|
|
1138
|
+
and the usual functions: `map`, `filter`, `foldr`, `foldl`, `zip`,
|
|
1139
|
+
`zipWith`, `take`, `drop`, `takeWhile`, `iterate`, `repeat`, `cycle`,
|
|
1140
|
+
`sum`, `product`, `length`, `reverse`, `concat`, `concatMap`, `elem`,
|
|
1141
|
+
`lookup`, `words`, `lines`, `show`, `fromIntegral`, `div`, `mod`,
|
|
1142
|
+
`compare`, `maybe`, `fromMaybe`, `either`, `error`, `undefined`, the
|
|
1143
|
+
`Data.Char` basics and more. `HaskellMatch::Prelude::ARITY.keys` lists
|
|
1144
|
+
them all.
|
|
1145
|
+
* **Debugging.** `HaskellMatch::Haskell.generated_ruby(mod)` returns the
|
|
1146
|
+
Ruby a module was compiled to.
|
|
1147
|
+
|
|
1148
|
+
## Deep and infinite recursion: expert notes
|
|
1149
|
+
|
|
1150
|
+
This section is for readers who intend to recurse hundreds of thousands or
|
|
1151
|
+
millions of levels deep, or to iterate forever, and want to know exactly what
|
|
1152
|
+
happens underneath. The short version: haskell_match gives you GHC's
|
|
1153
|
+
behaviour (recursion limited by memory, loops in constant space) on top of a
|
|
1154
|
+
VM whose stack is fixed, and the price of that is paid in memory and in the
|
|
1155
|
+
GC. Every number below was measured on Ruby 3.3.6, x86-64 Linux, with the
|
|
1156
|
+
default settings; reproduce them with the snippets at the end.
|
|
1157
|
+
|
|
1158
|
+
### What a call costs on the stacks
|
|
1159
|
+
|
|
1160
|
+
A call `f.(x)` enters the native `call` method (one Ruby control frame for the
|
|
1161
|
+
C function), matches `x` against the decision tree with no allocation, and
|
|
1162
|
+
invokes the selected clause body with `rb_proc_call_with_block`. The body is
|
|
1163
|
+
a Ruby block, so it gets a second control frame. If the body itself calls
|
|
1164
|
+
`f.(y)`, the whole sequence nests. Each level therefore consumes:
|
|
1165
|
+
|
|
1166
|
+
* **Ruby VM stack**: two control frames (the C-function frame and the block
|
|
1167
|
+
frame) plus the block's locals and operand stack, roughly 150–300 bytes.
|
|
1168
|
+
* **Machine (C) stack**: the native method's own frame, the VM re-entry
|
|
1169
|
+
(`vm_exec`) that runs the block, and the Rust matcher's scratch space,
|
|
1170
|
+
roughly a kilobyte.
|
|
1171
|
+
|
|
1172
|
+
Ruby sizes both stacks when a thread or fiber is created and never grows
|
|
1173
|
+
them: 1 MB VM / 1 MB machine for a thread, 128 KB VM / 512 KB machine for a
|
|
1174
|
+
fiber (`RubyVM::DEFAULT_PARAMS`). A plain Ruby lambda that recurses with one
|
|
1175
|
+
frame per level dies at about 11,000 levels on the main thread; a clause body
|
|
1176
|
+
with two frames per level would die at about 7,000; inside a fiber, at about
|
|
1177
|
+
480. Nothing at run time can enlarge an existing stack, which is why the
|
|
1178
|
+
mechanisms below exist.
|
|
1179
|
+
|
|
1180
|
+
### Mechanism 1: stack segments (what plain recursion uses)
|
|
1181
|
+
|
|
1182
|
+
The native `call` keeps a per-thread nesting counter. When a body is about to
|
|
1183
|
+
run at a depth that is a multiple of `HaskellMatch.stack_segment` (default
|
|
1184
|
+
100), the body is run inside a brand-new Fiber instead of on the current
|
|
1185
|
+
stack. That Fiber has fresh 128 KB / 512 KB stacks; the recursion continues
|
|
1186
|
+
in it until another 100 levels, when the next segment is started. Segments
|
|
1187
|
+
form a linked chain of suspended fibers, each waiting for the inner one's
|
|
1188
|
+
result, which is exactly the shape of a growable stack.
|
|
1189
|
+
|
|
1190
|
+
Facts about segments:
|
|
1191
|
+
|
|
1192
|
+
* **Default 100 is deliberate.** A single fiber holds about 480 plain levels;
|
|
1193
|
+
bodies that call a few helper methods per level use more VM stack, so 100
|
|
1194
|
+
leaves a safety factor of four to five. Raising `stack_segment` lowers the
|
|
1195
|
+
memory per level (fewer, fuller fibers) but narrows that margin: 400 and
|
|
1196
|
+
above overflow a fiber with even the simplest body. Lowering it is always
|
|
1197
|
+
safe and only costs memory and fiber creations.
|
|
1198
|
+
* **Memory per level: about 1.6 KB.** Each segment commits its 128 KB fiber
|
|
1199
|
+
VM stack in full (the VM touches both ends of it), so the per-level cost is
|
|
1200
|
+
dominated by `128 KB / stack_segment`, not by the frames themselves. 200k
|
|
1201
|
+
levels peak at about 300 MB; 1M levels at about 1.6 GB. GHC, by comparison,
|
|
1202
|
+
spends about 24 bytes per level of `1 + length xs`. The shape is the same
|
|
1203
|
+
as Haskell's (non-tail recursion is linear in depth); the constant is about
|
|
1204
|
+
sixty times worse.
|
|
1205
|
+
* **Memory is reclaimed, lazily.** When the outermost call returns, every
|
|
1206
|
+
segment fiber has finished and is unreferenced; the Fiber objects are
|
|
1207
|
+
collected at the next GC and their stacks go back to Ruby's fiber pool,
|
|
1208
|
+
which marks the pages `MADV_FREE`. The kernel reclaims those pages under
|
|
1209
|
+
memory pressure, so RSS stays high after a deep call even though the
|
|
1210
|
+
memory is available (`LazyFree` in `/proc/self/smaps_rollup` shows it:
|
|
1211
|
+
288 MB of a 325 MB RSS after a 200k-deep call). Repeated deep calls reuse
|
|
1212
|
+
the pooled stacks and do not grow RSS further. This is not a leak; it is
|
|
1213
|
+
the pool keeping what it once needed.
|
|
1214
|
+
* **First call is slow, later calls are fast.** Committing fresh fiber stacks
|
|
1215
|
+
page-faults every page: the first 400k-deep call took 3.2 s, the second
|
|
1216
|
+
0.6 s, the third 0.35 s (0.9 µs per level); at 1M depth, 8.9 s then 2.8 s.
|
|
1217
|
+
Budget for the cold run if a deep recursion happens once.
|
|
1218
|
+
* **GC cost grows with live depth.** Fiber objects are not write-barrier
|
|
1219
|
+
protected (their stacks change on every instruction, so no barrier could
|
|
1220
|
+
track them), and CRuby therefore re-marks the VM and machine stacks of
|
|
1221
|
+
every suspended segment on every collection, minor ones included. A GC
|
|
1222
|
+
during a pending recursion costs O(live stack bytes): at 200k levels a
|
|
1223
|
+
minor GC takes about 165 ms instead of 2 ms. Nothing outside CRuby can
|
|
1224
|
+
change *that* a suspended fiber is rescanned; what can be changed is how
|
|
1225
|
+
much there is to scan, and that is what deep mode below does (28 ms for the
|
|
1226
|
+
same collection). `defer` avoids the fiber stacks altogether, and
|
|
1227
|
+
`GC.disable` around a known deep computation, or a larger
|
|
1228
|
+
`RUBY_GC_HEAP_INIT_SLOTS`, reduces the number of collections.
|
|
1229
|
+
* **Semantics across a segment boundary.** Exceptions propagate normally
|
|
1230
|
+
(`rb_fiber_resume` re-raises them in the parent), but the backtrace only
|
|
1231
|
+
covers the innermost segment. Non-local exits do not cross: `throw` to a
|
|
1232
|
+
`catch` outside the segment raises `UncaughtThrowError`, and `break` or
|
|
1233
|
+
`return` out of a body proc raises `LocalJumpError` at the boundary.
|
|
1234
|
+
Within 100 levels of the `catch`, everything behaves as in one stack.
|
|
1235
|
+
`Fiber.yield` inside a body yields the segment fiber, not yours.
|
|
1236
|
+
* **Threads and Ractors.** The depth counter is per OS thread, so each Ruby
|
|
1237
|
+
Thread and each Ractor recurses independently. If your own Fiber runs a
|
|
1238
|
+
deep recursion, the counter is shared with the thread that created it; the
|
|
1239
|
+
only effect is that segments may start a little earlier than needed.
|
|
1240
|
+
|
|
1241
|
+
### Deep mode: the same recursion with no native frames on the stack
|
|
1242
|
+
|
|
1243
|
+
Where do the 1.6 KB per level come from, given that an idle fiber commits only
|
|
1244
|
+
about 13 KB? From the C frames. In the default (native) mode the body is
|
|
1245
|
+
invoked by the Rust matcher: the machine stack holds, per level, the C
|
|
1246
|
+
function frame of `call`, the VM re-entry that runs the block, and the
|
|
1247
|
+
matcher's scratch space, about 1.3 KB, all of which the GC also scans
|
|
1248
|
+
conservatively because it cannot know which words are references. The Ruby
|
|
1249
|
+
frames themselves are small.
|
|
1250
|
+
|
|
1251
|
+
`deep: true` moves the body invocation into Ruby:
|
|
1252
|
+
|
|
1253
|
+
```ruby
|
|
1254
|
+
count = HaskellMatch.fn(:count, deep: true) do
|
|
1255
|
+
on([]) { 0 }
|
|
1256
|
+
on([_, *xs]) { |xs| 1 + count.(xs) }
|
|
1257
|
+
end
|
|
1258
|
+
```
|
|
1259
|
+
|
|
1260
|
+
`call` becomes a generated Ruby method of exact arity that asks the native
|
|
1261
|
+
`prepare` for the selected body and its bound values, then calls the body
|
|
1262
|
+
itself. A Ruby-to-Ruby call pushes no C frame, so while the recursion is
|
|
1263
|
+
pending the machine stack holds nothing of ours; segments, `tail`, `defer`
|
|
1264
|
+
and `max_depth` work exactly as before (the trampoline is reimplemented in
|
|
1265
|
+
Ruby, with the same chunked continuation stack). Measured against native
|
|
1266
|
+
mode at 200k levels:
|
|
1267
|
+
|
|
1268
|
+
| mode | shallow call | memory per level | minor GC at 200k depth | 200k levels, warm |
|
|
1269
|
+
|--------|-------------:|-----------------:|-----------------------:|------------------:|
|
|
1270
|
+
| native | ~230 ns | ~1.6 KB | ~165 ms | 0.94 s |
|
|
1271
|
+
| deep | ~460 ns | ~105 B | ~28 ms | 0.29 s |
|
|
1272
|
+
|
|
1273
|
+
A million plain levels in deep mode take about 3 s and 300 MB. The trade is
|
|
1274
|
+
clear-cut: deep mode costs an extra Ruby frame on *every* call, which doubles
|
|
1275
|
+
the time of a shallow call, and in exchange brings a level within a factor of
|
|
1276
|
+
four of GHC's 24 bytes and cuts GC marking six-fold. Native mode remains the
|
|
1277
|
+
default because most functions never recurse; set `deep: true` on the ones
|
|
1278
|
+
that do, or `HaskellMatch.deep_by_default = true` for a codebase that is
|
|
1279
|
+
recursive throughout. A deep-mode function may call native-mode functions
|
|
1280
|
+
and vice versa, including through `tail`.
|
|
1281
|
+
|
|
1282
|
+
### Mechanism 2: tail calls (constant space)
|
|
1283
|
+
|
|
1284
|
+
```ruby
|
|
1285
|
+
go = HaskellMatch.fn(:go) do
|
|
1286
|
+
on(acc, []) { |acc| acc }
|
|
1287
|
+
on(acc, [x, *xs]) { |acc, x, xs| go.tail(acc + x, xs) }
|
|
1288
|
+
end
|
|
1289
|
+
```
|
|
1290
|
+
|
|
1291
|
+
`go.tail(args...)` returns a `HaskellMatch::TailCall` marker. The native
|
|
1292
|
+
`call` sees it, replaces the current arguments with the marker's and matches
|
|
1293
|
+
again on the same frame; nothing is pushed on any stack and nothing counts
|
|
1294
|
+
towards `max_depth`. The target may be a different function, so mutual
|
|
1295
|
+
recursion (`even`/`odd`) loops in constant space too. Cost: one small `Data`
|
|
1296
|
+
allocation per iteration, about 1.1 µs per level including the list slice
|
|
1297
|
+
(a shared, copy-on-write subarray). This is the right tool for anything that
|
|
1298
|
+
is a loop in Haskell: accumulators, folds, state machines, servers. A marker
|
|
1299
|
+
returned anywhere but as the body's final value is just a value (`go.tail(1)`
|
|
1300
|
+
outside a call is an ordinary object).
|
|
1301
|
+
|
|
1302
|
+
### Mechanism 3: deferred calls (cheap depth)
|
|
1303
|
+
|
|
1304
|
+
```ruby
|
|
1305
|
+
length = HaskellMatch.fn(:length) do
|
|
1306
|
+
on([]) { 0 }
|
|
1307
|
+
on([_, *xs]) { |xs| length.defer(xs) { |n| 1 + n } } # 1 + length xs
|
|
1308
|
+
end
|
|
1309
|
+
```
|
|
1310
|
+
|
|
1311
|
+
`f.defer(args...) { |result| ... }` is a tail call that carries a
|
|
1312
|
+
continuation. The native `call` pushes the block on a stack it owns and
|
|
1313
|
+
continues with the call; when a body finally returns an ordinary value, the
|
|
1314
|
+
pending blocks are applied to it last-in first-out, each possibly returning
|
|
1315
|
+
another marker. The recursion's stack lives on the heap, so it is bounded by
|
|
1316
|
+
memory alone and `max_depth` never triggers.
|
|
1317
|
+
|
|
1318
|
+
That stack is deliberately a chain of 256-element Ruby arrays rather than one
|
|
1319
|
+
growing array. Ruby's GC is generational: an old array that is written to is
|
|
1320
|
+
put on the remembered set and rescanned in full by every minor collection, so
|
|
1321
|
+
a single million-element stack would have made each GC O(depth). A full chunk
|
|
1322
|
+
is never written again, gets promoted, and is skipped by minor GCs; only the
|
|
1323
|
+
current chunk is rescanned. The difference is large: 2M levels took 31 s with
|
|
1324
|
+
one array and 8.9 s with chunks (1M: 5.4 s vs 3.8 s), and peak memory fell
|
|
1325
|
+
from 770 MB to 470 MB because the old array's doubling growth is gone.
|
|
1326
|
+
|
|
1327
|
+
Per level `defer` costs one Proc plus its environment (about 200 bytes) and
|
|
1328
|
+
about 4 µs, mostly the allocation and the minor GCs it triggers (878 minor
|
|
1329
|
+
GCs over a 2M-deep run, each cheap). Prefer `defer` over plain recursion when
|
|
1330
|
+
depth is known to be large and memory matters; prefer plain recursion when it
|
|
1331
|
+
is not, since `defer` requires writing the continuation by hand and runs the
|
|
1332
|
+
continuation outside the body's frame (`self` and closure variables are those
|
|
1333
|
+
of the block, as usual).
|
|
1334
|
+
|
|
1335
|
+
### The depth guard
|
|
1336
|
+
|
|
1337
|
+
`HaskellMatch.max_depth` (default 250,000) bounds the nesting counter from
|
|
1338
|
+
mechanism 1. Past it, the next nested call raises
|
|
1339
|
+
`HaskellMatch::StackOverflowError` instead of allocating another segment. At
|
|
1340
|
+
1.6 KB per level the default corresponds to about 400 MB, the point of the
|
|
1341
|
+
limit being that a runaway recursion fails loudly rather than exhausting the
|
|
1342
|
+
machine, which is what GHC's stack limit (80% of RAM by default) is for too.
|
|
1343
|
+
Set it higher or to 0 (unlimited) for a computation that legitimately needs
|
|
1344
|
+
more; `tail` and `defer` never count towards it. The counter is restored
|
|
1345
|
+
exactly even when a body leaves by exception, so a caught error deep in a
|
|
1346
|
+
recursion does not shift later limits.
|
|
1347
|
+
|
|
1348
|
+
### Choosing
|
|
1349
|
+
|
|
1350
|
+
| Pattern of recursion | Use | Space | Time per level (warm) |
|
|
1351
|
+
|-------------------------------------------|--------------------------|-------------|-----------------------|
|
|
1352
|
+
| Loop with accumulator, fold, state machine| `f.tail(...)` | O(1) | ~1.1 µs |
|
|
1353
|
+
| Deep non-tail recursion, depth known large| `f.defer(...) { }` | ~200 B/level| ~4 µs |
|
|
1354
|
+
| Ordinary recursion, depth moderate | plain `f.(...)` | ~1.6 KB/level (≤ `max_depth`) | ~0.9 µs (first call slower) |
|
|
1355
|
+
| Ordinary recursion, depth large | `deep: true` + plain `f.(...)` | ~105 B/level (≤ `max_depth`) | ~1.5 µs, half the GC time |
|
|
1356
|
+
| Infinite data | `HaskellMatch.lazy` + patterns | per element forced | per element |
|
|
1357
|
+
|
|
1358
|
+
Infinite recursion in the Haskell sense, a producer that never returns, is
|
|
1359
|
+
expressed as a lazy list consumed by `tail`-recursive or bounded consumers:
|
|
1360
|
+
`take.(n, HaskellMatch::LazyList.iterate(1) { |x| x * 2 })` forces exactly
|
|
1361
|
+
`n` cells and no more, and each forced cell is memoised, so sharing works as
|
|
1362
|
+
in Haskell (two consumers of the same list see the same elements, computed
|
|
1363
|
+
once). An Enumerator passed directly is re-wrapped on each call, iterating
|
|
1364
|
+
from its start, which keeps calls referentially transparent at the cost of
|
|
1365
|
+
recomputing from scratch per call; keep a `LazyList` in a variable when the
|
|
1366
|
+
elements are expensive.
|
|
1367
|
+
|
|
1368
|
+
### Reproducing the measurements
|
|
1369
|
+
|
|
1370
|
+
```ruby
|
|
1371
|
+
require "haskell_match"
|
|
1372
|
+
HaskellMatch.max_depth = 0
|
|
1373
|
+
count = HaskellMatch.fn(:count) { on("[]") { 0 }; on("(_:xs)") { |xs| 1 + count.(xs) } }
|
|
1374
|
+
dlen = HaskellMatch.fn(:dlen) { on("[]") { 0 }; on("(_:xs)") { |xs| dlen.defer(xs) { |n| 1 + n } } }
|
|
1375
|
+
go = HaskellMatch.fn(:go) { on("acc", "[]") { |acc| acc }; on("acc", "(x:xs)") { |acc, x, xs| go.tail(acc + x, xs) } }
|
|
1376
|
+
|
|
1377
|
+
rss = -> { File.read("/proc/self/status")[/VmRSS:\s+(\d+)/, 1].to_i / 1024 }
|
|
1378
|
+
list = (1..200_000).to_a
|
|
1379
|
+
before = rss.(); count.(list); puts "segments: +#{rss.() - before} MB" # ~300 MB, ~1.6 KB/level
|
|
1380
|
+
before = rss.(); dlen.(list); puts "defer: +#{rss.() - before} MB" # ~45 MB
|
|
1381
|
+
before = rss.(); go.(0, list); puts "tail: +#{rss.() - before} MB" # ~0 MB
|
|
1382
|
+
GC.start; puts File.read("/proc/self/smaps_rollup")[/LazyFree:.*/] # reclaimable pages
|
|
1383
|
+
```
|
|
1384
|
+
|
|
1385
|
+
## Errors
|
|
1386
|
+
|
|
1387
|
+
All errors inherit from `HaskellMatch::Error`.
|
|
1388
|
+
|
|
1389
|
+
Definition time (`HaskellMatch::CompileError`): `PatternSyntaxError`,
|
|
1390
|
+
`UnknownConstructorError`, `ArityError` (constructor applied to the wrong
|
|
1391
|
+
number of patterns), `PatternTypeError` (one position matched against two
|
|
1392
|
+
types), `DuplicateVariableError`, `FieldError`, `DataDeclarationError`,
|
|
1393
|
+
`ClauseArityError`, `NonExhaustiveError` (`#missing` lists the witnesses),
|
|
1394
|
+
`RedundantClauseError` (`#clauses` lists the indices), `DefinitionError`.
|
|
1395
|
+
|
|
1396
|
+
Match time (`HaskellMatch::MatchError`): `MatchError` itself for a partial
|
|
1397
|
+
function with no matching clause, `TypeMismatchError` when a value is of a
|
|
1398
|
+
type no pattern of the function can accept (`expected a value of type Maybe
|
|
1399
|
+
but got 5 (Integer)`), `IrrefutablePatternError` when a `~` pattern fails to
|
|
1400
|
+
destructure. `HaskellMatch::StackOverflowError` is raised past
|
|
1401
|
+
`HaskellMatch.max_depth`. Wrong argument counts raise `ArgumentError`.
|
|
1402
|
+
|
|
1403
|
+
Exceptions raised in bodies and guards propagate unchanged, with their
|
|
1404
|
+
backtraces; `throw`, `return` and `next` behave as in any block.
|
|
1405
|
+
|
|
1406
|
+
## Concurrency
|
|
1407
|
+
|
|
1408
|
+
* **Threads**: safe. Native code runs under the GVL, the type registry is
|
|
1409
|
+
behind a mutex that is never held while calling back into Ruby, compiled
|
|
1410
|
+
functions are immutable, and per-call state lives on the stack.
|
|
1411
|
+
* **Fibers**: safe; no per-thread or per-fiber state is kept natively.
|
|
1412
|
+
* **Ractors**: the extension is declared Ractor-safe and data values are
|
|
1413
|
+
shareable. A function is shareable when defined with `ractor: true`, which
|
|
1414
|
+
makes its clause procs shareable (their `self` becomes an inert frozen
|
|
1415
|
+
object and any local variables they capture must already be shareable) and
|
|
1416
|
+
passes the function through `Ractor.make_shareable`:
|
|
1417
|
+
|
|
1418
|
+
```ruby
|
|
1419
|
+
f = HaskellMatch.fn(:f, ractor: true) { on("Just x") { |x| x }; on("Nothing") { 0 } }
|
|
1420
|
+
Ractor.new(f) { |g| g.(Just.new(1)) }.take # => 1
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
Inside a shareable function, recursion goes through the function's name or
|
|
1424
|
+
`recur` (the body's `self` is a module that knows the finished function).
|
|
1425
|
+
Do not also keep the function in a local variable of the same name: the
|
|
1426
|
+
variable would shadow the name, and `make_shareable` fixes a captured
|
|
1427
|
+
local's value (still `nil` at that point).
|
|
1428
|
+
|
|
1429
|
+
## Performance
|
|
1430
|
+
|
|
1431
|
+
`rake bench` compares against hand-written Ruby (Ruby 3.3, x86-64, one run;
|
|
1432
|
+
numbers are ns per call):
|
|
1433
|
+
|
|
1434
|
+
| benchmark | haskell_match | Ruby `case/in` | hand-written Ruby |
|
|
1435
|
+
|---------------------------------------------|--------------:|---------------:|------------------:|
|
|
1436
|
+
| `from_maybe` (2 args, constructor switch) | 201 | 238 | 97 (`case/when`) |
|
|
1437
|
+
| `area` (3 constructors, nested arithmetic) | 270 | 356 | |
|
|
1438
|
+
| `length` of a 20-element list (recursive) | 7850 | 4686 | 2092 (slices) |
|
|
1439
|
+
| `fib(20)` via literals `0`, `1`, `n` | 3 866 000 | | 1 216 000 (`if`) |
|
|
1440
|
+
| `pattern.match` (bindings hash) | 548 | | |
|
|
1441
|
+
| `case_of` (blocks collected per call) | 14 600 | | |
|
|
1442
|
+
|
|
1443
|
+
The matcher itself is cheap; what remains per call is one native method
|
|
1444
|
+
dispatch plus one Ruby block invocation for the body, which inline Ruby code
|
|
1445
|
+
does not pay. Recursive list functions additionally allocate one shared-slice
|
|
1446
|
+
Array per `(x:xs)` tail that is bound.
|
|
1447
|
+
|
|
1448
|
+
## How it works
|
|
1449
|
+
|
|
1450
|
+
`ext/haskell_match` is a Rust crate with a pure core and a thin Ruby layer:
|
|
1451
|
+
|
|
1452
|
+
* `core::lexer`, `core::parser` — Haskell pattern and `data` declaration syntax.
|
|
1453
|
+
* `core::types`, `core::resolve` — the type environment and name resolution
|
|
1454
|
+
(arity checks, record fields, variable numbering).
|
|
1455
|
+
* `core::typecheck` — every position across all clauses must have one type.
|
|
1456
|
+
* `core::exhaust` — Maranget's usefulness algorithm: redundancy and the list
|
|
1457
|
+
of unmatched patterns, including literal positions (`p1 where p1 is not one
|
|
1458
|
+
of {0, 1}`).
|
|
1459
|
+
* `core::tree` — compilation to a decision tree over numbered value slots.
|
|
1460
|
+
* `ruby::runtime` — evaluation over Ruby values, including Strings and lazy
|
|
1461
|
+
lists viewed as lists; `ruby` — the `Native` module, the trampoline that
|
|
1462
|
+
handles `tail`/`defer` markers and the Fiber-segmented recursion.
|
|
1463
|
+
* `lib/haskell_match/pattern_ast.rb` — the in-place pattern syntax, rendered
|
|
1464
|
+
to Haskell text; `lazy_list.rb` — memoised lazy lists.
|
|
1465
|
+
|
|
1466
|
+
The pure core has its own `cargo test` suite; the Ruby suite covers the
|
|
1467
|
+
public API, GC stress, threads and Ractors.
|
|
1468
|
+
|
|
1469
|
+
## License
|
|
1470
|
+
|
|
1471
|
+
Licensed under either of
|
|
1472
|
+
|
|
1473
|
+
* Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or
|
|
1474
|
+
http://www.apache.org/licenses/LICENSE-2.0)
|
|
1475
|
+
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
|
1476
|
+
http://opensource.org/licenses/MIT)
|
|
1477
|
+
|
|
1478
|
+
at your option.
|
|
1479
|
+
|
|
1480
|
+
### Contribution
|
|
1481
|
+
|
|
1482
|
+
Unless you explicitly state otherwise, any contribution intentionally submitted
|
|
1483
|
+
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
|
1484
|
+
dual licensed as above, without any additional terms or conditions.
|