implica 0.3.0__py3-none-any.whl

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.

Potentially problematic release.


This version of implica might be problematic. Click here for more details.

@@ -0,0 +1,945 @@
1
+ Metadata-Version: 2.1
2
+ Name: implica
3
+ Version: 0.3.0
4
+ Summary: A package for working with graphs representing minimal implicational logic models
5
+ Author: Carlos Fernandez
6
+ Author-email: carlos.ferlo@outlook.com
7
+ Requires-Python: >=3.10,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.10
10
+ Classifier: Programming Language :: Python :: 3.11
11
+ Classifier: Programming Language :: Python :: 3.12
12
+ Requires-Dist: pydantic (>=2.0.0,<3.0.0)
13
+ Description-Content-Type: text/markdown
14
+
15
+ # Implica
16
+
17
+ A Python package for working with graphs representing minimal implicational logic models. This library provides tools for constructing and manipulating type systems based on combinatory logic, with support for transactional graph operations.
18
+
19
+ **Import as `imp` for a clean, concise API!**
20
+
21
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10+-blue.svg)](https://www.python.org/downloads/)
22
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
23
+
24
+ ## Features
25
+
26
+ - 🎯 **Type System**: Build complex type expressions using variables and applications (function types)
27
+ - 🧩 **Combinators**: Work with S and K combinators from combinatory logic
28
+ - 📊 **Graph Structure**: Represent type transformations as nodes and edges in a directed graph
29
+ - 🔄 **Transactional Operations**: Safely modify graphs with automatic rollback on failure
30
+ - 📦 **Bulk Operations**: Add multiple nodes or edges atomically with `add_many_nodes` and `add_many_edges`
31
+ - 🛡️ **Idempotent Mutations**: Use `try_add_node` and `try_add_edge` for safe, duplicate-tolerant operations
32
+ - ✅ **Validation**: Ensure graph consistency with built-in validation
33
+ - 🚀 **Performance**: Optimized data structures for O(1) lookups and efficient traversal
34
+
35
+ ## Installation
36
+
37
+ ### Using Poetry (recommended)
38
+
39
+ ```bash
40
+ poetry add implica
41
+ ```
42
+
43
+ ### Using pip
44
+
45
+ ```bash
46
+ pip install implica
47
+ ```
48
+
49
+ ### From source
50
+
51
+ ```bash
52
+ git clone https://github.com/CarlosFerLo/implicational-logic-graph.git
53
+ cd implicational-logic-graph
54
+ poetry install
55
+ ```
56
+
57
+ ## Quick Start
58
+
59
+ ```python
60
+ import implica as imp
61
+
62
+ # Create type variables
63
+ A = imp.var("A")
64
+ B = imp.var("B")
65
+
66
+ # Create a function type: A -> B
67
+ func_type = imp.app(A, B)
68
+
69
+ # Create a graph with nodes
70
+ graph = imp.Graph()
71
+ with graph.connect() as conn:
72
+ conn.add_node(imp.node(A))
73
+ conn.add_node(imp.node(B))
74
+
75
+ print(f"Graph has {graph.node_count()} nodes")
76
+ ```
77
+
78
+ ## Core Concepts
79
+
80
+ ### Types
81
+
82
+ The library provides a type system for minimal implicational logic:
83
+
84
+ - **Variable**: Atomic type variables (e.g., `A`, `B`, `C`)
85
+ - **Application**: Function types representing `input_type -> output_type`
86
+
87
+ ```python
88
+ import implica as imp
89
+
90
+ # Simple type variables
91
+ A = imp.var("A")
92
+ B = imp.var("B")
93
+ C = imp.var("C")
94
+
95
+ # Function type: A -> B
96
+ simple_function = imp.app(A, B)
97
+
98
+ # Complex type: (A -> B) -> C
99
+ complex_function = imp.app(imp.app(A, B), C)
100
+
101
+ # Nested type: A -> (B -> C)
102
+ nested_function = imp.app(A, imp.app(B, C))
103
+
104
+ print(simple_function) # Output: A -> B
105
+ print(complex_function) # Output: (A -> B) -> C
106
+ print(nested_function) # Output: A -> B -> C
107
+ ```
108
+
109
+ ### Combinators
110
+
111
+ Combinators represent transformations between types. The library includes the S and K combinators from combinatory logic:
112
+
113
+ ```python
114
+ import implica as imp
115
+
116
+ A = imp.var("A")
117
+ B = imp.var("B")
118
+ C = imp.var("C")
119
+
120
+ # S combinator: (A -> B -> C) -> (A -> B) -> A -> C
121
+ s_comb = imp.S(A, B, C)
122
+ print(s_comb) # Output: S: (A -> B -> C) -> (A -> B) -> A -> C
123
+
124
+ # K combinator: A -> B -> A
125
+ k_comb = imp.K(A, B)
126
+ print(k_comb) # Output: K: A -> B -> A
127
+
128
+ # Custom combinator
129
+ identity = imp.Combinator(name="I", type=imp.app(A, A))
130
+ print(identity) # Output: I: A -> A
131
+ ```
132
+
133
+ ### Graph Elements
134
+
135
+ #### Nodes
136
+
137
+ Nodes represent types in the graph:
138
+
139
+ ```python
140
+ import implica as imp
141
+
142
+ A = imp.var("A")
143
+ B = imp.var("B")
144
+
145
+ # Create nodes
146
+ node_a = imp.node(A)
147
+ node_b = imp.node(B)
148
+ node_func = imp.node(imp.app(A, B))
149
+
150
+ print(node_a) # Output: Node(A)
151
+ print(node_func) # Output: Node(A -> B)
152
+ ```
153
+
154
+ #### Edges
155
+
156
+ Edges represent combinator transformations between types:
157
+
158
+ ```python
159
+ import implica as imp
160
+
161
+ A = imp.var("A")
162
+ B = imp.var("B")
163
+
164
+ # Create nodes
165
+ n1 = imp.node(A)
166
+ n2 = imp.node(B)
167
+
168
+ # Create a combinator that transforms A to B
169
+ comb = imp.Combinator(name="f", type=imp.app(A, B))
170
+
171
+ # Create an edge
172
+ e = imp.edge(n1, n2, comb)
173
+ print(e) # Output: A --[f: A -> B]--> B
174
+ ```
175
+
176
+ ### Graph Operations
177
+
178
+ #### Creating and Modifying Graphs
179
+
180
+ ```python
181
+ import implica as imp
182
+
183
+ # Create an empty graph
184
+ graph = imp.Graph()
185
+
186
+ # Create types and nodes
187
+ A = imp.var("A")
188
+ B = imp.var("B")
189
+ C = imp.var("C")
190
+
191
+ n_a = imp.node(A)
192
+ n_b = imp.node(B)
193
+ n_c = imp.node(C)
194
+
195
+ # Use transactional connections to modify the graph
196
+ with graph.connect() as conn:
197
+ conn.add_node(n_a)
198
+ conn.add_node(n_b)
199
+ conn.add_node(n_c)
200
+
201
+ print(f"Nodes: {graph.node_count()}") # Output: Nodes: 3
202
+
203
+ # Add edges
204
+ comb_ab = imp.Combinator(name="f", type=imp.app(A, B))
205
+ comb_bc = imp.Combinator(name="g", type=imp.app(B, C))
206
+
207
+ with graph.connect() as conn:
208
+ conn.add_edge(imp.edge(n_a, n_b, comb_ab))
209
+ conn.add_edge(imp.edge(n_b, n_c, comb_bc))
210
+
211
+ print(f"Edges: {graph.edge_count()}") # Output: Edges: 2
212
+ ```
213
+
214
+ #### Querying Graphs
215
+
216
+ ```python
217
+ # Get node by UID
218
+ node_retrieved = graph.get_node(n_a.uid)
219
+
220
+ # Get node by type
221
+ node_by_type = graph.get_node_by_type(A)
222
+
223
+ # Check if node exists
224
+ exists = graph.has_node(n_a.uid)
225
+
226
+ # Get outgoing edges from a node
227
+ outgoing = graph.get_outgoing_edges(n_a.uid)
228
+ print(f"Outgoing from A: {len(outgoing)}")
229
+
230
+ # Get incoming edges to a node
231
+ incoming = graph.get_incoming_edges(n_c.uid)
232
+ print(f"Incoming to C: {len(incoming)}")
233
+
234
+ # Iterate over all nodes
235
+ for n in graph.nodes():
236
+ print(f"Node: {n.type}")
237
+
238
+ # Iterate over all edges
239
+ for e in graph.edges():
240
+ print(f"Edge: {e}")
241
+ ```
242
+
243
+ #### Graph Validation
244
+
245
+ ```python
246
+ # Validate graph consistency
247
+ try:
248
+ graph.validate()
249
+ print("Graph is valid!")
250
+ except ValueError as e:
251
+ print(f"Graph validation failed: {e}")
252
+ ```
253
+
254
+ ### Advanced Mutations
255
+
256
+ The library provides several mutation types for flexible graph modifications:
257
+
258
+ #### Bulk Mutations
259
+
260
+ Add multiple nodes or edges in a single atomic operation:
261
+
262
+ ```python
263
+ import implica as imp
264
+
265
+ graph = imp.Graph()
266
+
267
+ # Create multiple nodes
268
+ nodes = [imp.node(imp.var(f"T{i}")) for i in range(5)]
269
+
270
+ # Add all nodes atomically - if any fails, all are rolled back
271
+ with graph.connect() as conn:
272
+ conn.add_many_nodes(nodes)
273
+
274
+ # Create multiple edges
275
+ A, B, C = imp.var("A"), imp.var("B"), imp.var("C")
276
+ combinators = [
277
+ imp.Combinator("f1", imp.app(A, B)),
278
+ imp.Combinator("f2", imp.app(B, C)),
279
+ ]
280
+
281
+ # Add all edges atomically
282
+ with graph.connect() as conn:
283
+ conn.add_many_edges(combinators)
284
+ ```
285
+
286
+ **Benefits:**
287
+
288
+ - All-or-nothing semantics: if any operation fails, all are rolled back
289
+ - More efficient than individual additions
290
+ - Cleaner code for batch operations
291
+
292
+ **Removal Operations:**
293
+
294
+ The same bulk operations are available for removing nodes and edges:
295
+
296
+ ```python
297
+ # Remove multiple nodes at once
298
+ node_uids = [n.uid for n in nodes[:3]]
299
+ with graph.connect() as conn:
300
+ conn.remove_many_nodes(node_uids)
301
+
302
+ # Remove multiple edges at once
303
+ edge_uids = [e.uid for e in some_edges]
304
+ with graph.connect() as conn:
305
+ conn.remove_many_edges(edge_uids)
306
+ ```
307
+
308
+ #### Idempotent Mutations
309
+
310
+ Use safe mutations that don't fail when items already exist or don't exist:
311
+
312
+ ```python
313
+ import implica as imp
314
+
315
+ graph = imp.Graph()
316
+
317
+ A = imp.var("A")
318
+ n_a = imp.node(A)
319
+
320
+ # Regular add would fail on duplicate
321
+ with graph.connect() as conn:
322
+ conn.add_node(n_a)
323
+
324
+ # try_add_node won't fail if node exists
325
+ with graph.connect() as conn:
326
+ conn.try_add_node(n_a) # No error, even though n_a exists
327
+
328
+ # Same with edges
329
+ B = imp.var("B")
330
+ with graph.connect() as conn:
331
+ conn.try_add_node(imp.node(B))
332
+
333
+ comb = imp.Combinator("f", imp.app(A, B))
334
+ with graph.connect() as conn:
335
+ conn.try_add_edge(comb) # Adds the edge
336
+
337
+ with graph.connect() as conn:
338
+ conn.try_add_edge(comb) # No error, edge already exists
339
+
340
+ # Safe removal operations
341
+ with graph.connect() as conn:
342
+ conn.try_remove_node(n_a.uid) # Removes the node
343
+
344
+ with graph.connect() as conn:
345
+ conn.try_remove_node(n_a.uid) # No error, node doesn't exist anymore
346
+
347
+ # Same with edges
348
+ with graph.connect() as conn:
349
+ conn.try_remove_edge("some_edge_uid") # No error even if edge doesn't exist
350
+ ```
351
+
352
+ **Use Cases:**
353
+
354
+ - Building graphs from multiple sources where duplicates may occur
355
+ - Idempotent initialization routines
356
+ - Avoiding explicit existence checks before adding or removing elements
357
+ - Incremental graph construction without duplicate errors
358
+ - Cleanup operations that should be safe to run multiple times
359
+
360
+ ## Usage Examples
361
+
362
+ ### Example 1: Simple Type Chain
363
+
364
+ Build a chain of type transformations:
365
+
366
+ ```python
367
+ import implica as imp
368
+
369
+ # Create graph
370
+ graph = imp.Graph()
371
+
372
+ # Define types
373
+ types = [imp.var(name) for name in ["A", "B", "C", "D"]]
374
+ nodes = [imp.node(t) for t in types]
375
+
376
+ # Add nodes
377
+ with graph.connect() as conn:
378
+ for n in nodes:
379
+ conn.add_node(n)
380
+
381
+ # Create transformation chain: A -> B -> C -> D
382
+ with graph.connect() as conn:
383
+ for i in range(len(nodes) - 1):
384
+ comb = imp.Combinator(
385
+ name=f"f{i}",
386
+ type=imp.app(types[i], types[i + 1])
387
+ )
388
+ conn.add_edge(imp.edge(nodes[i], nodes[i + 1], comb))
389
+
390
+ print(f"Created chain with {graph.node_count()} nodes and {graph.edge_count()} edges")
391
+
392
+ # Query the chain
393
+ a_node = graph.get_node_by_type(imp.var("A"))
394
+ outgoing = graph.get_outgoing_edges(a_node.uid)
395
+ print(f"A has {len(outgoing)} outgoing edge(s)")
396
+ ```
397
+
398
+ ### Example 2: Complex Type Structure
399
+
400
+ Work with higher-order functions:
401
+
402
+ ```python
403
+ import implica as imp
404
+
405
+ # Create graph
406
+ graph = imp.Graph()
407
+
408
+ # Define type variables
409
+ A = imp.var("A")
410
+ B = imp.var("B")
411
+ C = imp.var("C")
412
+
413
+ # Create complex types
414
+ # Type 1: A
415
+ # Type 2: B
416
+ # Type 3: A -> B
417
+ # Type 4: (A -> B) -> C
418
+ t1 = A
419
+ t2 = B
420
+ t3 = imp.app(A, B)
421
+ t4 = imp.app(t3, C)
422
+
423
+ nodes = [imp.node(t) for t in [t1, t2, t3, t4]]
424
+
425
+ # Add nodes
426
+ with graph.connect() as conn:
427
+ for n in nodes:
428
+ conn.add_node(n)
429
+
430
+ # Add S and K combinators as edges
431
+ s_comb = imp.S(A, B, C)
432
+ k_comb = imp.K(A, B)
433
+
434
+ # Note: You would connect these based on your specific logic model
435
+ print(f"Created graph with {graph.node_count()} nodes")
436
+ print(f"S combinator type: {s_comb.type}")
437
+ print(f"K combinator type: {k_comb.type}")
438
+ ```
439
+
440
+ ### Example 3: Transactional Rollback
441
+
442
+ Demonstrate automatic rollback on failure:
443
+
444
+ ```python
445
+ import implica as imp
446
+
447
+ graph = imp.Graph()
448
+
449
+ A = imp.var("A")
450
+ B = imp.var("B")
451
+ n_a = imp.node(A)
452
+ n_b = imp.node(B)
453
+
454
+ # Add initial nodes
455
+ with graph.connect() as conn:
456
+ conn.add_node(n_a)
457
+ conn.add_node(n_b)
458
+
459
+ print(f"Initial nodes: {graph.node_count()}") # Output: 2
460
+
461
+ # Try to add invalid edge (will fail and rollback)
462
+ try:
463
+ with graph.connect() as conn:
464
+ # This will succeed
465
+ C = imp.var("C")
466
+ n_c = imp.node(C)
467
+ conn.add_node(n_c)
468
+
469
+ # This will fail (trying to add duplicate node)
470
+ conn.add_node(n_a) # Already exists!
471
+ except RuntimeError as e:
472
+ print(f"Transaction failed: {e}")
473
+
474
+ # Graph remains unchanged
475
+ print(f"Nodes after failed transaction: {graph.node_count()}") # Output: 2
476
+ print(f"Has C: {graph.get_node_by_type(imp.var('C')) is not None}") # Output: False
477
+ ```
478
+
479
+ ### Example 4: Building a Proof Tree
480
+
481
+ Create a structure representing logical derivations:
482
+
483
+ ```python
484
+ import implica as imp
485
+
486
+ # Create a graph representing a proof
487
+ graph = imp.Graph()
488
+
489
+ # Axioms (base types)
490
+ P = imp.var("P")
491
+ Q = imp.var("Q")
492
+ R = imp.var("R")
493
+
494
+ # Derived types (implications)
495
+ PQ = imp.app(P, Q) # P -> Q
496
+ QR = imp.app(Q, R) # Q -> R
497
+ PR = imp.app(P, R) # P -> R (conclusion)
498
+
499
+ # Create nodes for each type in the proof
500
+ nodes_dict = {
501
+ "P": imp.node(P),
502
+ "Q": imp.node(Q),
503
+ "R": imp.node(R),
504
+ "P->Q": imp.node(PQ),
505
+ "Q->R": imp.node(QR),
506
+ "P->R": imp.node(PR),
507
+ }
508
+
509
+ # Add all nodes
510
+ with graph.connect() as conn:
511
+ for n in nodes_dict.values():
512
+ conn.add_node(n)
513
+
514
+ # Add inference rules as edges
515
+ with graph.connect() as conn:
516
+ # Modus Ponens: P, P->Q ⊢ Q
517
+ mp1 = imp.Combinator(name="MP1", type=imp.app(P, Q))
518
+ conn.add_edge(imp.edge(nodes_dict["P"], nodes_dict["Q"], mp1))
519
+
520
+ # Modus Ponens: Q, Q->R ⊢ R
521
+ mp2 = imp.Combinator(name="MP2", type=imp.app(Q, R))
522
+ conn.add_edge(imp.edge(nodes_dict["Q"], nodes_dict["R"], mp2))
523
+
524
+ # Composition: P->Q, Q->R ⊢ P->R
525
+ comp = imp.Combinator(name="Comp", type=imp.app(P, R))
526
+ conn.add_edge(imp.edge(nodes_dict["P"], nodes_dict["R"], comp))
527
+
528
+ print(f"Proof tree has {graph.node_count()} types")
529
+ print(f"Proof tree has {graph.edge_count()} inference rules")
530
+
531
+ # Validate the proof structure
532
+ graph.validate()
533
+ print("Proof structure is valid!")
534
+ ```
535
+
536
+ ### Example 5: Exploring Graph Structure
537
+
538
+ Navigate and analyze the graph:
539
+
540
+ ```python
541
+ import implica as imp
542
+
543
+ # Build a diamond-shaped graph
544
+ # A
545
+ # / \
546
+ # B C
547
+ # \ /
548
+ # D
549
+
550
+ graph = imp.Graph()
551
+
552
+ A, B, C, D = imp.var("A"), imp.var("B"), imp.var("C"), imp.var("D")
553
+ n_a, n_b, n_c, n_d = imp.node(A), imp.node(B), imp.node(C), imp.node(D)
554
+
555
+ with graph.connect() as conn:
556
+ conn.add_node(n_a)
557
+ conn.add_node(n_b)
558
+ conn.add_node(n_c)
559
+ conn.add_node(n_d)
560
+
561
+ # A -> B, A -> C
562
+ conn.add_edge(imp.edge(n_a, n_b, imp.Combinator("f1", imp.app(A, B))))
563
+ conn.add_edge(imp.edge(n_a, n_c, imp.Combinator("f2", imp.app(A, C))))
564
+
565
+ # B -> D, C -> D
566
+ conn.add_edge(imp.edge(n_b, n_d, imp.Combinator("g1", imp.app(B, D))))
567
+ conn.add_edge(imp.edge(n_c, n_d, imp.Combinator("g2", imp.app(C, D))))
568
+
569
+ # Analyze the structure
570
+ print("=== Graph Analysis ===")
571
+ print(f"Total nodes: {graph.node_count()}")
572
+ print(f"Total edges: {graph.edge_count()}")
573
+
574
+ # Find all paths from A
575
+ print("\nFrom A:")
576
+ for e in graph.get_outgoing_edges(n_a.uid):
577
+ print(f" -> {e.dst_node.type} via {e.combinator.name}")
578
+
579
+ # Find all paths to D
580
+ print("\nTo D:")
581
+ for e in graph.get_incoming_edges(n_d.uid):
582
+ print(f" <- {e.src_node.type} via {e.combinator.name}")
583
+
584
+ # Check connectivity
585
+ print("\nNode connectivity:")
586
+ for n in graph.nodes():
587
+ incoming = len(graph.get_incoming_edges(n.uid))
588
+ outgoing = len(graph.get_outgoing_edges(n.uid))
589
+ print(f" {n.type}: {incoming} in, {outgoing} out")
590
+ ```
591
+
592
+ ### Example 6: Bulk Operations
593
+
594
+ Add multiple nodes and edges efficiently:
595
+
596
+ ```python
597
+ import implica as imp
598
+
599
+ graph = imp.Graph()
600
+
601
+ # Create many type variables at once
602
+ type_vars = [imp.var(f"T{i}") for i in range(10)]
603
+ nodes = [imp.node(t) for t in type_vars]
604
+
605
+ # Add all nodes in a single transaction
606
+ with graph.connect() as conn:
607
+ conn.add_many_nodes(nodes)
608
+
609
+ print(f"Added {graph.node_count()} nodes in one operation")
610
+
611
+ # Create a chain of transformations
612
+ combinators = []
613
+ for i in range(len(type_vars) - 1):
614
+ comb = imp.Combinator(
615
+ name=f"f{i}",
616
+ type=imp.app(type_vars[i], type_vars[i + 1])
617
+ )
618
+ combinators.append(comb)
619
+
620
+ # Add all edges in a single transaction
621
+ with graph.connect() as conn:
622
+ conn.add_many_edges(combinators)
623
+
624
+ print(f"Added {graph.edge_count()} edges in one operation")
625
+
626
+ # If any node or edge fails, all are rolled back atomically
627
+ try:
628
+ with graph.connect() as conn:
629
+ # This will fail because nodes[0] already exists
630
+ conn.add_many_nodes([nodes[0], imp.node(imp.var("NEW"))])
631
+ except RuntimeError:
632
+ print("Transaction rolled back - no new nodes added")
633
+ ```
634
+
635
+ ### Example 7: Idempotent Operations
636
+
637
+ Use safe mutations that don't fail on duplicates:
638
+
639
+ ```python
640
+ import implica as imp
641
+
642
+ graph = imp.Graph()
643
+
644
+ A = imp.var("A")
645
+ B = imp.var("B")
646
+ n_a = imp.node(A)
647
+ n_b = imp.node(B)
648
+
649
+ # Add nodes normally
650
+ with graph.connect() as conn:
651
+ conn.add_node(n_a)
652
+ conn.add_node(n_b)
653
+
654
+ print(f"Initial nodes: {graph.node_count()}") # Output: 2
655
+
656
+ # Try to add nodes again - won't fail!
657
+ with graph.connect() as conn:
658
+ conn.try_add_node(n_a) # Already exists, but no error
659
+ conn.try_add_node(n_b) # Already exists, but no error
660
+ conn.try_add_node(imp.node(imp.var("C"))) # New node, will be added
661
+
662
+ print(f"Nodes after try_add: {graph.node_count()}") # Output: 3
663
+
664
+ # Same with edges
665
+ comb_ab = imp.Combinator("f", imp.app(A, B))
666
+
667
+ with graph.connect() as conn:
668
+ conn.try_add_edge(comb_ab) # Will be added
669
+
670
+ print(f"Edges: {graph.edge_count()}") # Output: 1
671
+
672
+ with graph.connect() as conn:
673
+ conn.try_add_edge(comb_ab) # Already exists, but no error
674
+
675
+ print(f"Edges after try_add: {graph.edge_count()}") # Output: 1
676
+
677
+ # Useful for idempotent operations and avoiding duplicate checks
678
+ def ensure_basic_types(graph, type_names):
679
+ """Ensure all basic types exist in the graph."""
680
+ with graph.connect() as conn:
681
+ for name in type_names:
682
+ conn.try_add_node(imp.node(imp.var(name)))
683
+
684
+ # Can call this multiple times safely
685
+ ensure_basic_types(graph, ["A", "B", "C", "D"])
686
+ ensure_basic_types(graph, ["C", "D", "E", "F"]) # C and D won't cause errors
687
+
688
+ print(f"Final nodes: {graph.node_count()}") # Output: 6 (A, B, C, D, E, F)
689
+ ```
690
+
691
+ ### Example 8: Bulk Removal Operations
692
+
693
+ Remove multiple elements efficiently:
694
+
695
+ ```python
696
+ import implica as imp
697
+
698
+ graph = imp.Graph()
699
+
700
+ # Build a graph with multiple nodes and edges
701
+ types = [imp.var(f"T{i}") for i in range(10)]
702
+ nodes = [imp.node(t) for t in types]
703
+
704
+ with graph.connect() as conn:
705
+ conn.add_many_nodes(nodes)
706
+
707
+ # Create edges
708
+ combinators = []
709
+ for i in range(len(types) - 1):
710
+ comb = imp.Combinator(f"f{i}", imp.app(types[i], types[i + 1]))
711
+ combinators.append(comb)
712
+
713
+ with graph.connect() as conn:
714
+ conn.add_many_edges(combinators)
715
+
716
+ print(f"Initial: {graph.node_count()} nodes, {graph.edge_count()} edges")
717
+ # Output: Initial: 10 nodes, 9 edges
718
+
719
+ # Remove multiple edges at once
720
+ edge_uids = [graph.get_outgoing_edges(nodes[i].uid)[0].uid for i in range(3)]
721
+ with graph.connect() as conn:
722
+ conn.remove_many_edges(edge_uids)
723
+
724
+ print(f"After edge removal: {graph.edge_count()} edges")
725
+ # Output: After edge removal: 6 edges
726
+
727
+ # Remove multiple nodes at once (and their connected edges)
728
+ node_uids = [nodes[i].uid for i in range(5)]
729
+ with graph.connect() as conn:
730
+ conn.remove_many_nodes(node_uids)
731
+
732
+ print(f"After node removal: {graph.node_count()} nodes, {graph.edge_count()} edges")
733
+ # Output: After node removal: 5 nodes, 0 edges (edges were connected to removed nodes)
734
+
735
+ # Safe removal with try_remove (won't fail if already removed)
736
+ with graph.connect() as conn:
737
+ conn.try_remove_node(nodes[0].uid) # Already removed, no error
738
+ conn.try_remove_edge("nonexistent_uid") # Doesn't exist, no error
739
+
740
+ print(f"Final: {graph.node_count()} nodes") # Output: Final: 5 nodes
741
+ ```
742
+
743
+ ### Example 9: Safe Cleanup Operations
744
+
745
+ Use idempotent removal for cleanup tasks:
746
+
747
+ ```python
748
+ import implica as imp
749
+
750
+ def cleanup_temporary_nodes(graph, temp_node_uids):
751
+ """
752
+ Remove temporary nodes if they exist.
753
+ This function is safe to call multiple times.
754
+ """
755
+ with graph.connect() as conn:
756
+ for uid in temp_node_uids:
757
+ conn.try_remove_node(uid)
758
+
759
+ graph = imp.Graph()
760
+
761
+ # Add some nodes
762
+ A, B, C = imp.var("A"), imp.var("B"), imp.var("C")
763
+ n_a, n_b, n_c = imp.node(A), imp.node(B), imp.node(C)
764
+
765
+ with graph.connect() as conn:
766
+ conn.add_many_nodes([n_a, n_b, n_c])
767
+
768
+ print(f"Initial nodes: {graph.node_count()}") # Output: 3
769
+
770
+ # Clean up - safe to call multiple times
771
+ temp_uids = [n_a.uid, n_b.uid]
772
+ cleanup_temporary_nodes(graph, temp_uids)
773
+ print(f"After cleanup: {graph.node_count()}") # Output: 1
774
+
775
+ # Call again - won't fail even though nodes are already removed
776
+ cleanup_temporary_nodes(graph, temp_uids)
777
+ print(f"After second cleanup: {graph.node_count()}") # Output: 1
778
+
779
+ # Combine try_remove with try_add for flexible graph updates
780
+ def ensure_graph_state(graph, required_nodes, forbidden_nodes):
781
+ """
782
+ Ensure graph has required nodes and doesn't have forbidden ones.
783
+ """
784
+ with graph.connect() as conn:
785
+ # Add required nodes (idempotent)
786
+ for n in required_nodes:
787
+ conn.try_add_node(n)
788
+
789
+ # Remove forbidden nodes (idempotent)
790
+ for uid in forbidden_nodes:
791
+ conn.try_remove_node(uid)
792
+
793
+ # Can call this function repeatedly to maintain desired state
794
+ ensure_graph_state(
795
+ graph,
796
+ required_nodes=[imp.node(imp.var("X")), imp.node(imp.var("Y"))],
797
+ forbidden_nodes=[n_c.uid]
798
+ )
799
+
800
+ print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
801
+ ```
802
+
803
+ ## API Reference
804
+
805
+ ### Core Module (`implica.core`)
806
+
807
+ **Types:**
808
+
809
+ - `var(name: str) -> Variable`: Create a type variable
810
+ - `app(input_type: BaseType, output_type: BaseType) -> Application`: Create a function type
811
+ - `Variable`: Atomic type variable
812
+ - `Application`: Function application type
813
+
814
+ **Combinators:**
815
+
816
+ - `S(A, B, C) -> Combinator`: Create S combinator
817
+ - `K(A, B) -> Combinator`: Create K combinator
818
+ - `Combinator`: Generic combinator with name and type
819
+
820
+ ### Graph Module (`implica.graph`)
821
+
822
+ **Elements:**
823
+
824
+ - `node(type: BaseType) -> Node`: Create a node
825
+ - `edge(src: Node, dst: Node, comb: Combinator) -> Edge`: Create an edge
826
+ - `Node`: Graph node representing a type
827
+ - `Edge`: Graph edge representing a transformation
828
+
829
+ **Graph:**
830
+
831
+ - `Graph()`: Create a new empty graph
832
+ - `graph.connect() -> Connection`: Create a transactional connection
833
+ - `graph.validate() -> bool`: Validate graph consistency
834
+ - `graph.has_node(uid: str) -> bool`: Check if node exists
835
+ - `graph.get_node(uid: str) -> Node`: Get node by UID
836
+ - `graph.get_node_by_type(type: BaseType) -> Optional[Node]`: Get node by type
837
+ - `graph.get_outgoing_edges(uid: str) -> list[Edge]`: Get outgoing edges
838
+ - `graph.get_incoming_edges(uid: str) -> list[Edge]`: Get incoming edges
839
+ - `graph.nodes() -> Iterator[Node]`: Iterate over nodes
840
+ - `graph.edges() -> Iterator[Edge]`: Iterate over edges
841
+ - `graph.node_count() -> int`: Get number of nodes
842
+ - `graph.edge_count() -> int`: Get number of edges
843
+
844
+ **Connection:**
845
+
846
+ - `Connection`: Transactional graph modification context
847
+ - `conn.add_node(node: Node) -> Connection`: Queue node addition
848
+ - `conn.add_edge(edge: Edge) -> Connection`: Queue edge addition
849
+ - `conn.remove_node(uid: str) -> Connection`: Queue node removal
850
+ - `conn.remove_edge(uid: str) -> Connection`: Queue edge removal
851
+ - `conn.add_many_nodes(nodes: list[Node]) -> Connection`: Queue multiple node additions
852
+ - `conn.add_many_edges(combinators: list[Combinator]) -> Connection`: Queue multiple edge additions
853
+ - `conn.try_add_node(node: Node) -> Connection`: Queue node addition (no error if exists)
854
+ - `conn.try_add_edge(combinator: Combinator) -> Connection`: Queue edge addition (no error if exists)
855
+ - `conn.remove_many_nodes(node_uids: list[str]) -> Connection`: Queue multiple node removals
856
+ - `conn.remove_many_edges(edge_uids: list[str]) -> Connection`: Queue multiple edge removals
857
+ - `conn.try_remove_node(node_uid: str) -> Connection`: Queue node removal (no error if not exists)
858
+ - `conn.try_remove_edge(edge_uid: str) -> Connection`: Queue edge removal (no error if not exists)
859
+ - `conn.commit()`: Apply all queued operations
860
+ - `conn.rollback()`: Discard all queued operations
861
+
862
+ ### Mutations Module (`implica.mutations`)
863
+
864
+ - `Mutation`: Abstract base class for all mutations
865
+ - `AddNode(node)`: Add a single node
866
+ - `RemoveNode(node_uid)`: Remove a single node
867
+ - `AddEdge(edge)`: Add a single edge
868
+ - `RemoveEdge(edge_uid)`: Remove a single edge
869
+ - `AddManyNodes(nodes)`: Add multiple nodes atomically
870
+ - `AddManyEdges(edges)`: Add multiple edges atomically
871
+ - `TryAddNode(node)`: Add a node or do nothing if it exists
872
+ - `TryAddEdge(edge)`: Add an edge or do nothing if it exists
873
+ - `RemoveManyNodes(node_uids)`: Remove multiple nodes atomically
874
+ - `RemoveManyEdges(edge_uids)`: Remove multiple edges atomically
875
+ - `TryRemoveNode(node_uid)`: Remove a node or do nothing if it doesn't exist
876
+ - `TryRemoveEdge(edge_uid)`: Remove an edge or do nothing if it doesn't exist
877
+
878
+ ## Development
879
+
880
+ ### Setup
881
+
882
+ ```bash
883
+ # Clone the repository
884
+ git clone https://github.com/carlosFerLo/implicational-logic-graph.git
885
+ cd implicational-logic-graph
886
+
887
+ # Install dependencies
888
+ poetry install
889
+
890
+ # Run tests
891
+ poetry run pytest
892
+
893
+ # Run tests with coverage
894
+ poetry run pytest --cov=src/implicational_logic_graph
895
+ ```
896
+
897
+ ### Running Tests
898
+
899
+ ```bash
900
+ # Run all tests
901
+ poetry run pytest
902
+
903
+ # Run specific test file
904
+ poetry run pytest tests/test_graph.py
905
+
906
+ # Run with verbose output
907
+ poetry run pytest -v
908
+
909
+ # Run with coverage report
910
+ poetry run pytest --cov=src/implica --cov-report=html
911
+ ```
912
+
913
+ ## Contributing
914
+
915
+ Contributions are welcome! Please feel free to submit a Pull Request.
916
+
917
+ 1. Fork the repository
918
+ 2. Create your feature branch (`git checkout -b feature/amazing-feature`)
919
+ 3. Commit your changes (`git commit -m 'Add some amazing feature'`)
920
+ 4. Push to the branch (`git push origin feature/amazing-feature`)
921
+ 5. Open a Pull Request
922
+
923
+ ## License
924
+
925
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
926
+
927
+ ## Acknowledgments
928
+
929
+ - Based on concepts from combinatory logic and type theory
930
+ - Inspired by minimal implicational logic models
931
+ - Built with [Pydantic](https://pydantic-docs.helpmanual.io/) for data validation
932
+
933
+ ## Citation
934
+
935
+ If you use this library in your research, please cite:
936
+
937
+ ```bibtex
938
+ @software{implica,
939
+ author = {Carlos Fernandez},
940
+ title = {Implica: Implicational Logic Graph Library},
941
+ year = {2025},
942
+ url = {https://github.com/CarlosFerLo/implicational-logic-graph}
943
+ }
944
+ ```
945
+