implica 0.4.0__tar.gz → 0.4.2__tar.gz
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.
- implica-0.4.2/PKG-INFO +2302 -0
- implica-0.4.2/README.md +2287 -0
- {implica-0.4.0 → implica-0.4.2}/pyproject.toml +1 -1
- {implica-0.4.0 → implica-0.4.2}/src/implica/__init__.py +15 -1
- {implica-0.4.0 → implica-0.4.2}/src/implica/core/__init__.py +4 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/core/combinator.py +22 -38
- implica-0.4.2/src/implica/core/schema.py +215 -0
- implica-0.4.2/src/implica/graph/connection.py +1053 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/graph/elements.py +68 -24
- {implica-0.4.0 → implica-0.4.2}/src/implica/graph/graph.py +302 -1
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/__init__.py +8 -0
- implica-0.4.2/src/implica/mutations/try_update_edge.py +50 -0
- implica-0.4.2/src/implica/mutations/try_update_node.py +50 -0
- implica-0.4.2/src/implica/mutations/update_edge.py +50 -0
- implica-0.4.2/src/implica/mutations/update_node.py +50 -0
- implica-0.4.0/PKG-INFO +0 -1367
- implica-0.4.0/README.md +0 -1352
- implica-0.4.0/src/implica/graph/connection.py +0 -467
- {implica-0.4.0 → implica-0.4.2}/LICENSE +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/core/types.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/graph/__init__.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/add_edge.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/add_many_edges.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/add_many_nodes.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/add_node.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/base.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/remove_edge.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/remove_many_edges.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/remove_many_nodes.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/remove_node.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/try_add_edge.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/try_add_many_edges.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/try_add_many_nodes.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/try_add_node.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/try_remove_edge.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/try_remove_many_edges.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/try_remove_many_nodes.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/mutations/try_remove_node.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/utils/__init__.py +0 -0
- {implica-0.4.0 → implica-0.4.2}/src/implica/utils/parsing.py +0 -0
implica-0.4.2/PKG-INFO
ADDED
|
@@ -0,0 +1,2302 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: implica
|
|
3
|
+
Version: 0.4.2
|
|
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
|
+
[](https://www.python.org/downloads/)
|
|
22
|
+
[](LICENSE)
|
|
23
|
+
|
|
24
|
+
## Features
|
|
25
|
+
|
|
26
|
+
- 🎯 **Type System**: Build complex type expressions using variables and applications (function types)
|
|
27
|
+
- 🔍 **Variable Extraction**: Recursively extract all variables from any type expression
|
|
28
|
+
- 🧩 **Combinators**: Work with S and K combinators from combinatory logic
|
|
29
|
+
- 🔎 **Type Pattern Matching**: Match types against patterns with type variables (TypeSchema)
|
|
30
|
+
- 🎨 **Schema-Based Queries**: Filter nodes and edges by type structure using pattern matching
|
|
31
|
+
- 🔧 **Schema-Based Mutations**: Add, remove, and update elements matching type patterns
|
|
32
|
+
- 📊 **Graph Structure**: Represent type transformations as nodes and edges in a directed graph
|
|
33
|
+
- 🔄 **Transactional Operations**: Safely modify graphs with automatic rollback on failure
|
|
34
|
+
- 📦 **Bulk Operations**: Add multiple nodes or edges atomically with `add_many_nodes` and `add_many_edges`
|
|
35
|
+
- 🏷️ **Properties**: Attach custom metadata dictionaries to nodes and edges for enriched graph semantics
|
|
36
|
+
- 🛡️ **Idempotent Mutations**: Use `try_add_node` and `try_add_edge` for safe, duplicate-tolerant operations
|
|
37
|
+
- ✅ **Validation**: Ensure graph consistency with built-in validation
|
|
38
|
+
- 🚀 **Performance**: Optimized data structures for O(1) lookups and efficient traversal
|
|
39
|
+
|
|
40
|
+
## Installation
|
|
41
|
+
|
|
42
|
+
### Using Poetry (recommended)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
poetry add implica
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Using pip
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
pip install implica
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
### From source
|
|
55
|
+
|
|
56
|
+
```bash
|
|
57
|
+
git clone https://github.com/CarlosFerLo/implicational-logic-graph.git
|
|
58
|
+
cd implicational-logic-graph
|
|
59
|
+
poetry install
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## Quick Start
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
import implica as imp
|
|
66
|
+
|
|
67
|
+
# Create type variables
|
|
68
|
+
A = imp.var("A")
|
|
69
|
+
B = imp.var("B")
|
|
70
|
+
|
|
71
|
+
# Create a function type: A -> B
|
|
72
|
+
func_type = imp.app(A, B)
|
|
73
|
+
|
|
74
|
+
# Create a graph with nodes
|
|
75
|
+
graph = imp.Graph()
|
|
76
|
+
with graph.connect() as conn:
|
|
77
|
+
conn.add_node(imp.node(A))
|
|
78
|
+
conn.add_node(imp.node(B))
|
|
79
|
+
|
|
80
|
+
print(f"Graph has {graph.node_count()} nodes")
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
## Core Concepts
|
|
84
|
+
|
|
85
|
+
### Types
|
|
86
|
+
|
|
87
|
+
The library provides a type system for minimal implicational logic:
|
|
88
|
+
|
|
89
|
+
- **Variable**: Atomic type variables (e.g., `A`, `B`, `C`)
|
|
90
|
+
- **Application**: Function types representing `input_type -> output_type`
|
|
91
|
+
|
|
92
|
+
```python
|
|
93
|
+
import implica as imp
|
|
94
|
+
|
|
95
|
+
# Simple type variables
|
|
96
|
+
A = imp.var("A")
|
|
97
|
+
B = imp.var("B")
|
|
98
|
+
C = imp.var("C")
|
|
99
|
+
|
|
100
|
+
# Function type: A -> B
|
|
101
|
+
simple_function = imp.app(A, B)
|
|
102
|
+
|
|
103
|
+
# Complex type: (A -> B) -> C
|
|
104
|
+
complex_function = imp.app(imp.app(A, B), C)
|
|
105
|
+
|
|
106
|
+
# Nested type: A -> (B -> C)
|
|
107
|
+
nested_function = imp.app(A, imp.app(B, C))
|
|
108
|
+
|
|
109
|
+
print(simple_function) # Output: A -> B
|
|
110
|
+
print(complex_function) # Output: (A -> B) -> C
|
|
111
|
+
print(nested_function) # Output: A -> B -> C
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
#### Parsing Types from Strings
|
|
115
|
+
|
|
116
|
+
You can also create types by parsing string representations using `type_from_string()`:
|
|
117
|
+
|
|
118
|
+
```python
|
|
119
|
+
import implica as imp
|
|
120
|
+
|
|
121
|
+
# Parse simple variables
|
|
122
|
+
A = imp.type_from_string("A")
|
|
123
|
+
print(A) # Output: A
|
|
124
|
+
|
|
125
|
+
# Parse function types
|
|
126
|
+
func = imp.type_from_string("A -> B")
|
|
127
|
+
print(func) # Output: A -> B
|
|
128
|
+
|
|
129
|
+
# Parse nested types with right-associativity
|
|
130
|
+
nested = imp.type_from_string("A -> B -> C")
|
|
131
|
+
print(nested) # Output: A -> B -> C
|
|
132
|
+
# This is equivalent to: A -> (B -> C)
|
|
133
|
+
|
|
134
|
+
# Parse with explicit parentheses
|
|
135
|
+
left_assoc = imp.type_from_string("(A -> B) -> C")
|
|
136
|
+
print(left_assoc) # Output: (A -> B) -> C
|
|
137
|
+
|
|
138
|
+
# Parse complex nested types
|
|
139
|
+
complex = imp.type_from_string("((A -> B) -> C) -> (D -> E)")
|
|
140
|
+
print(complex) # Output: ((A -> B) -> C) -> D -> E
|
|
141
|
+
|
|
142
|
+
# Multi-character variable names
|
|
143
|
+
person_func = imp.type_from_string("Person -> String")
|
|
144
|
+
print(person_func) # Output: Person -> String
|
|
145
|
+
|
|
146
|
+
# Variables with numbers and underscores
|
|
147
|
+
data_type = imp.type_from_string("data_1 -> result_2")
|
|
148
|
+
print(data_type) # Output: data_1 -> result_2
|
|
149
|
+
|
|
150
|
+
# Unicode characters are supported
|
|
151
|
+
greek = imp.type_from_string("α -> β -> γ")
|
|
152
|
+
print(greek) # Output: α -> β -> γ
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
**Parsing rules:**
|
|
156
|
+
|
|
157
|
+
- The arrow operator `->` is **right-associative**: `A -> B -> C` is parsed as `A -> (B -> C)`
|
|
158
|
+
- Use parentheses to override associativity: `(A -> B) -> C`
|
|
159
|
+
- Variable names can contain letters, numbers, and underscores
|
|
160
|
+
- Whitespace is ignored: `A->B` and `A -> B` are equivalent
|
|
161
|
+
- Unicode characters in variable names are supported
|
|
162
|
+
|
|
163
|
+
**Error handling:**
|
|
164
|
+
|
|
165
|
+
```python
|
|
166
|
+
import implica as imp
|
|
167
|
+
|
|
168
|
+
# Empty string raises ValueError
|
|
169
|
+
try:
|
|
170
|
+
imp.type_from_string("")
|
|
171
|
+
except ValueError as e:
|
|
172
|
+
print(f"Error: {e}") # Error: Type string cannot be empty
|
|
173
|
+
|
|
174
|
+
# Mismatched parentheses
|
|
175
|
+
try:
|
|
176
|
+
imp.type_from_string("(A -> B")
|
|
177
|
+
except ValueError as e:
|
|
178
|
+
print(f"Error: {e}") # Error: Mismatched parentheses
|
|
179
|
+
|
|
180
|
+
# Invalid characters
|
|
181
|
+
try:
|
|
182
|
+
imp.type_from_string("A @ B")
|
|
183
|
+
except ValueError as e:
|
|
184
|
+
print(f"Error: {e}") # Error: Invalid character '@' at position 2
|
|
185
|
+
|
|
186
|
+
# Missing operand
|
|
187
|
+
try:
|
|
188
|
+
imp.type_from_string("A ->")
|
|
189
|
+
except ValueError as e:
|
|
190
|
+
print(f"Error: {e}") # Error: Unexpected end of input
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
**Use cases:**
|
|
194
|
+
|
|
195
|
+
- Parse type expressions from configuration files or user input
|
|
196
|
+
- Create types dynamically from strings
|
|
197
|
+
- Simplify type construction with readable syntax
|
|
198
|
+
- Testing and debugging with human-readable type representations
|
|
199
|
+
|
|
200
|
+
#### Extracting Variables from Types
|
|
201
|
+
|
|
202
|
+
Every type has a `variables` property that returns a list of all variables contained in that type. This is computed recursively and may include duplicate variables if they appear multiple times:
|
|
203
|
+
|
|
204
|
+
```python
|
|
205
|
+
import implica as imp
|
|
206
|
+
|
|
207
|
+
# Simple variable returns itself
|
|
208
|
+
A = imp.var("A")
|
|
209
|
+
print(A.variables) # Output: [Variable(name='A')]
|
|
210
|
+
|
|
211
|
+
# Application returns all variables from both input and output
|
|
212
|
+
B = imp.var("B")
|
|
213
|
+
func = imp.app(A, B)
|
|
214
|
+
print([v.name for v in func.variables]) # Output: ['A', 'B']
|
|
215
|
+
|
|
216
|
+
# Nested types return all variables recursively
|
|
217
|
+
C = imp.var("C")
|
|
218
|
+
nested = imp.app(imp.app(A, B), C) # (A -> B) -> C
|
|
219
|
+
print([v.name for v in nested.variables]) # Output: ['A', 'B', 'C']
|
|
220
|
+
|
|
221
|
+
# Duplicates are included
|
|
222
|
+
same_var = imp.app(A, A) # A -> A
|
|
223
|
+
print([v.name for v in same_var.variables]) # Output: ['A', 'A']
|
|
224
|
+
|
|
225
|
+
# Complex example with duplicates
|
|
226
|
+
complex = imp.app(imp.app(A, B), A) # (A -> B) -> A
|
|
227
|
+
print([v.name for v in complex.variables]) # Output: ['A', 'B', 'A']
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
**Use cases:**
|
|
231
|
+
|
|
232
|
+
- Analyze which variables are used in a complex type expression
|
|
233
|
+
- Count occurrences of specific variables in a type
|
|
234
|
+
- Validate that certain variables are present or absent
|
|
235
|
+
- Generate variable lists for quantification or substitution operations
|
|
236
|
+
|
|
237
|
+
### Combinators
|
|
238
|
+
|
|
239
|
+
Combinators represent transformations between types. The library includes the S and K combinators from combinatory logic.
|
|
240
|
+
|
|
241
|
+
**Important:** All combinators must have an **Application type** (function type), not a simple Variable. This ensures that combinators represent actual transformations from input types to output types. Attempting to create a combinator with a non-Application type will raise a `ValidationError`.
|
|
242
|
+
|
|
243
|
+
```python
|
|
244
|
+
import implica as imp
|
|
245
|
+
|
|
246
|
+
A = imp.var("A")
|
|
247
|
+
B = imp.var("B")
|
|
248
|
+
C = imp.var("C")
|
|
249
|
+
|
|
250
|
+
# S combinator: (A -> B -> C) -> (A -> B) -> A -> C
|
|
251
|
+
s_comb = imp.S(A, B, C)
|
|
252
|
+
print(s_comb) # Output: S: (A -> B -> C) -> (A -> B) -> A -> C
|
|
253
|
+
|
|
254
|
+
# K combinator: A -> B -> A
|
|
255
|
+
k_comb = imp.K(A, B)
|
|
256
|
+
print(k_comb) # Output: K: A -> B -> A
|
|
257
|
+
|
|
258
|
+
# Custom combinator (must have Application type)
|
|
259
|
+
identity = imp.Combinator(name="I", type=imp.app(A, A))
|
|
260
|
+
print(identity) # Output: I: A -> A
|
|
261
|
+
|
|
262
|
+
# This will FAIL - cannot create combinator with Variable type:
|
|
263
|
+
try:
|
|
264
|
+
invalid_comb = imp.Combinator(name="X", type=A) # A is a Variable, not Application!
|
|
265
|
+
except ValidationError as e:
|
|
266
|
+
print(f"Error: {e}") # Error: type must be an Application
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### TypeSchema - Pattern Matching for Types
|
|
270
|
+
|
|
271
|
+
`TypeSchema` allows you to perform pattern matching on types using type variables, similar to unification in type systems. This is useful for:
|
|
272
|
+
|
|
273
|
+
- Checking if a type conforms to a specific pattern
|
|
274
|
+
- Extracting type components from complex types
|
|
275
|
+
- Validating combinator types against their expected signatures
|
|
276
|
+
- Generic type manipulation and analysis
|
|
277
|
+
|
|
278
|
+
#### Pattern Variables vs Exact Matching
|
|
279
|
+
|
|
280
|
+
TypeSchema supports two modes of variable matching:
|
|
281
|
+
|
|
282
|
+
1. **Pattern Variables** (without prefix): Act as wildcards that can match any type
|
|
283
|
+
2. **Exact Match Variables** (with `$` prefix): Must match exactly the named type
|
|
284
|
+
|
|
285
|
+
```python
|
|
286
|
+
import implica as imp
|
|
287
|
+
|
|
288
|
+
# Pattern variables act as wildcards
|
|
289
|
+
A = imp.var("A")
|
|
290
|
+
B = imp.var("B")
|
|
291
|
+
|
|
292
|
+
# Schema with pattern variable - matches ANY types
|
|
293
|
+
schema = imp.TypeSchema(pattern=imp.app(A, B))
|
|
294
|
+
result = schema.match(imp.app(imp.var("X"), imp.var("Y")))
|
|
295
|
+
# ✓ Matches! A binds to X, B binds to Y
|
|
296
|
+
|
|
297
|
+
# Exact match variables - require exact name match
|
|
298
|
+
exact_schema = imp.TypeSchema(pattern=imp.app(imp.var("$A"), imp.var("$B")))
|
|
299
|
+
result1 = exact_schema.match(imp.app(imp.var("A"), imp.var("B")))
|
|
300
|
+
# ✓ Matches! $A matches A exactly, $B matches B exactly
|
|
301
|
+
|
|
302
|
+
result2 = exact_schema.match(imp.app(imp.var("X"), imp.var("Y")))
|
|
303
|
+
# ✗ Fails! $A expects exactly "A", but got "X"
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
**When to use each mode:**
|
|
307
|
+
|
|
308
|
+
- Use **pattern variables** (no prefix) for generic pattern matching and extracting type components
|
|
309
|
+
- Use **exact match variables** (`$` prefix) when you need to match specific named types
|
|
310
|
+
- Mix both modes in the same pattern for flexible matching
|
|
311
|
+
|
|
312
|
+
```python
|
|
313
|
+
# Mixed example: exact match for input, pattern variable for output
|
|
314
|
+
mixed = imp.TypeSchema(pattern=imp.app(imp.var("$Person"), imp.var("Y")))
|
|
315
|
+
|
|
316
|
+
result1 = mixed.match(imp.app(imp.var("Person"), imp.var("String")))
|
|
317
|
+
# ✓ Matches! $Person matches Person, Y binds to String
|
|
318
|
+
|
|
319
|
+
result2 = mixed.match(imp.app(imp.var("Employee"), imp.var("String")))
|
|
320
|
+
# ✗ Fails! $Person expects exactly "Person", not "Employee"
|
|
321
|
+
```
|
|
322
|
+
|
|
323
|
+
#### Basic Usage
|
|
324
|
+
|
|
325
|
+
```python
|
|
326
|
+
import implica as imp
|
|
327
|
+
|
|
328
|
+
# Create a schema with pattern variables A and B
|
|
329
|
+
schema = imp.TypeSchema(pattern=imp.type_from_string("A -> B"))
|
|
330
|
+
|
|
331
|
+
# Try to match a concrete type
|
|
332
|
+
target = imp.type_from_string("X -> Y")
|
|
333
|
+
result = schema.match(target)
|
|
334
|
+
|
|
335
|
+
if result:
|
|
336
|
+
print(f"Match successful!")
|
|
337
|
+
print(f"A = {result.bindings['A']}") # A = X (pattern variable A matched X)
|
|
338
|
+
print(f"B = {result.bindings['B']}") # B = Y (pattern variable B matched Y)
|
|
339
|
+
|
|
340
|
+
# Using exact match
|
|
341
|
+
exact_schema = imp.TypeSchema(pattern=imp.app(imp.var("$Int"), imp.var("$String")))
|
|
342
|
+
result1 = exact_schema.match(imp.app(imp.var("Int"), imp.var("String")))
|
|
343
|
+
print(result1 is not None) # True - exact match!
|
|
344
|
+
|
|
345
|
+
result2 = exact_schema.match(imp.app(imp.var("Float"), imp.var("String")))
|
|
346
|
+
print(result2 is not None) # False - Int doesn't match Float
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
#### K Combinator Pattern Example
|
|
350
|
+
|
|
351
|
+
The K combinator has type `A -> B -> A`, where the same type variable `A` appears twice:
|
|
352
|
+
|
|
353
|
+
```python
|
|
354
|
+
# Create a schema for the K pattern
|
|
355
|
+
k_schema = imp.schema(imp.type_from_string("A -> B -> A"))
|
|
356
|
+
|
|
357
|
+
# This fails - wrong structure (only 2 components instead of 3)
|
|
358
|
+
result1 = k_schema.match(imp.type_from_string("X -> Y"))
|
|
359
|
+
# result1 == None
|
|
360
|
+
|
|
361
|
+
# This succeeds - matches the pattern
|
|
362
|
+
result2 = k_schema.match(imp.type_from_string("(A -> B) -> C -> (A -> B)"))
|
|
363
|
+
# result2.bindings == {'A': (A -> B), 'B': C}
|
|
364
|
+
```
|
|
365
|
+
|
|
366
|
+
#### Consistency Checking
|
|
367
|
+
|
|
368
|
+
Type variables must match consistently throughout the pattern:
|
|
369
|
+
|
|
370
|
+
```python
|
|
371
|
+
# Pattern A -> A requires both sides to be the same
|
|
372
|
+
id_schema = imp.schema(imp.type_from_string("A -> A"))
|
|
373
|
+
|
|
374
|
+
# Succeeds - both sides are X
|
|
375
|
+
result1 = id_schema.match(imp.type_from_string("X -> X"))
|
|
376
|
+
# result1.bindings == {'A': X}
|
|
377
|
+
|
|
378
|
+
# Fails - X and Y are different
|
|
379
|
+
result2 = id_schema.match(imp.type_from_string("X -> Y"))
|
|
380
|
+
# result2 == None
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
#### Variables Bind to Complex Types
|
|
384
|
+
|
|
385
|
+
Type variables can bind to any type, including complex ones:
|
|
386
|
+
|
|
387
|
+
```python
|
|
388
|
+
schema = imp.schema(imp.type_from_string("A -> B"))
|
|
389
|
+
target = imp.type_from_string("(X -> Y) -> (Z -> W)")
|
|
390
|
+
|
|
391
|
+
result = schema.match(target)
|
|
392
|
+
# result.bindings == {'A': (X -> Y), 'B': (Z -> W)}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
#### Validating Combinator Types
|
|
396
|
+
|
|
397
|
+
Check if a combinator matches its expected type pattern:
|
|
398
|
+
|
|
399
|
+
```python
|
|
400
|
+
# Create a K combinator with specific types
|
|
401
|
+
k_combinator = imp.K(imp.var("Int"), imp.var("String"))
|
|
402
|
+
# k_combinator.type == Int -> String -> Int
|
|
403
|
+
|
|
404
|
+
# Check if it matches the general K pattern
|
|
405
|
+
k_pattern = imp.schema(imp.type_from_string("A -> B -> A"))
|
|
406
|
+
result = k_pattern.match(k_combinator.type)
|
|
407
|
+
|
|
408
|
+
if result:
|
|
409
|
+
print(f"K combinator is valid!")
|
|
410
|
+
print(f"A = {result.bindings['A']}") # A = Int
|
|
411
|
+
print(f"B = {result.bindings['B']}") # B = String
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
#### How TypeSchema Works
|
|
415
|
+
|
|
416
|
+
The matching algorithm implements type unification with two modes:
|
|
417
|
+
|
|
418
|
+
1. **Pattern Variable Matching** (no prefix): When the pattern is a type variable without `$` (e.g., `A`, `X`), it acts as a wildcard that binds to any target type
|
|
419
|
+
2. **Exact Matching** (with `$` prefix): When the pattern is a type variable with `$` (e.g., `$A`, `$Person`), it requires the target to be a Variable with exactly that name (without the `$`)
|
|
420
|
+
3. **Consistency Checking**: If a pattern variable is already bound, new bindings must match the existing one
|
|
421
|
+
4. **Structural Matching**: For application types (`->`), both input and output must match recursively
|
|
422
|
+
5. **Failure**: Returns `None` if any part of the match fails
|
|
423
|
+
|
|
424
|
+
**Matching Examples:**
|
|
425
|
+
|
|
426
|
+
```python
|
|
427
|
+
# Pattern variable - matches anything
|
|
428
|
+
pattern_var = imp.TypeSchema(pattern=imp.var("X"))
|
|
429
|
+
pattern_var.match(imp.var("A")) # ✓ X binds to A
|
|
430
|
+
pattern_var.match(imp.var("B")) # ✓ X binds to B
|
|
431
|
+
pattern_var.match(imp.app(imp.var("A"), imp.var("B"))) # ✓ X binds to A -> B
|
|
432
|
+
|
|
433
|
+
# Exact match - requires specific name
|
|
434
|
+
exact_var = imp.TypeSchema(pattern=imp.var("$A"))
|
|
435
|
+
exact_var.match(imp.var("A")) # ✓ $A matches A exactly
|
|
436
|
+
exact_var.match(imp.var("B")) # ✗ $A expects A, got B
|
|
437
|
+
exact_var.match(imp.app(imp.var("A"), imp.var("B"))) # ✗ $A expects Variable, got Application
|
|
438
|
+
|
|
439
|
+
# Structural matching with exact constraints
|
|
440
|
+
schema = imp.TypeSchema(pattern=imp.app(imp.var("$Int"), imp.var("Y")))
|
|
441
|
+
schema.match(imp.app(imp.var("Int"), imp.var("String"))) # ✓ $Int matches Int, Y binds to String
|
|
442
|
+
schema.match(imp.app(imp.var("Float"), imp.var("String"))) # ✗ $Int expects Int, got Float
|
|
443
|
+
```
|
|
444
|
+
|
|
445
|
+
**TypeSchema API:**
|
|
446
|
+
|
|
447
|
+
- `TypeSchema(pattern: BaseType)`: Create a new type schema
|
|
448
|
+
- Use variables without `$` prefix for wildcard pattern matching
|
|
449
|
+
- Use variables with `$` prefix (e.g., `var("$A")`) for exact name matching
|
|
450
|
+
- `schema.match(target: BaseType) -> Optional[TypeMatch]`: Try to match a type against the pattern
|
|
451
|
+
- `schema.get_pattern_variables() -> Set[str]`: Get all variable names in the pattern (includes `$` prefix if present)
|
|
452
|
+
- `TypeMatch`: Result of a successful match
|
|
453
|
+
- `original_type: BaseType`: The type that was matched
|
|
454
|
+
- `bindings: Dict[str, BaseType]`: Variable name → matched type (pattern variables only, not exact matches)
|
|
455
|
+
- `schema(pattern: BaseType) -> TypeSchema`: Helper function to create a schema
|
|
456
|
+
|
|
457
|
+
### Graph Elements
|
|
458
|
+
|
|
459
|
+
#### Nodes
|
|
460
|
+
|
|
461
|
+
Nodes represent types in the graph and can have optional properties (metadata):
|
|
462
|
+
|
|
463
|
+
```python
|
|
464
|
+
import implica as imp
|
|
465
|
+
|
|
466
|
+
A = imp.var("A")
|
|
467
|
+
B = imp.var("B")
|
|
468
|
+
|
|
469
|
+
# Create nodes without properties
|
|
470
|
+
node_a = imp.node(A)
|
|
471
|
+
node_b = imp.node(B)
|
|
472
|
+
node_func = imp.node(imp.app(A, B))
|
|
473
|
+
|
|
474
|
+
print(node_a) # Output: Node(A)
|
|
475
|
+
print(node_func) # Output: Node(A -> B)
|
|
476
|
+
|
|
477
|
+
# Create nodes with properties
|
|
478
|
+
node_with_props = imp.node(A, properties={"color": "red", "weight": 1.5})
|
|
479
|
+
print(node_with_props.properties) # Output: {'color': 'red', 'weight': 1.5}
|
|
480
|
+
|
|
481
|
+
# Properties can be any JSON-serializable data
|
|
482
|
+
node_complex = imp.node(B, properties={
|
|
483
|
+
"metadata": {"author": "user1", "timestamp": 123456},
|
|
484
|
+
"tags": ["important", "test"],
|
|
485
|
+
"config": {"timeout": 30}
|
|
486
|
+
})
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
**Properties Features:**
|
|
490
|
+
|
|
491
|
+
- Optional: Nodes without properties have an empty dict `{}`
|
|
492
|
+
- Immutable: Properties are frozen with the node
|
|
493
|
+
- Flexible: Can store any JSON-serializable data
|
|
494
|
+
- Non-identifying: Two nodes with the same type have the same UID regardless of properties
|
|
495
|
+
|
|
496
|
+
#### Edges
|
|
497
|
+
|
|
498
|
+
Edges represent combinator transformations between types and can have optional properties. The combinator must have an Application type where the input matches the source node and the output matches the destination node.
|
|
499
|
+
|
|
500
|
+
```python
|
|
501
|
+
import implica as imp
|
|
502
|
+
|
|
503
|
+
A = imp.var("A")
|
|
504
|
+
B = imp.var("B")
|
|
505
|
+
|
|
506
|
+
# Create nodes
|
|
507
|
+
n1 = imp.node(A)
|
|
508
|
+
n2 = imp.node(B)
|
|
509
|
+
|
|
510
|
+
# Create a combinator that transforms A to B (must be Application type!)
|
|
511
|
+
comb = imp.Combinator(name="f", type=imp.app(A, B))
|
|
512
|
+
|
|
513
|
+
# Create an edge without properties
|
|
514
|
+
e = imp.Edge(src_node=n1, dst_node=n2, combinator=comb)
|
|
515
|
+
print(e) # Output: A --[f: A -> B]--> B
|
|
516
|
+
|
|
517
|
+
# Create an edge with properties
|
|
518
|
+
e_with_props = imp.Edge(
|
|
519
|
+
src_node=n1,
|
|
520
|
+
dst_node=n2,
|
|
521
|
+
combinator=comb,
|
|
522
|
+
properties={"weight": 2.5, "label": "transition"}
|
|
523
|
+
)
|
|
524
|
+
print(e_with_props.properties) # Output: {'weight': 2.5, 'label': 'transition'}
|
|
525
|
+
|
|
526
|
+
# Alternative: use the edge helper function
|
|
527
|
+
# This automatically creates nodes from the combinator's input/output types
|
|
528
|
+
e2 = imp.edge(comb)
|
|
529
|
+
print(e2) # Output: A --[f: A -> B]--> B
|
|
530
|
+
|
|
531
|
+
# Edge helper with properties
|
|
532
|
+
e3 = imp.edge(comb, properties={"cost": 10})
|
|
533
|
+
print(e3.properties) # Output: {'cost': 10}
|
|
534
|
+
```
|
|
535
|
+
|
|
536
|
+
**Properties Features:**
|
|
537
|
+
|
|
538
|
+
- Optional: Edges without properties have an empty dict `{}`
|
|
539
|
+
- Immutable: Properties are frozen with the edge
|
|
540
|
+
- Flexible: Can store metrics, labels, costs, etc.
|
|
541
|
+
- Non-identifying: Two edges with the same structure have the same UID regardless of properties
|
|
542
|
+
|
|
543
|
+
**Edge Validation:** When creating an edge, the following validations are performed:
|
|
544
|
+
|
|
545
|
+
1. The combinator must have an **Application type** (not a Variable)
|
|
546
|
+
2. The combinator's input type must match the source node's type
|
|
547
|
+
3. The combinator's output type must match the destination node's type
|
|
548
|
+
|
|
549
|
+
If any validation fails, a `ValueError` is raised.
|
|
550
|
+
|
|
551
|
+
### Graph Operations
|
|
552
|
+
|
|
553
|
+
#### Creating and Modifying Graphs
|
|
554
|
+
|
|
555
|
+
```python
|
|
556
|
+
import implica as imp
|
|
557
|
+
|
|
558
|
+
# Create an empty graph
|
|
559
|
+
graph = imp.Graph()
|
|
560
|
+
|
|
561
|
+
# Create types and nodes
|
|
562
|
+
A = imp.var("A")
|
|
563
|
+
B = imp.var("B")
|
|
564
|
+
C = imp.var("C")
|
|
565
|
+
|
|
566
|
+
n_a = imp.node(A)
|
|
567
|
+
n_b = imp.node(B)
|
|
568
|
+
n_c = imp.node(C)
|
|
569
|
+
|
|
570
|
+
# Use transactional connections to modify the graph
|
|
571
|
+
with graph.connect() as conn:
|
|
572
|
+
conn.add_node(n_a)
|
|
573
|
+
conn.add_node(n_b)
|
|
574
|
+
conn.add_node(n_c)
|
|
575
|
+
|
|
576
|
+
print(f"Nodes: {graph.node_count()}") # Output: Nodes: 3
|
|
577
|
+
|
|
578
|
+
# Add edges
|
|
579
|
+
comb_ab = imp.Combinator(name="f", type=imp.app(A, B))
|
|
580
|
+
comb_bc = imp.Combinator(name="g", type=imp.app(B, C))
|
|
581
|
+
|
|
582
|
+
with graph.connect() as conn:
|
|
583
|
+
conn.add_edge(comb_ab)
|
|
584
|
+
conn.add_edge(comb_bc)
|
|
585
|
+
|
|
586
|
+
print(f"Edges: {graph.edge_count()}") # Output: Edges: 2
|
|
587
|
+
```
|
|
588
|
+
|
|
589
|
+
#### Adding Nodes and Edges with Properties
|
|
590
|
+
|
|
591
|
+
You can add properties to nodes and edges directly through the connection API:
|
|
592
|
+
|
|
593
|
+
```python
|
|
594
|
+
import implica as imp
|
|
595
|
+
|
|
596
|
+
graph = imp.Graph()
|
|
597
|
+
A = imp.var("A")
|
|
598
|
+
B = imp.var("B")
|
|
599
|
+
C = imp.var("C")
|
|
600
|
+
|
|
601
|
+
# Add nodes with properties using type
|
|
602
|
+
with graph.connect() as conn:
|
|
603
|
+
conn.add_node(type=A, properties={"color": "red", "weight": 1.5})
|
|
604
|
+
conn.add_node(type=B, properties={"color": "blue", "weight": 2.0})
|
|
605
|
+
conn.add_node(type=C) # Without properties
|
|
606
|
+
|
|
607
|
+
# Add edges with properties
|
|
608
|
+
comb = imp.Combinator(name="f", type=imp.app(A, B))
|
|
609
|
+
|
|
610
|
+
with graph.connect() as conn:
|
|
611
|
+
conn.add_edge(comb, properties={"cost": 10, "label": "primary"})
|
|
612
|
+
|
|
613
|
+
# Verify properties
|
|
614
|
+
node_a = graph.get_node(A.uid)
|
|
615
|
+
print(node_a.properties) # Output: {'color': 'red', 'weight': 1.5}
|
|
616
|
+
|
|
617
|
+
edges = list(graph.edges())
|
|
618
|
+
print(edges[0].properties) # Output: {'cost': 10, 'label': 'primary'}
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
#### Bulk Operations with Properties
|
|
622
|
+
|
|
623
|
+
When adding multiple nodes or edges, you can specify properties in two ways:
|
|
624
|
+
|
|
625
|
+
- **Single dict**: Applied to all elements
|
|
626
|
+
- **List of dicts**: One dict per element (must match count)
|
|
627
|
+
|
|
628
|
+
```python
|
|
629
|
+
import implica as imp
|
|
630
|
+
|
|
631
|
+
graph = imp.Graph()
|
|
632
|
+
A, B, C = imp.var("A"), imp.var("B"), imp.var("C")
|
|
633
|
+
|
|
634
|
+
# Single properties dict for all nodes
|
|
635
|
+
with graph.connect() as conn:
|
|
636
|
+
conn.add_many_nodes(
|
|
637
|
+
types=[A, B, C],
|
|
638
|
+
properties={"category": "important"} # Applied to all
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Individual properties for each node
|
|
642
|
+
with graph.connect() as conn:
|
|
643
|
+
conn.add_many_nodes(
|
|
644
|
+
types=[A, B, C],
|
|
645
|
+
properties=[
|
|
646
|
+
{"color": "red"},
|
|
647
|
+
{"color": "blue"},
|
|
648
|
+
{"color": "green"}
|
|
649
|
+
]
|
|
650
|
+
)
|
|
651
|
+
|
|
652
|
+
# Same with edges
|
|
653
|
+
combs = [
|
|
654
|
+
imp.Combinator("f1", imp.app(A, B)),
|
|
655
|
+
imp.Combinator("f2", imp.app(B, C))
|
|
656
|
+
]
|
|
657
|
+
|
|
658
|
+
with graph.connect() as conn:
|
|
659
|
+
# Single weight for all edges
|
|
660
|
+
conn.add_many_edges(combs, properties={"weight": 1.0})
|
|
661
|
+
|
|
662
|
+
with graph.connect() as conn:
|
|
663
|
+
# Individual properties for each edge
|
|
664
|
+
conn.add_many_edges(
|
|
665
|
+
combs,
|
|
666
|
+
properties=[{"weight": 1.0}, {"weight": 2.0}]
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
# Validation: length must match
|
|
670
|
+
try:
|
|
671
|
+
with graph.connect() as conn:
|
|
672
|
+
conn.add_many_nodes(
|
|
673
|
+
types=[A, B],
|
|
674
|
+
properties=[{"x": 1}] # Only 1 dict for 2 types!
|
|
675
|
+
)
|
|
676
|
+
except ValueError as e:
|
|
677
|
+
print(f"Error: {e}") # Length mismatch error
|
|
678
|
+
```
|
|
679
|
+
|
|
680
|
+
**Properties in Connection Methods:**
|
|
681
|
+
|
|
682
|
+
All connection methods support properties:
|
|
683
|
+
|
|
684
|
+
- `add_node(type=..., properties={...})` or `add_node(node=...)`
|
|
685
|
+
- `add_edge(combinator, properties={...})`
|
|
686
|
+
- `add_many_nodes(types=[...], properties=...)` - Single dict or list of dicts
|
|
687
|
+
- `add_many_edges(combinators, properties=...)` - Single dict or list of dicts
|
|
688
|
+
- `try_add_node(type=..., properties={...})` - Idempotent version
|
|
689
|
+
- `try_add_edge(combinator, properties={...})` - Idempotent version
|
|
690
|
+
- `try_add_many_nodes(types=[...], properties=...)` - Idempotent bulk
|
|
691
|
+
- `try_add_many_edges(combinators, properties=...)` - Idempotent bulk
|
|
692
|
+
|
|
693
|
+
#### Querying Graphs
|
|
694
|
+
|
|
695
|
+
```python
|
|
696
|
+
# Get node by UID
|
|
697
|
+
node_retrieved = graph.get_node(n_a.uid)
|
|
698
|
+
|
|
699
|
+
# Get node by type
|
|
700
|
+
node_by_type = graph.get_node_by_type(A)
|
|
701
|
+
|
|
702
|
+
# Check if node exists
|
|
703
|
+
exists = graph.has_node(n_a.uid)
|
|
704
|
+
|
|
705
|
+
# Get outgoing edges from a node
|
|
706
|
+
outgoing = graph.get_outgoing_edges(n_a.uid)
|
|
707
|
+
print(f"Outgoing from A: {len(outgoing)}")
|
|
708
|
+
|
|
709
|
+
# Get incoming edges to a node
|
|
710
|
+
incoming = graph.get_incoming_edges(n_c.uid)
|
|
711
|
+
print(f"Incoming to C: {len(incoming)}")
|
|
712
|
+
|
|
713
|
+
# Iterate over all nodes
|
|
714
|
+
for n in graph.nodes():
|
|
715
|
+
print(f"Node: {n.type}")
|
|
716
|
+
|
|
717
|
+
# Iterate over all edges
|
|
718
|
+
for e in graph.edges():
|
|
719
|
+
print(f"Edge: {e}")
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
#### Graph Validation
|
|
723
|
+
|
|
724
|
+
```python
|
|
725
|
+
# Validate graph consistency
|
|
726
|
+
try:
|
|
727
|
+
graph.validate()
|
|
728
|
+
print("Graph is valid!")
|
|
729
|
+
except ValueError as e:
|
|
730
|
+
print(f"Graph validation failed: {e}")
|
|
731
|
+
```
|
|
732
|
+
|
|
733
|
+
### Advanced Mutations
|
|
734
|
+
|
|
735
|
+
The library provides several mutation types for flexible graph modifications:
|
|
736
|
+
|
|
737
|
+
#### Bulk Mutations
|
|
738
|
+
|
|
739
|
+
Add multiple nodes or edges in a single atomic operation:
|
|
740
|
+
|
|
741
|
+
```python
|
|
742
|
+
import implica as imp
|
|
743
|
+
|
|
744
|
+
graph = imp.Graph()
|
|
745
|
+
|
|
746
|
+
# Create multiple nodes
|
|
747
|
+
nodes = [imp.node(imp.var(f"T{i}")) for i in range(5)]
|
|
748
|
+
|
|
749
|
+
# Add all nodes atomically - if any fails, all are rolled back
|
|
750
|
+
with graph.connect() as conn:
|
|
751
|
+
conn.add_many_nodes(nodes)
|
|
752
|
+
|
|
753
|
+
# Create multiple edges
|
|
754
|
+
A, B, C = imp.var("A"), imp.var("B"), imp.var("C")
|
|
755
|
+
combinators = [
|
|
756
|
+
imp.Combinator("f1", imp.app(A, B)),
|
|
757
|
+
imp.Combinator("f2", imp.app(B, C)),
|
|
758
|
+
]
|
|
759
|
+
|
|
760
|
+
# Add all edges atomically
|
|
761
|
+
with graph.connect() as conn:
|
|
762
|
+
conn.add_many_edges(combinators)
|
|
763
|
+
```
|
|
764
|
+
|
|
765
|
+
**Benefits:**
|
|
766
|
+
|
|
767
|
+
- All-or-nothing semantics: if any operation fails, all are rolled back
|
|
768
|
+
- More efficient than individual additions
|
|
769
|
+
- Cleaner code for batch operations
|
|
770
|
+
|
|
771
|
+
**Removal Operations:**
|
|
772
|
+
|
|
773
|
+
The same bulk operations are available for removing nodes and edges:
|
|
774
|
+
|
|
775
|
+
```python
|
|
776
|
+
# Remove multiple nodes at once
|
|
777
|
+
node_uids = [n.uid for n in nodes[:3]]
|
|
778
|
+
with graph.connect() as conn:
|
|
779
|
+
conn.remove_many_nodes(node_uids)
|
|
780
|
+
|
|
781
|
+
# Remove multiple edges at once
|
|
782
|
+
edge_uids = [e.uid for e in some_edges]
|
|
783
|
+
with graph.connect() as conn:
|
|
784
|
+
conn.remove_many_edges(edge_uids)
|
|
785
|
+
```
|
|
786
|
+
|
|
787
|
+
#### Idempotent Mutations
|
|
788
|
+
|
|
789
|
+
Use safe mutations that don't fail when items already exist or don't exist:
|
|
790
|
+
|
|
791
|
+
```python
|
|
792
|
+
import implica as imp
|
|
793
|
+
|
|
794
|
+
graph = imp.Graph()
|
|
795
|
+
|
|
796
|
+
A = imp.var("A")
|
|
797
|
+
n_a = imp.node(A)
|
|
798
|
+
|
|
799
|
+
# Regular add would fail on duplicate
|
|
800
|
+
with graph.connect() as conn:
|
|
801
|
+
conn.add_node(n_a)
|
|
802
|
+
|
|
803
|
+
# try_add_node won't fail if node exists
|
|
804
|
+
with graph.connect() as conn:
|
|
805
|
+
conn.try_add_node(n_a) # No error, even though n_a exists
|
|
806
|
+
|
|
807
|
+
# Same with edges
|
|
808
|
+
B = imp.var("B")
|
|
809
|
+
with graph.connect() as conn:
|
|
810
|
+
conn.try_add_node(imp.node(B))
|
|
811
|
+
|
|
812
|
+
comb = imp.Combinator("f", imp.app(A, B))
|
|
813
|
+
with graph.connect() as conn:
|
|
814
|
+
conn.try_add_edge(comb) # Adds the edge
|
|
815
|
+
|
|
816
|
+
with graph.connect() as conn:
|
|
817
|
+
conn.try_add_edge(comb) # No error, edge already exists
|
|
818
|
+
|
|
819
|
+
# Safe removal operations
|
|
820
|
+
with graph.connect() as conn:
|
|
821
|
+
conn.try_remove_node(n_a.uid) # Removes the node
|
|
822
|
+
|
|
823
|
+
with graph.connect() as conn:
|
|
824
|
+
conn.try_remove_node(n_a.uid) # No error, node doesn't exist anymore
|
|
825
|
+
|
|
826
|
+
# Same with edges
|
|
827
|
+
with graph.connect() as conn:
|
|
828
|
+
conn.try_remove_edge("some_edge_uid") # No error even if edge doesn't exist
|
|
829
|
+
```
|
|
830
|
+
|
|
831
|
+
**Use Cases:**
|
|
832
|
+
|
|
833
|
+
- Building graphs from multiple sources where duplicates may occur
|
|
834
|
+
- Idempotent initialization routines
|
|
835
|
+
- Avoiding explicit existence checks before adding or removing elements
|
|
836
|
+
- Incremental graph construction without duplicate errors
|
|
837
|
+
- Cleanup operations that should be safe to run multiple times
|
|
838
|
+
|
|
839
|
+
## Usage Examples
|
|
840
|
+
|
|
841
|
+
### Example 1: Simple Type Chain
|
|
842
|
+
|
|
843
|
+
Build a chain of type transformations:
|
|
844
|
+
|
|
845
|
+
```python
|
|
846
|
+
import implica as imp
|
|
847
|
+
|
|
848
|
+
# Create graph
|
|
849
|
+
graph = imp.Graph()
|
|
850
|
+
|
|
851
|
+
# Define types
|
|
852
|
+
types = [imp.var(name) for name in ["A", "B", "C", "D"]]
|
|
853
|
+
nodes = [imp.node(t) for t in types]
|
|
854
|
+
|
|
855
|
+
# Add nodes
|
|
856
|
+
with graph.connect() as conn:
|
|
857
|
+
for n in nodes:
|
|
858
|
+
conn.add_node(n)
|
|
859
|
+
|
|
860
|
+
# Create transformation chain: A -> B -> C -> D
|
|
861
|
+
with graph.connect() as conn:
|
|
862
|
+
for i in range(len(nodes) - 1):
|
|
863
|
+
comb = imp.Combinator(
|
|
864
|
+
name=f"f{i}",
|
|
865
|
+
type=imp.app(types[i], types[i + 1])
|
|
866
|
+
)
|
|
867
|
+
conn.add_edge(imp.edge(nodes[i], nodes[i + 1], comb))
|
|
868
|
+
|
|
869
|
+
print(f"Created chain with {graph.node_count()} nodes and {graph.edge_count()} edges")
|
|
870
|
+
|
|
871
|
+
# Query the chain
|
|
872
|
+
a_node = graph.get_node_by_type(imp.var("A"))
|
|
873
|
+
outgoing = graph.get_outgoing_edges(a_node.uid)
|
|
874
|
+
print(f"A has {len(outgoing)} outgoing edge(s)")
|
|
875
|
+
```
|
|
876
|
+
|
|
877
|
+
### Example 2: Complex Type Structure
|
|
878
|
+
|
|
879
|
+
Work with higher-order functions:
|
|
880
|
+
|
|
881
|
+
```python
|
|
882
|
+
import implica as imp
|
|
883
|
+
|
|
884
|
+
# Create graph
|
|
885
|
+
graph = imp.Graph()
|
|
886
|
+
|
|
887
|
+
# Define type variables
|
|
888
|
+
A = imp.var("A")
|
|
889
|
+
B = imp.var("B")
|
|
890
|
+
C = imp.var("C")
|
|
891
|
+
|
|
892
|
+
# Create complex types
|
|
893
|
+
# Type 1: A
|
|
894
|
+
# Type 2: B
|
|
895
|
+
# Type 3: A -> B
|
|
896
|
+
# Type 4: (A -> B) -> C
|
|
897
|
+
t1 = A
|
|
898
|
+
t2 = B
|
|
899
|
+
t3 = imp.app(A, B)
|
|
900
|
+
t4 = imp.app(t3, C)
|
|
901
|
+
|
|
902
|
+
nodes = [imp.node(t) for t in [t1, t2, t3, t4]]
|
|
903
|
+
|
|
904
|
+
# Add nodes
|
|
905
|
+
with graph.connect() as conn:
|
|
906
|
+
for n in nodes:
|
|
907
|
+
conn.add_node(n)
|
|
908
|
+
|
|
909
|
+
# Add S and K combinators as edges
|
|
910
|
+
s_comb = imp.S(A, B, C)
|
|
911
|
+
k_comb = imp.K(A, B)
|
|
912
|
+
|
|
913
|
+
# Note: You would connect these based on your specific logic model
|
|
914
|
+
print(f"Created graph with {graph.node_count()} nodes")
|
|
915
|
+
print(f"S combinator type: {s_comb.type}")
|
|
916
|
+
print(f"K combinator type: {k_comb.type}")
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
### Example 3: Transactional Rollback
|
|
920
|
+
|
|
921
|
+
Demonstrate automatic rollback on failure:
|
|
922
|
+
|
|
923
|
+
```python
|
|
924
|
+
import implica as imp
|
|
925
|
+
|
|
926
|
+
graph = imp.Graph()
|
|
927
|
+
|
|
928
|
+
A = imp.var("A")
|
|
929
|
+
B = imp.var("B")
|
|
930
|
+
n_a = imp.node(A)
|
|
931
|
+
n_b = imp.node(B)
|
|
932
|
+
|
|
933
|
+
# Add initial nodes
|
|
934
|
+
with graph.connect() as conn:
|
|
935
|
+
conn.add_node(n_a)
|
|
936
|
+
conn.add_node(n_b)
|
|
937
|
+
|
|
938
|
+
print(f"Initial nodes: {graph.node_count()}") # Output: 2
|
|
939
|
+
|
|
940
|
+
# Try to add invalid edge (will fail and rollback)
|
|
941
|
+
try:
|
|
942
|
+
with graph.connect() as conn:
|
|
943
|
+
# This will succeed
|
|
944
|
+
C = imp.var("C")
|
|
945
|
+
n_c = imp.node(C)
|
|
946
|
+
conn.add_node(n_c)
|
|
947
|
+
|
|
948
|
+
# This will fail (trying to add duplicate node)
|
|
949
|
+
conn.add_node(n_a) # Already exists!
|
|
950
|
+
except RuntimeError as e:
|
|
951
|
+
print(f"Transaction failed: {e}")
|
|
952
|
+
|
|
953
|
+
# Graph remains unchanged
|
|
954
|
+
print(f"Nodes after failed transaction: {graph.node_count()}") # Output: 2
|
|
955
|
+
print(f"Has C: {graph.get_node_by_type(imp.var('C')) is not None}") # Output: False
|
|
956
|
+
```
|
|
957
|
+
|
|
958
|
+
### Example 4: Building a Proof Tree
|
|
959
|
+
|
|
960
|
+
Create a structure representing logical derivations:
|
|
961
|
+
|
|
962
|
+
```python
|
|
963
|
+
import implica as imp
|
|
964
|
+
|
|
965
|
+
# Create a graph representing a proof
|
|
966
|
+
graph = imp.Graph()
|
|
967
|
+
|
|
968
|
+
# Axioms (base types)
|
|
969
|
+
P = imp.var("P")
|
|
970
|
+
Q = imp.var("Q")
|
|
971
|
+
R = imp.var("R")
|
|
972
|
+
|
|
973
|
+
# Derived types (implications)
|
|
974
|
+
PQ = imp.app(P, Q) # P -> Q
|
|
975
|
+
QR = imp.app(Q, R) # Q -> R
|
|
976
|
+
PR = imp.app(P, R) # P -> R (conclusion)
|
|
977
|
+
|
|
978
|
+
# Create nodes for each type in the proof
|
|
979
|
+
nodes_dict = {
|
|
980
|
+
"P": imp.node(P),
|
|
981
|
+
"Q": imp.node(Q),
|
|
982
|
+
"R": imp.node(R),
|
|
983
|
+
"P->Q": imp.node(PQ),
|
|
984
|
+
"Q->R": imp.node(QR),
|
|
985
|
+
"P->R": imp.node(PR),
|
|
986
|
+
}
|
|
987
|
+
|
|
988
|
+
# Add all nodes
|
|
989
|
+
with graph.connect() as conn:
|
|
990
|
+
for n in nodes_dict.values():
|
|
991
|
+
conn.add_node(n)
|
|
992
|
+
|
|
993
|
+
# Add inference rules as edges
|
|
994
|
+
with graph.connect() as conn:
|
|
995
|
+
# Modus Ponens: P, P->Q ⊢ Q
|
|
996
|
+
mp1 = imp.Combinator(name="MP1", type=imp.app(P, Q))
|
|
997
|
+
conn.add_edge(imp.edge(nodes_dict["P"], nodes_dict["Q"], mp1))
|
|
998
|
+
|
|
999
|
+
# Modus Ponens: Q, Q->R ⊢ R
|
|
1000
|
+
mp2 = imp.Combinator(name="MP2", type=imp.app(Q, R))
|
|
1001
|
+
conn.add_edge(imp.edge(nodes_dict["Q"], nodes_dict["R"], mp2))
|
|
1002
|
+
|
|
1003
|
+
# Composition: P->Q, Q->R ⊢ P->R
|
|
1004
|
+
comp = imp.Combinator(name="Comp", type=imp.app(P, R))
|
|
1005
|
+
conn.add_edge(imp.edge(nodes_dict["P"], nodes_dict["R"], comp))
|
|
1006
|
+
|
|
1007
|
+
print(f"Proof tree has {graph.node_count()} types")
|
|
1008
|
+
print(f"Proof tree has {graph.edge_count()} inference rules")
|
|
1009
|
+
|
|
1010
|
+
# Validate the proof structure
|
|
1011
|
+
graph.validate()
|
|
1012
|
+
print("Proof structure is valid!")
|
|
1013
|
+
```
|
|
1014
|
+
|
|
1015
|
+
### Example 5: Exploring Graph Structure
|
|
1016
|
+
|
|
1017
|
+
Navigate and analyze the graph:
|
|
1018
|
+
|
|
1019
|
+
```python
|
|
1020
|
+
import implica as imp
|
|
1021
|
+
|
|
1022
|
+
# Build a diamond-shaped graph
|
|
1023
|
+
# A
|
|
1024
|
+
# / \
|
|
1025
|
+
# B C
|
|
1026
|
+
# \ /
|
|
1027
|
+
# D
|
|
1028
|
+
|
|
1029
|
+
graph = imp.Graph()
|
|
1030
|
+
|
|
1031
|
+
A, B, C, D = imp.var("A"), imp.var("B"), imp.var("C"), imp.var("D")
|
|
1032
|
+
n_a, n_b, n_c, n_d = imp.node(A), imp.node(B), imp.node(C), imp.node(D)
|
|
1033
|
+
|
|
1034
|
+
with graph.connect() as conn:
|
|
1035
|
+
conn.add_node(n_a)
|
|
1036
|
+
conn.add_node(n_b)
|
|
1037
|
+
conn.add_node(n_c)
|
|
1038
|
+
conn.add_node(n_d)
|
|
1039
|
+
|
|
1040
|
+
# A -> B, A -> C
|
|
1041
|
+
conn.add_edge(imp.edge(n_a, n_b, imp.Combinator("f1", imp.app(A, B))))
|
|
1042
|
+
conn.add_edge(imp.edge(n_a, n_c, imp.Combinator("f2", imp.app(A, C))))
|
|
1043
|
+
|
|
1044
|
+
# B -> D, C -> D
|
|
1045
|
+
conn.add_edge(imp.edge(n_b, n_d, imp.Combinator("g1", imp.app(B, D))))
|
|
1046
|
+
conn.add_edge(imp.edge(n_c, n_d, imp.Combinator("g2", imp.app(C, D))))
|
|
1047
|
+
|
|
1048
|
+
# Analyze the structure
|
|
1049
|
+
print("=== Graph Analysis ===")
|
|
1050
|
+
print(f"Total nodes: {graph.node_count()}")
|
|
1051
|
+
print(f"Total edges: {graph.edge_count()}")
|
|
1052
|
+
|
|
1053
|
+
# Find all paths from A
|
|
1054
|
+
print("\nFrom A:")
|
|
1055
|
+
for e in graph.get_outgoing_edges(n_a.uid):
|
|
1056
|
+
print(f" -> {e.dst_node.type} via {e.combinator.name}")
|
|
1057
|
+
|
|
1058
|
+
# Find all paths to D
|
|
1059
|
+
print("\nTo D:")
|
|
1060
|
+
for e in graph.get_incoming_edges(n_d.uid):
|
|
1061
|
+
print(f" <- {e.src_node.type} via {e.combinator.name}")
|
|
1062
|
+
|
|
1063
|
+
# Check connectivity
|
|
1064
|
+
print("\nNode connectivity:")
|
|
1065
|
+
for n in graph.nodes():
|
|
1066
|
+
incoming = len(graph.get_incoming_edges(n.uid))
|
|
1067
|
+
outgoing = len(graph.get_outgoing_edges(n.uid))
|
|
1068
|
+
print(f" {n.type}: {incoming} in, {outgoing} out")
|
|
1069
|
+
```
|
|
1070
|
+
|
|
1071
|
+
### Example 6: Bulk Operations
|
|
1072
|
+
|
|
1073
|
+
Add multiple nodes and edges efficiently:
|
|
1074
|
+
|
|
1075
|
+
```python
|
|
1076
|
+
import implica as imp
|
|
1077
|
+
|
|
1078
|
+
graph = imp.Graph()
|
|
1079
|
+
|
|
1080
|
+
# Create many type variables at once
|
|
1081
|
+
type_vars = [imp.var(f"T{i}") for i in range(10)]
|
|
1082
|
+
nodes = [imp.node(t) for t in type_vars]
|
|
1083
|
+
|
|
1084
|
+
# Add all nodes in a single transaction
|
|
1085
|
+
with graph.connect() as conn:
|
|
1086
|
+
conn.add_many_nodes(nodes)
|
|
1087
|
+
|
|
1088
|
+
print(f"Added {graph.node_count()} nodes in one operation")
|
|
1089
|
+
|
|
1090
|
+
# Create a chain of transformations
|
|
1091
|
+
combinators = []
|
|
1092
|
+
for i in range(len(type_vars) - 1):
|
|
1093
|
+
comb = imp.Combinator(
|
|
1094
|
+
name=f"f{i}",
|
|
1095
|
+
type=imp.app(type_vars[i], type_vars[i + 1])
|
|
1096
|
+
)
|
|
1097
|
+
combinators.append(comb)
|
|
1098
|
+
|
|
1099
|
+
# Add all edges in a single transaction
|
|
1100
|
+
with graph.connect() as conn:
|
|
1101
|
+
conn.add_many_edges(combinators)
|
|
1102
|
+
|
|
1103
|
+
print(f"Added {graph.edge_count()} edges in one operation")
|
|
1104
|
+
|
|
1105
|
+
# If any node or edge fails, all are rolled back atomically
|
|
1106
|
+
try:
|
|
1107
|
+
with graph.connect() as conn:
|
|
1108
|
+
# This will fail because nodes[0] already exists
|
|
1109
|
+
conn.add_many_nodes([nodes[0], imp.node(imp.var("NEW"))])
|
|
1110
|
+
except RuntimeError:
|
|
1111
|
+
print("Transaction rolled back - no new nodes added")
|
|
1112
|
+
```
|
|
1113
|
+
|
|
1114
|
+
### Example 7: Idempotent Operations
|
|
1115
|
+
|
|
1116
|
+
Use safe mutations that don't fail on duplicates:
|
|
1117
|
+
|
|
1118
|
+
```python
|
|
1119
|
+
import implica as imp
|
|
1120
|
+
|
|
1121
|
+
graph = imp.Graph()
|
|
1122
|
+
|
|
1123
|
+
A = imp.var("A")
|
|
1124
|
+
B = imp.var("B")
|
|
1125
|
+
n_a = imp.node(A)
|
|
1126
|
+
n_b = imp.node(B)
|
|
1127
|
+
|
|
1128
|
+
# Add nodes normally
|
|
1129
|
+
with graph.connect() as conn:
|
|
1130
|
+
conn.add_node(n_a)
|
|
1131
|
+
conn.add_node(n_b)
|
|
1132
|
+
|
|
1133
|
+
print(f"Initial nodes: {graph.node_count()}") # Output: 2
|
|
1134
|
+
|
|
1135
|
+
# Try to add nodes again - won't fail!
|
|
1136
|
+
with graph.connect() as conn:
|
|
1137
|
+
conn.try_add_node(n_a) # Already exists, but no error
|
|
1138
|
+
conn.try_add_node(n_b) # Already exists, but no error
|
|
1139
|
+
conn.try_add_node(imp.node(imp.var("C"))) # New node, will be added
|
|
1140
|
+
|
|
1141
|
+
print(f"Nodes after try_add: {graph.node_count()}") # Output: 3
|
|
1142
|
+
|
|
1143
|
+
# Same with edges
|
|
1144
|
+
comb_ab = imp.Combinator("f", imp.app(A, B))
|
|
1145
|
+
|
|
1146
|
+
with graph.connect() as conn:
|
|
1147
|
+
conn.try_add_edge(comb_ab) # Will be added
|
|
1148
|
+
|
|
1149
|
+
print(f"Edges: {graph.edge_count()}") # Output: 1
|
|
1150
|
+
|
|
1151
|
+
with graph.connect() as conn:
|
|
1152
|
+
conn.try_add_edge(comb_ab) # Already exists, but no error
|
|
1153
|
+
|
|
1154
|
+
print(f"Edges after try_add: {graph.edge_count()}") # Output: 1
|
|
1155
|
+
|
|
1156
|
+
# Useful for idempotent operations and avoiding duplicate checks
|
|
1157
|
+
def ensure_basic_types(graph, type_names):
|
|
1158
|
+
"""Ensure all basic types exist in the graph."""
|
|
1159
|
+
with graph.connect() as conn:
|
|
1160
|
+
for name in type_names:
|
|
1161
|
+
conn.try_add_node(imp.node(imp.var(name)))
|
|
1162
|
+
|
|
1163
|
+
# Can call this multiple times safely
|
|
1164
|
+
ensure_basic_types(graph, ["A", "B", "C", "D"])
|
|
1165
|
+
ensure_basic_types(graph, ["C", "D", "E", "F"]) # C and D won't cause errors
|
|
1166
|
+
|
|
1167
|
+
print(f"Final nodes: {graph.node_count()}") # Output: 6 (A, B, C, D, E, F)
|
|
1168
|
+
```
|
|
1169
|
+
|
|
1170
|
+
### Example 8: Bulk Removal Operations
|
|
1171
|
+
|
|
1172
|
+
Remove multiple elements efficiently:
|
|
1173
|
+
|
|
1174
|
+
```python
|
|
1175
|
+
import implica as imp
|
|
1176
|
+
|
|
1177
|
+
graph = imp.Graph()
|
|
1178
|
+
|
|
1179
|
+
# Build a graph with multiple nodes and edges
|
|
1180
|
+
types = [imp.var(f"T{i}") for i in range(10)]
|
|
1181
|
+
nodes = [imp.node(t) for t in types]
|
|
1182
|
+
|
|
1183
|
+
with graph.connect() as conn:
|
|
1184
|
+
conn.add_many_nodes(nodes)
|
|
1185
|
+
|
|
1186
|
+
# Create edges
|
|
1187
|
+
combinators = []
|
|
1188
|
+
for i in range(len(types) - 1):
|
|
1189
|
+
comb = imp.Combinator(f"f{i}", imp.app(types[i], types[i + 1]))
|
|
1190
|
+
combinators.append(comb)
|
|
1191
|
+
|
|
1192
|
+
with graph.connect() as conn:
|
|
1193
|
+
conn.add_many_edges(combinators)
|
|
1194
|
+
|
|
1195
|
+
print(f"Initial: {graph.node_count()} nodes, {graph.edge_count()} edges")
|
|
1196
|
+
# Output: Initial: 10 nodes, 9 edges
|
|
1197
|
+
|
|
1198
|
+
# Remove multiple edges at once
|
|
1199
|
+
edge_uids = [graph.get_outgoing_edges(nodes[i].uid)[0].uid for i in range(3)]
|
|
1200
|
+
with graph.connect() as conn:
|
|
1201
|
+
conn.remove_many_edges(edge_uids)
|
|
1202
|
+
|
|
1203
|
+
print(f"After edge removal: {graph.edge_count()} edges")
|
|
1204
|
+
# Output: After edge removal: 6 edges
|
|
1205
|
+
|
|
1206
|
+
# Remove multiple nodes at once (and their connected edges)
|
|
1207
|
+
node_uids = [nodes[i].uid for i in range(5)]
|
|
1208
|
+
with graph.connect() as conn:
|
|
1209
|
+
conn.remove_many_nodes(node_uids)
|
|
1210
|
+
|
|
1211
|
+
print(f"After node removal: {graph.node_count()} nodes, {graph.edge_count()} edges")
|
|
1212
|
+
# Output: After node removal: 5 nodes, 0 edges (edges were connected to removed nodes)
|
|
1213
|
+
|
|
1214
|
+
# Safe removal with try_remove (won't fail if already removed)
|
|
1215
|
+
with graph.connect() as conn:
|
|
1216
|
+
conn.try_remove_node(nodes[0].uid) # Already removed, no error
|
|
1217
|
+
conn.try_remove_edge("nonexistent_uid") # Doesn't exist, no error
|
|
1218
|
+
|
|
1219
|
+
print(f"Final: {graph.node_count()} nodes") # Output: Final: 5 nodes
|
|
1220
|
+
```
|
|
1221
|
+
|
|
1222
|
+
### Example 9: Safe Cleanup Operations
|
|
1223
|
+
|
|
1224
|
+
Use idempotent removal for cleanup tasks:
|
|
1225
|
+
|
|
1226
|
+
```python
|
|
1227
|
+
import implica as imp
|
|
1228
|
+
|
|
1229
|
+
def cleanup_temporary_nodes(graph, temp_node_uids):
|
|
1230
|
+
"""
|
|
1231
|
+
Remove temporary nodes if they exist.
|
|
1232
|
+
This function is safe to call multiple times.
|
|
1233
|
+
"""
|
|
1234
|
+
with graph.connect() as conn:
|
|
1235
|
+
for uid in temp_node_uids:
|
|
1236
|
+
conn.try_remove_node(uid)
|
|
1237
|
+
|
|
1238
|
+
graph = imp.Graph()
|
|
1239
|
+
|
|
1240
|
+
# Add some nodes
|
|
1241
|
+
A, B, C = imp.var("A"), imp.var("B"), imp.var("C")
|
|
1242
|
+
n_a, n_b, n_c = imp.node(A), imp.node(B), imp.node(C)
|
|
1243
|
+
|
|
1244
|
+
with graph.connect() as conn:
|
|
1245
|
+
conn.add_many_nodes([n_a, n_b, n_c])
|
|
1246
|
+
|
|
1247
|
+
print(f"Initial nodes: {graph.node_count()}") # Output: 3
|
|
1248
|
+
|
|
1249
|
+
# Clean up - safe to call multiple times
|
|
1250
|
+
temp_uids = [n_a.uid, n_b.uid]
|
|
1251
|
+
cleanup_temporary_nodes(graph, temp_uids)
|
|
1252
|
+
print(f"After cleanup: {graph.node_count()}") # Output: 1
|
|
1253
|
+
|
|
1254
|
+
# Call again - won't fail even though nodes are already removed
|
|
1255
|
+
cleanup_temporary_nodes(graph, temp_uids)
|
|
1256
|
+
print(f"After second cleanup: {graph.node_count()}") # Output: 1
|
|
1257
|
+
|
|
1258
|
+
# Combine try_remove with try_add for flexible graph updates
|
|
1259
|
+
def ensure_graph_state(graph, required_nodes, forbidden_nodes):
|
|
1260
|
+
"""
|
|
1261
|
+
Ensure graph has required nodes and doesn't have forbidden ones.
|
|
1262
|
+
"""
|
|
1263
|
+
with graph.connect() as conn:
|
|
1264
|
+
# Add required nodes (idempotent)
|
|
1265
|
+
for n in required_nodes:
|
|
1266
|
+
conn.try_add_node(n)
|
|
1267
|
+
|
|
1268
|
+
# Remove forbidden nodes (idempotent)
|
|
1269
|
+
for uid in forbidden_nodes:
|
|
1270
|
+
conn.try_remove_node(uid)
|
|
1271
|
+
|
|
1272
|
+
# Can call this function repeatedly to maintain desired state
|
|
1273
|
+
ensure_graph_state(
|
|
1274
|
+
graph,
|
|
1275
|
+
required_nodes=[imp.node(imp.var("X")), imp.node(imp.var("Y"))],
|
|
1276
|
+
forbidden_nodes=[n_c.uid]
|
|
1277
|
+
)
|
|
1278
|
+
|
|
1279
|
+
print(f"Final state: {graph.node_count()} nodes") # Output: 2 (X and Y)
|
|
1280
|
+
```
|
|
1281
|
+
|
|
1282
|
+
### Example 10: Using Schema-Based API for Pattern Matching
|
|
1283
|
+
|
|
1284
|
+
Use TypeSchemas to query and manipulate the graph based on type patterns:
|
|
1285
|
+
|
|
1286
|
+
```python
|
|
1287
|
+
import implica as imp
|
|
1288
|
+
|
|
1289
|
+
# Create a graph with various types
|
|
1290
|
+
graph = imp.Graph()
|
|
1291
|
+
|
|
1292
|
+
# Add variable and application nodes
|
|
1293
|
+
A = imp.var("A")
|
|
1294
|
+
B = imp.var("B")
|
|
1295
|
+
C = imp.var("C")
|
|
1296
|
+
AB = imp.app(A, B)
|
|
1297
|
+
BC = imp.app(B, C)
|
|
1298
|
+
ABC = imp.app(AB, C)
|
|
1299
|
+
|
|
1300
|
+
with graph.connect() as conn:
|
|
1301
|
+
conn.add_many_nodes(
|
|
1302
|
+
types=[A, B, C, AB, BC, ABC],
|
|
1303
|
+
properties=[
|
|
1304
|
+
{"kind": "variable", "priority": 1},
|
|
1305
|
+
{"kind": "variable", "priority": 1},
|
|
1306
|
+
{"kind": "variable", "priority": 1},
|
|
1307
|
+
{"kind": "application", "priority": 2},
|
|
1308
|
+
{"kind": "application", "priority": 2},
|
|
1309
|
+
{"kind": "application", "priority": 3},
|
|
1310
|
+
]
|
|
1311
|
+
)
|
|
1312
|
+
|
|
1313
|
+
# Create a schema to match application types (X -> Y)
|
|
1314
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1315
|
+
|
|
1316
|
+
# Query: Get all application nodes with their bindings
|
|
1317
|
+
matches = graph.get_nodes_matching_schema(app_schema)
|
|
1318
|
+
print(f"\nApplication nodes found: {len(matches)}")
|
|
1319
|
+
for node, match in matches:
|
|
1320
|
+
print(f" {node.type}: X={match.bindings['X']}, Y={match.bindings['Y']}")
|
|
1321
|
+
|
|
1322
|
+
# Output:
|
|
1323
|
+
# Application nodes found: 3
|
|
1324
|
+
# A -> B: X=A, Y=B
|
|
1325
|
+
# B -> C: X=B, Y=C
|
|
1326
|
+
# (A -> B) -> C: X=(A -> B), Y=C
|
|
1327
|
+
|
|
1328
|
+
# Filter: Get only high-priority application nodes
|
|
1329
|
+
high_priority_apps = graph.filter_nodes_by_schema(
|
|
1330
|
+
app_schema,
|
|
1331
|
+
properties={"priority": 3}
|
|
1332
|
+
)
|
|
1333
|
+
print(f"\nHigh priority applications: {len(high_priority_apps)}")
|
|
1334
|
+
# Output: High priority applications: 1 (the nested one)
|
|
1335
|
+
|
|
1336
|
+
# Count application nodes
|
|
1337
|
+
app_count = graph.count_nodes_matching_schema(app_schema)
|
|
1338
|
+
print(f"Total applications: {app_count}") # Output: 3
|
|
1339
|
+
|
|
1340
|
+
# Mutation: Update all application nodes
|
|
1341
|
+
with graph.connect() as conn:
|
|
1342
|
+
conn.update_nodes_matching_schema(
|
|
1343
|
+
app_schema,
|
|
1344
|
+
{"processed": True, "timestamp": 123456}
|
|
1345
|
+
)
|
|
1346
|
+
|
|
1347
|
+
# Verify updates
|
|
1348
|
+
for node in graph.filter_nodes_by_schema(app_schema):
|
|
1349
|
+
print(f"{node.type}: processed={node.properties.get('processed')}")
|
|
1350
|
+
|
|
1351
|
+
# Mutation: Add only application types from a list
|
|
1352
|
+
new_types = [
|
|
1353
|
+
imp.var("D"),
|
|
1354
|
+
imp.app(imp.var("D"), imp.var("E")),
|
|
1355
|
+
imp.var("F"),
|
|
1356
|
+
]
|
|
1357
|
+
|
|
1358
|
+
with graph.connect() as conn:
|
|
1359
|
+
# Only the application will be added
|
|
1360
|
+
conn.add_nodes_for_schema(app_schema, new_types, properties={"new": True})
|
|
1361
|
+
|
|
1362
|
+
print(f"Nodes after selective add: {graph.node_count()}")
|
|
1363
|
+
# Output: 7 (6 original + 1 new application)
|
|
1364
|
+
|
|
1365
|
+
# Mutation: Remove temporary application nodes
|
|
1366
|
+
with graph.connect() as conn:
|
|
1367
|
+
conn.try_remove_nodes_matching_schema(
|
|
1368
|
+
app_schema,
|
|
1369
|
+
properties={"new": True}
|
|
1370
|
+
)
|
|
1371
|
+
|
|
1372
|
+
print(f"Nodes after cleanup: {graph.node_count()}")
|
|
1373
|
+
# Output: 6 (back to original)
|
|
1374
|
+
|
|
1375
|
+
# Edge filtering with schemas
|
|
1376
|
+
D = imp.var("D")
|
|
1377
|
+
E = imp.var("E")
|
|
1378
|
+
|
|
1379
|
+
with graph.connect() as conn:
|
|
1380
|
+
conn.add_node(type=D)
|
|
1381
|
+
conn.add_node(type=E)
|
|
1382
|
+
conn.add_edge(imp.Combinator("f1", imp.app(A, B)))
|
|
1383
|
+
conn.add_edge(imp.Combinator("f2", imp.app(AB, C)))
|
|
1384
|
+
conn.add_edge(imp.Combinator("f3", imp.app(D, E)))
|
|
1385
|
+
|
|
1386
|
+
# Find edges where source is an application
|
|
1387
|
+
edges_from_apps = graph.filter_edges_by_schema(src_schema=app_schema)
|
|
1388
|
+
print(f"\nEdges from applications: {len(edges_from_apps)}")
|
|
1389
|
+
# Output: 1 (the edge from AB to C)
|
|
1390
|
+
|
|
1391
|
+
# Find edges where destination is a variable
|
|
1392
|
+
var_schema = imp.TypeSchema(pattern=imp.var("Z"))
|
|
1393
|
+
edges_to_vars = graph.filter_edges_by_schema(dst_schema=var_schema)
|
|
1394
|
+
print(f"Edges to variables: {len(edges_to_vars)}")
|
|
1395
|
+
# Output: 2 (A->B and D->E both go to variables)
|
|
1396
|
+
|
|
1397
|
+
# Complex pattern: nested applications like (X -> Y) -> Z
|
|
1398
|
+
nested_schema = imp.TypeSchema(
|
|
1399
|
+
pattern=imp.app(imp.app(imp.var("X"), imp.var("Y")), imp.var("Z"))
|
|
1400
|
+
)
|
|
1401
|
+
nested_matches = graph.get_nodes_matching_schema(nested_schema)
|
|
1402
|
+
print(f"\nNested applications: {len(nested_matches)}")
|
|
1403
|
+
for node, match in nested_matches:
|
|
1404
|
+
print(f" {node.type}")
|
|
1405
|
+
print(f" X={match.bindings['X']}, Y={match.bindings['Y']}, Z={match.bindings['Z']}")
|
|
1406
|
+
|
|
1407
|
+
# Output:
|
|
1408
|
+
# Nested applications: 1
|
|
1409
|
+
# (A -> B) -> C
|
|
1410
|
+
# X=A, Y=B, Z=C
|
|
1411
|
+
```
|
|
1412
|
+
|
|
1413
|
+
**Schema-Based API Benefits:**
|
|
1414
|
+
|
|
1415
|
+
- **Pattern Matching**: Find types by their structure, not just exact matches
|
|
1416
|
+
- **Variable Bindings**: Extract components from complex types
|
|
1417
|
+
- **Bulk Operations**: Add, remove, or update multiple nodes/edges matching a pattern
|
|
1418
|
+
- **Property Filtering**: Combine pattern matching with property constraints
|
|
1419
|
+
- **Transactional Safety**: All mutations are transactional with automatic rollback
|
|
1420
|
+
|
|
1421
|
+
**Important Note:** A single variable pattern like `var("X")` matches **ALL** types (both Variables and Applications) because TypeSchema uses unification. To filter only application types, use an application pattern like `app(var("X"), var("Y"))`.
|
|
1422
|
+
|
|
1423
|
+
### Example 11: Analyzing Type Variables
|
|
1424
|
+
|
|
1425
|
+
Extract and analyze variables from complex type expressions:
|
|
1426
|
+
|
|
1427
|
+
```python
|
|
1428
|
+
import implica as imp
|
|
1429
|
+
|
|
1430
|
+
# Create a complex type expression
|
|
1431
|
+
A = imp.var("A")
|
|
1432
|
+
B = imp.var("B")
|
|
1433
|
+
C = imp.var("C")
|
|
1434
|
+
|
|
1435
|
+
# ((A -> B) -> C) -> (A -> B)
|
|
1436
|
+
inner = imp.app(A, B)
|
|
1437
|
+
middle = imp.app(inner, C)
|
|
1438
|
+
complex_type = imp.app(middle, inner)
|
|
1439
|
+
|
|
1440
|
+
print(f"Type: {complex_type}")
|
|
1441
|
+
# Output: ((A -> B) -> C) -> A -> B
|
|
1442
|
+
|
|
1443
|
+
# Get all variables (including duplicates)
|
|
1444
|
+
variables = complex_type.variables
|
|
1445
|
+
print(f"All variables: {[v.name for v in variables]}")
|
|
1446
|
+
# Output: ['A', 'B', 'C', 'A', 'B']
|
|
1447
|
+
|
|
1448
|
+
# Count unique variables
|
|
1449
|
+
unique_vars = {v.name for v in variables}
|
|
1450
|
+
print(f"Unique variables: {unique_vars}")
|
|
1451
|
+
# Output: {'A', 'B', 'C'}
|
|
1452
|
+
|
|
1453
|
+
# Count occurrences
|
|
1454
|
+
from collections import Counter
|
|
1455
|
+
var_counts = Counter(v.name for v in variables)
|
|
1456
|
+
print(f"Variable counts: {dict(var_counts)}")
|
|
1457
|
+
# Output: {'A': 2, 'B': 2, 'C': 1}
|
|
1458
|
+
|
|
1459
|
+
# Check if a specific variable is used
|
|
1460
|
+
def uses_variable(type_expr, var_name):
|
|
1461
|
+
"""Check if a type expression uses a specific variable."""
|
|
1462
|
+
return any(v.name == var_name for v in type_expr.variables)
|
|
1463
|
+
|
|
1464
|
+
print(f"Uses A: {uses_variable(complex_type, 'A')}") # Output: True
|
|
1465
|
+
print(f"Uses D: {uses_variable(complex_type, 'D')}") # Output: False
|
|
1466
|
+
|
|
1467
|
+
# Find all types in a graph that use a specific variable
|
|
1468
|
+
graph = imp.Graph()
|
|
1469
|
+
|
|
1470
|
+
# Create various types
|
|
1471
|
+
types_to_add = [
|
|
1472
|
+
imp.var("X"),
|
|
1473
|
+
imp.var("Y"),
|
|
1474
|
+
imp.app(A, B),
|
|
1475
|
+
imp.app(B, C),
|
|
1476
|
+
imp.app(A, imp.app(B, C)),
|
|
1477
|
+
]
|
|
1478
|
+
|
|
1479
|
+
with graph.connect() as conn:
|
|
1480
|
+
for t in types_to_add:
|
|
1481
|
+
conn.add_node(imp.node(t))
|
|
1482
|
+
|
|
1483
|
+
# Find all nodes containing variable "B"
|
|
1484
|
+
nodes_with_B = [
|
|
1485
|
+
n for n in graph.nodes()
|
|
1486
|
+
if any(v.name == "B" for v in n.type.variables)
|
|
1487
|
+
]
|
|
1488
|
+
|
|
1489
|
+
print(f"\nNodes containing variable B:")
|
|
1490
|
+
for n in nodes_with_B:
|
|
1491
|
+
print(f" - {n.type}")
|
|
1492
|
+
# Output:
|
|
1493
|
+
# - A -> B
|
|
1494
|
+
# - B -> C
|
|
1495
|
+
# - A -> B -> C
|
|
1496
|
+
|
|
1497
|
+
# Calculate the "complexity" of a type by counting its variables
|
|
1498
|
+
def type_complexity(type_expr):
|
|
1499
|
+
"""Calculate complexity as the total number of variables."""
|
|
1500
|
+
return len(type_expr.variables)
|
|
1501
|
+
|
|
1502
|
+
print(f"\nType complexities:")
|
|
1503
|
+
for n in graph.nodes():
|
|
1504
|
+
print(f" {n.type}: {type_complexity(n.type)}")
|
|
1505
|
+
# Output:
|
|
1506
|
+
# X: 1
|
|
1507
|
+
# Y: 1
|
|
1508
|
+
# A -> B: 2
|
|
1509
|
+
# B -> C: 2
|
|
1510
|
+
# A -> B -> C: 3
|
|
1511
|
+
```
|
|
1512
|
+
|
|
1513
|
+
### Example 12: Using type_from_string for Dynamic Type Creation
|
|
1514
|
+
|
|
1515
|
+
Parse types from strings for flexible, dynamic type construction:
|
|
1516
|
+
|
|
1517
|
+
```python
|
|
1518
|
+
import implica as imp
|
|
1519
|
+
|
|
1520
|
+
# Create a graph
|
|
1521
|
+
graph = imp.Graph()
|
|
1522
|
+
|
|
1523
|
+
# Define type expressions as strings (e.g., from a config file or user input)
|
|
1524
|
+
type_strings = [
|
|
1525
|
+
"Person",
|
|
1526
|
+
"String",
|
|
1527
|
+
"Int",
|
|
1528
|
+
"Person -> String", # Get name
|
|
1529
|
+
"Person -> Int", # Get age
|
|
1530
|
+
"(Person -> String) -> (Person -> Int) -> Person", # Complex transformation
|
|
1531
|
+
]
|
|
1532
|
+
|
|
1533
|
+
# Parse and add all types to the graph
|
|
1534
|
+
nodes = []
|
|
1535
|
+
for type_str in type_strings:
|
|
1536
|
+
parsed_type = imp.type_from_string(type_str)
|
|
1537
|
+
node = imp.node(parsed_type)
|
|
1538
|
+
nodes.append(node)
|
|
1539
|
+
|
|
1540
|
+
with graph.connect() as conn:
|
|
1541
|
+
conn.add_many_nodes(nodes)
|
|
1542
|
+
|
|
1543
|
+
print(f"Created graph with {graph.node_count()} nodes from string definitions")
|
|
1544
|
+
# Output: Created graph with 6 nodes from string definitions
|
|
1545
|
+
|
|
1546
|
+
# You can also build a type library from a configuration
|
|
1547
|
+
type_library = {
|
|
1548
|
+
"identity": "A -> A",
|
|
1549
|
+
"const": "A -> B -> A",
|
|
1550
|
+
"compose": "(B -> C) -> (A -> B) -> A -> C",
|
|
1551
|
+
"apply": "(A -> B) -> A -> B",
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
print("\n=== Type Library ===")
|
|
1555
|
+
for name, type_str in type_library.items():
|
|
1556
|
+
parsed = imp.type_from_string(type_str)
|
|
1557
|
+
print(f"{name:10} : {parsed}")
|
|
1558
|
+
|
|
1559
|
+
# Output:
|
|
1560
|
+
# identity : A -> A
|
|
1561
|
+
# const : A -> B -> A
|
|
1562
|
+
# compose : (B -> C) -> (A -> B) -> A -> C
|
|
1563
|
+
# apply : (A -> B) -> A -> B
|
|
1564
|
+
|
|
1565
|
+
# Validate type strings from user input
|
|
1566
|
+
def validate_and_parse_type(user_input: str) -> tuple[bool, str, object]:
|
|
1567
|
+
"""
|
|
1568
|
+
Validate and parse a type string from user input.
|
|
1569
|
+
Returns (is_valid, message, parsed_type)
|
|
1570
|
+
"""
|
|
1571
|
+
try:
|
|
1572
|
+
parsed = imp.type_from_string(user_input)
|
|
1573
|
+
return True, f"Valid type: {parsed}", parsed
|
|
1574
|
+
except ValueError as e:
|
|
1575
|
+
return False, f"Invalid type: {e}", None
|
|
1576
|
+
|
|
1577
|
+
# Test validation
|
|
1578
|
+
test_inputs = [
|
|
1579
|
+
"A -> B",
|
|
1580
|
+
"(A -> B) -> C",
|
|
1581
|
+
"A ->", # Invalid
|
|
1582
|
+
"Person -> (Address -> String)",
|
|
1583
|
+
"A @ B", # Invalid
|
|
1584
|
+
]
|
|
1585
|
+
|
|
1586
|
+
print("\n=== Type Validation ===")
|
|
1587
|
+
for test in test_inputs:
|
|
1588
|
+
is_valid, message, parsed = validate_and_parse_type(test)
|
|
1589
|
+
status = "✓" if is_valid else "✗"
|
|
1590
|
+
print(f"{status} '{test:30}' : {message}")
|
|
1591
|
+
|
|
1592
|
+
# Output:
|
|
1593
|
+
# ✓ 'A -> B' : Valid type: A -> B
|
|
1594
|
+
# ✓ '(A -> B) -> C' : Valid type: (A -> B) -> C
|
|
1595
|
+
# ✗ 'A ->' : Invalid type: Unexpected end of input
|
|
1596
|
+
# ✓ 'Person -> (Address -> String)' : Valid type: Person -> Address -> String
|
|
1597
|
+
# ✗ 'A @ B' : Invalid type: Invalid character '@' at position 2
|
|
1598
|
+
|
|
1599
|
+
# Build a type inference system
|
|
1600
|
+
def infer_result_type(func_type_str: str, input_type_str: str) -> str:
|
|
1601
|
+
"""
|
|
1602
|
+
Given a function type and an input type, infer the result type.
|
|
1603
|
+
Returns the result type as a string, or an error message.
|
|
1604
|
+
"""
|
|
1605
|
+
try:
|
|
1606
|
+
func_type = imp.type_from_string(func_type_str)
|
|
1607
|
+
input_type = imp.type_from_string(input_type_str)
|
|
1608
|
+
|
|
1609
|
+
# Check if func_type is an application
|
|
1610
|
+
if not isinstance(func_type, imp.Application):
|
|
1611
|
+
return f"Error: {func_type_str} is not a function type"
|
|
1612
|
+
|
|
1613
|
+
# Check if input matches function's input type
|
|
1614
|
+
if func_type.input_type.uid != input_type.uid:
|
|
1615
|
+
return f"Error: Type mismatch. Expected {func_type.input_type}, got {input_type}"
|
|
1616
|
+
|
|
1617
|
+
return str(func_type.output_type)
|
|
1618
|
+
except ValueError as e:
|
|
1619
|
+
return f"Error: {e}"
|
|
1620
|
+
|
|
1621
|
+
# Test type inference
|
|
1622
|
+
print("\n=== Type Inference ===")
|
|
1623
|
+
print(f"Apply 'A -> B' to 'A': {infer_result_type('A -> B', 'A')}")
|
|
1624
|
+
# Output: B
|
|
1625
|
+
|
|
1626
|
+
print(f"Apply 'Person -> String' to 'Person': {infer_result_type('Person -> String', 'Person')}")
|
|
1627
|
+
# Output: String
|
|
1628
|
+
|
|
1629
|
+
print(f"Apply 'A -> B -> C' to 'A': {infer_result_type('A -> B -> C', 'A')}")
|
|
1630
|
+
# Output: B -> C
|
|
1631
|
+
|
|
1632
|
+
print(f"Apply 'A -> B' to 'C': {infer_result_type('A -> B', 'C')}")
|
|
1633
|
+
# Output: Error: Type mismatch. Expected A, got C
|
|
1634
|
+
|
|
1635
|
+
# Parse types from a domain-specific language
|
|
1636
|
+
dsl_program = """
|
|
1637
|
+
define Input
|
|
1638
|
+
define Output
|
|
1639
|
+
define Processor = Input -> Output
|
|
1640
|
+
define Chain = Processor -> Processor -> Processor
|
|
1641
|
+
"""
|
|
1642
|
+
|
|
1643
|
+
print("\n=== Parsing DSL ===")
|
|
1644
|
+
for line in dsl_program.strip().split('\n'):
|
|
1645
|
+
line = line.strip()
|
|
1646
|
+
if line.startswith("define "):
|
|
1647
|
+
parts = line[7:].split(" = ")
|
|
1648
|
+
type_name = parts[0].strip()
|
|
1649
|
+
|
|
1650
|
+
if len(parts) == 1:
|
|
1651
|
+
# Simple type definition
|
|
1652
|
+
print(f"{type_name}: <primitive type>")
|
|
1653
|
+
else:
|
|
1654
|
+
# Complex type definition
|
|
1655
|
+
type_expr = parts[1].strip()
|
|
1656
|
+
parsed = imp.type_from_string(type_expr)
|
|
1657
|
+
print(f"{type_name}: {parsed}")
|
|
1658
|
+
|
|
1659
|
+
# Output:
|
|
1660
|
+
# Input: <primitive type>
|
|
1661
|
+
# Output: <primitive type>
|
|
1662
|
+
# Processor: Input -> Output
|
|
1663
|
+
# Chain: Processor -> Processor -> Processor
|
|
1664
|
+
```
|
|
1665
|
+
|
|
1666
|
+
## Schema-Based Graph API
|
|
1667
|
+
|
|
1668
|
+
This section explains how to use the TypeSchema-based API for querying and manipulating the implicational logic graph. The schema-based API provides powerful pattern matching capabilities for working with nodes and edges based on their type structure.
|
|
1669
|
+
|
|
1670
|
+
### Understanding TypeSchemas
|
|
1671
|
+
|
|
1672
|
+
TypeSchemas provide pattern matching for types using unification. A schema contains a pattern with type variables that can be matched against concrete types.
|
|
1673
|
+
|
|
1674
|
+
#### Key Concepts
|
|
1675
|
+
|
|
1676
|
+
- **Pattern Variables** (no prefix): A variable like `X` or `Y` acts as a wildcard that matches **any** type (Variable or Application)
|
|
1677
|
+
- **Exact Match Variables** (with `$` prefix): A variable like `$A` or `$Person` matches only the specific named type
|
|
1678
|
+
- **Structural Matching**: A pattern like `X -> Y` matches any Application type with input `X` and output `Y`
|
|
1679
|
+
- **Variable Bindings**: When a match succeeds, pattern variables are bound to the matched types
|
|
1680
|
+
|
|
1681
|
+
#### Pattern Matching Modes
|
|
1682
|
+
|
|
1683
|
+
```python
|
|
1684
|
+
import implica as imp
|
|
1685
|
+
|
|
1686
|
+
# Pattern variable - matches ALL types
|
|
1687
|
+
any_schema = imp.TypeSchema(pattern=imp.var("X"))
|
|
1688
|
+
# This will match Variables, Applications, everything
|
|
1689
|
+
|
|
1690
|
+
# Exact match - matches only specific type
|
|
1691
|
+
exact_schema = imp.TypeSchema(pattern=imp.var("$A"))
|
|
1692
|
+
# This will match ONLY Variable with name "A"
|
|
1693
|
+
|
|
1694
|
+
# Application pattern - matches only Application types
|
|
1695
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1696
|
+
# This will match only Application types (function types)
|
|
1697
|
+
|
|
1698
|
+
# Mixed pattern - exact source, pattern destination
|
|
1699
|
+
mixed_schema = imp.TypeSchema(pattern=imp.app(imp.var("$Person"), imp.var("Y")))
|
|
1700
|
+
# This matches Applications where input is exactly "Person", output is anything
|
|
1701
|
+
```
|
|
1702
|
+
|
|
1703
|
+
**Use cases for exact matching (`$` prefix):**
|
|
1704
|
+
|
|
1705
|
+
- Filter nodes by specific named types: `filter_nodes_by_schema(TypeSchema(var("$Int")))`
|
|
1706
|
+
- Find edges from/to specific types: `filter_edges_by_schema(src_schema=TypeSchema(var("$String")))`
|
|
1707
|
+
- Remove or update specific named types while preserving others
|
|
1708
|
+
- Combine exact and pattern matching for precise queries
|
|
1709
|
+
|
|
1710
|
+
### Schema Query Operations
|
|
1711
|
+
|
|
1712
|
+
#### Get Nodes Matching Schema
|
|
1713
|
+
|
|
1714
|
+
Retrieve all nodes whose types match a schema pattern, along with variable bindings:
|
|
1715
|
+
|
|
1716
|
+
```python
|
|
1717
|
+
import implica as imp
|
|
1718
|
+
|
|
1719
|
+
g = imp.Graph()
|
|
1720
|
+
|
|
1721
|
+
# Add nodes
|
|
1722
|
+
a = imp.var("A")
|
|
1723
|
+
b = imp.var("B")
|
|
1724
|
+
ab = imp.app(a, b)
|
|
1725
|
+
|
|
1726
|
+
with g.connect() as conn:
|
|
1727
|
+
conn.add_many_nodes(types=[a, b, ab])
|
|
1728
|
+
|
|
1729
|
+
# Match all application types (X -> Y)
|
|
1730
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1731
|
+
matches = g.get_nodes_matching_schema(app_schema)
|
|
1732
|
+
|
|
1733
|
+
for node, match in matches:
|
|
1734
|
+
print(f"Node: {node.type}")
|
|
1735
|
+
print(f"Bindings: {match.bindings}")
|
|
1736
|
+
# Bindings will show what X and Y matched
|
|
1737
|
+
```
|
|
1738
|
+
|
|
1739
|
+
#### Filter Nodes by Schema
|
|
1740
|
+
|
|
1741
|
+
Filter nodes by schema and optionally by properties:
|
|
1742
|
+
|
|
1743
|
+
```python
|
|
1744
|
+
# Filter application types with specific properties
|
|
1745
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1746
|
+
nodes = g.filter_nodes_by_schema(app_schema, properties={"weight": 1.0})
|
|
1747
|
+
|
|
1748
|
+
# All returned nodes will have application types AND weight=1.0
|
|
1749
|
+
```
|
|
1750
|
+
|
|
1751
|
+
#### Filter Edges by Schema
|
|
1752
|
+
|
|
1753
|
+
Filter edges by matching source node type, destination node type, and/or combinator type:
|
|
1754
|
+
|
|
1755
|
+
```python
|
|
1756
|
+
# Filter edges where source is an application
|
|
1757
|
+
src_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1758
|
+
edges = g.filter_edges_by_schema(src_schema=src_schema)
|
|
1759
|
+
|
|
1760
|
+
# Filter edges with source application AND destination variable
|
|
1761
|
+
dst_schema = imp.TypeSchema(pattern=imp.var("Z"))
|
|
1762
|
+
edges = g.filter_edges_by_schema(
|
|
1763
|
+
src_schema=src_schema,
|
|
1764
|
+
dst_schema=dst_schema
|
|
1765
|
+
)
|
|
1766
|
+
|
|
1767
|
+
# Add property filtering
|
|
1768
|
+
edges = g.filter_edges_by_schema(
|
|
1769
|
+
src_schema=src_schema,
|
|
1770
|
+
properties={"label": "main"}
|
|
1771
|
+
)
|
|
1772
|
+
```
|
|
1773
|
+
|
|
1774
|
+
#### Count and Check Existence
|
|
1775
|
+
|
|
1776
|
+
```python
|
|
1777
|
+
# Count nodes matching schema
|
|
1778
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1779
|
+
count = g.count_nodes_matching_schema(app_schema)
|
|
1780
|
+
|
|
1781
|
+
# Check if any node matches
|
|
1782
|
+
has_apps = g.has_node_matching_schema(app_schema)
|
|
1783
|
+
|
|
1784
|
+
# Count edges matching schema
|
|
1785
|
+
count = g.count_edges_matching_schema(src_schema=app_schema)
|
|
1786
|
+
|
|
1787
|
+
# Check if any edge matches
|
|
1788
|
+
has_edges = g.has_edge_matching_schema(dst_schema=app_schema)
|
|
1789
|
+
```
|
|
1790
|
+
|
|
1791
|
+
### Schema Mutation Operations
|
|
1792
|
+
|
|
1793
|
+
All mutation operations use the Connection API for transactional safety. Schema-based mutations query the graph at mutation-building time.
|
|
1794
|
+
|
|
1795
|
+
#### Add Nodes for Schema
|
|
1796
|
+
|
|
1797
|
+
Add only those nodes from a list whose types match a schema:
|
|
1798
|
+
|
|
1799
|
+
```python
|
|
1800
|
+
import implica as imp
|
|
1801
|
+
|
|
1802
|
+
g = imp.Graph()
|
|
1803
|
+
|
|
1804
|
+
# List of types to potentially add
|
|
1805
|
+
types = [imp.var("A"), imp.var("B"), imp.app(imp.var("A"), imp.var("B"))]
|
|
1806
|
+
|
|
1807
|
+
# Only add application types
|
|
1808
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1809
|
+
|
|
1810
|
+
with g.connect() as conn:
|
|
1811
|
+
conn.add_nodes_for_schema(app_schema, types)
|
|
1812
|
+
|
|
1813
|
+
# Result: Only the Application node is added
|
|
1814
|
+
```
|
|
1815
|
+
|
|
1816
|
+
With properties:
|
|
1817
|
+
|
|
1818
|
+
```python
|
|
1819
|
+
with g.connect() as conn:
|
|
1820
|
+
conn.add_nodes_for_schema(
|
|
1821
|
+
app_schema,
|
|
1822
|
+
types,
|
|
1823
|
+
properties={"source": "generated"}
|
|
1824
|
+
)
|
|
1825
|
+
```
|
|
1826
|
+
|
|
1827
|
+
#### Remove Nodes Matching Schema
|
|
1828
|
+
|
|
1829
|
+
Remove all nodes in the graph whose types match a schema:
|
|
1830
|
+
|
|
1831
|
+
```python
|
|
1832
|
+
# Remove all application nodes
|
|
1833
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1834
|
+
|
|
1835
|
+
with g.connect() as conn:
|
|
1836
|
+
conn.remove_nodes_matching_schema(app_schema)
|
|
1837
|
+
|
|
1838
|
+
# With property filter
|
|
1839
|
+
with g.connect() as conn:
|
|
1840
|
+
conn.remove_nodes_matching_schema(
|
|
1841
|
+
app_schema,
|
|
1842
|
+
properties={"temporary": True}
|
|
1843
|
+
)
|
|
1844
|
+
```
|
|
1845
|
+
|
|
1846
|
+
#### Remove Edges Matching Schema
|
|
1847
|
+
|
|
1848
|
+
Remove all edges matching schema patterns:
|
|
1849
|
+
|
|
1850
|
+
```python
|
|
1851
|
+
# Remove edges where source is an application
|
|
1852
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1853
|
+
|
|
1854
|
+
with g.connect() as conn:
|
|
1855
|
+
conn.remove_edges_matching_schema(src_schema=app_schema)
|
|
1856
|
+
|
|
1857
|
+
# Remove edges with specific source AND destination patterns
|
|
1858
|
+
with g.connect() as conn:
|
|
1859
|
+
conn.remove_edges_matching_schema(
|
|
1860
|
+
src_schema=app_schema,
|
|
1861
|
+
dst_schema=imp.TypeSchema(pattern=imp.var("Z"))
|
|
1862
|
+
)
|
|
1863
|
+
```
|
|
1864
|
+
|
|
1865
|
+
#### Update Nodes/Edges Matching Schema
|
|
1866
|
+
|
|
1867
|
+
Update properties of all matching nodes or edges:
|
|
1868
|
+
|
|
1869
|
+
```python
|
|
1870
|
+
# Update all application nodes
|
|
1871
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1872
|
+
|
|
1873
|
+
with g.connect() as conn:
|
|
1874
|
+
conn.update_nodes_matching_schema(
|
|
1875
|
+
app_schema,
|
|
1876
|
+
{"processed": True, "depth": 1}
|
|
1877
|
+
)
|
|
1878
|
+
|
|
1879
|
+
# Update with filter
|
|
1880
|
+
with g.connect() as conn:
|
|
1881
|
+
conn.update_nodes_matching_schema(
|
|
1882
|
+
app_schema,
|
|
1883
|
+
{"status": "done"},
|
|
1884
|
+
filter_properties={"status": "pending"}
|
|
1885
|
+
)
|
|
1886
|
+
|
|
1887
|
+
# Update edges
|
|
1888
|
+
with g.connect() as conn:
|
|
1889
|
+
conn.update_edges_matching_schema(
|
|
1890
|
+
{"weight": 2.0},
|
|
1891
|
+
src_schema=app_schema
|
|
1892
|
+
)
|
|
1893
|
+
```
|
|
1894
|
+
|
|
1895
|
+
#### Try Variants
|
|
1896
|
+
|
|
1897
|
+
All mutation operations have `try_*` variants that don't fail if elements don't exist:
|
|
1898
|
+
|
|
1899
|
+
```python
|
|
1900
|
+
with g.connect() as conn:
|
|
1901
|
+
# Won't fail if nodes don't exist
|
|
1902
|
+
conn.try_remove_nodes_matching_schema(app_schema)
|
|
1903
|
+
|
|
1904
|
+
# Won't fail if edges don't exist
|
|
1905
|
+
conn.try_remove_edges_matching_schema(src_schema=app_schema)
|
|
1906
|
+
|
|
1907
|
+
# Won't fail for non-existent nodes
|
|
1908
|
+
conn.try_update_nodes_matching_schema(app_schema, {"value": 1})
|
|
1909
|
+
```
|
|
1910
|
+
|
|
1911
|
+
### Schema API Best Practices
|
|
1912
|
+
|
|
1913
|
+
#### 1. Schema Pattern Design
|
|
1914
|
+
|
|
1915
|
+
```python
|
|
1916
|
+
import implica as imp
|
|
1917
|
+
|
|
1918
|
+
# ❌ Don't use pattern variable alone to filter specific Variable types
|
|
1919
|
+
# Pattern variables match ALL types, not just Variables
|
|
1920
|
+
var_schema = imp.TypeSchema(pattern=imp.var("X"))
|
|
1921
|
+
nodes = g.filter_nodes_by_schema(var_schema) # Returns ALL nodes (Variables AND Applications)
|
|
1922
|
+
|
|
1923
|
+
# ✓ Use exact match to filter specific named Variables
|
|
1924
|
+
exact_schema = imp.TypeSchema(pattern=imp.var("$Int"))
|
|
1925
|
+
int_nodes = g.filter_nodes_by_schema(exact_schema) # Returns only Variable nodes named "Int"
|
|
1926
|
+
|
|
1927
|
+
# ✓ Use application pattern to filter Applications
|
|
1928
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1929
|
+
apps = g.filter_nodes_by_schema(app_schema) # Returns only Application nodes
|
|
1930
|
+
|
|
1931
|
+
# ✓ Mix exact and pattern matching for precise control
|
|
1932
|
+
mixed_schema = imp.TypeSchema(pattern=imp.app(imp.var("$Person"), imp.var("Y")))
|
|
1933
|
+
person_funcs = g.filter_nodes_by_schema(mixed_schema) # Returns only "Person -> Y" Applications
|
|
1934
|
+
|
|
1935
|
+
# ✓ Use specific patterns for more complex matching
|
|
1936
|
+
nested_schema = imp.TypeSchema(pattern=imp.app(imp.app(imp.var("X"), imp.var("Y")), imp.var("Z")))
|
|
1937
|
+
nested = g.filter_nodes_by_schema(nested_schema) # Returns (X -> Y) -> Z types
|
|
1938
|
+
```
|
|
1939
|
+
|
|
1940
|
+
**Pattern Selection Guide:**
|
|
1941
|
+
|
|
1942
|
+
| Goal | Pattern | Example |
|
|
1943
|
+
| ------------------------ | ----------------------------- | --------------------------------------- |
|
|
1944
|
+
| Match specific Variable | `var("$Name")` | `var("$Int")` matches only "Int" |
|
|
1945
|
+
| Match any Variable | Use exact match for each name | Iterate over known names |
|
|
1946
|
+
| Match any Application | `app(var("X"), var("Y"))` | Matches all function types |
|
|
1947
|
+
| Match specific structure | Exact types in pattern | `app(var("$A"), var("$B"))` |
|
|
1948
|
+
| Extract components | Pattern variables | `app(var("X"), var("Y"))` with bindings |
|
|
1949
|
+
|
|
1950
|
+
#### 2. Combine Schema and Property Filters
|
|
1951
|
+
|
|
1952
|
+
```python
|
|
1953
|
+
# Use both schema and properties for precise filtering
|
|
1954
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1955
|
+
|
|
1956
|
+
nodes = g.filter_nodes_by_schema(
|
|
1957
|
+
app_schema,
|
|
1958
|
+
properties={"processed": False, "priority": "high"}
|
|
1959
|
+
)
|
|
1960
|
+
```
|
|
1961
|
+
|
|
1962
|
+
#### 3. Use Transactions for Safety
|
|
1963
|
+
|
|
1964
|
+
```python
|
|
1965
|
+
# Always use context manager for mutations
|
|
1966
|
+
with g.connect() as conn:
|
|
1967
|
+
conn.remove_nodes_matching_schema(schema)
|
|
1968
|
+
conn.add_nodes_for_schema(other_schema, new_types)
|
|
1969
|
+
# Automatic rollback on error
|
|
1970
|
+
```
|
|
1971
|
+
|
|
1972
|
+
#### 4. Check Before Bulk Operations
|
|
1973
|
+
|
|
1974
|
+
```python
|
|
1975
|
+
# Check count before removing
|
|
1976
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
1977
|
+
count = g.count_nodes_matching_schema(app_schema)
|
|
1978
|
+
print(f"Will remove {count} nodes")
|
|
1979
|
+
|
|
1980
|
+
if count > 0:
|
|
1981
|
+
with g.connect() as conn:
|
|
1982
|
+
conn.remove_nodes_matching_schema(app_schema)
|
|
1983
|
+
```
|
|
1984
|
+
|
|
1985
|
+
#### 5. Use Try Variants for Idempotent Operations
|
|
1986
|
+
|
|
1987
|
+
```python
|
|
1988
|
+
# Use try_* for operations that might not find matches
|
|
1989
|
+
with g.connect() as conn:
|
|
1990
|
+
# Safe even if no matches
|
|
1991
|
+
conn.try_remove_nodes_matching_schema(schema)
|
|
1992
|
+
conn.try_update_edges_matching_schema(props, src_schema=schema)
|
|
1993
|
+
```
|
|
1994
|
+
|
|
1995
|
+
#### 6. Leverage Variable Bindings
|
|
1996
|
+
|
|
1997
|
+
```python
|
|
1998
|
+
# Access variable bindings from matches
|
|
1999
|
+
app_schema = imp.TypeSchema(pattern=imp.app(imp.var("X"), imp.var("Y")))
|
|
2000
|
+
matches = g.get_nodes_matching_schema(app_schema)
|
|
2001
|
+
|
|
2002
|
+
for node, match in matches:
|
|
2003
|
+
input_type = match.bindings["X"]
|
|
2004
|
+
output_type = match.bindings["Y"]
|
|
2005
|
+
print(f"Edge from {input_type} to {output_type}")
|
|
2006
|
+
```
|
|
2007
|
+
|
|
2008
|
+
### Schema API Summary
|
|
2009
|
+
|
|
2010
|
+
The schema-based API provides a powerful and expressive way to query and manipulate graphs based on type patterns. By combining schema matching with property filtering, you can perform complex operations on graph structures efficiently and safely.
|
|
2011
|
+
|
|
2012
|
+
**Key takeaways:**
|
|
2013
|
+
|
|
2014
|
+
- Schemas use unification, not type discrimination
|
|
2015
|
+
- **Pattern variables** (no prefix) match **all** types - use for generic matching
|
|
2016
|
+
- **Exact match variables** (`$` prefix) match specific named types - use for precise filtering
|
|
2017
|
+
- Single pattern variable patterns match **all** types (both Variables and Applications)
|
|
2018
|
+
- Use application patterns `app(var("X"), var("Y"))` to filter only Application types
|
|
2019
|
+
- Use exact match `var("$TypeName")` to filter only a specific Variable type
|
|
2020
|
+
- Combine exact and pattern matching for flexible queries
|
|
2021
|
+
- Combine schemas with property filters for precise control
|
|
2022
|
+
- Always use transactions for mutations
|
|
2023
|
+
- Use `try_*` variants for robust error handling
|
|
2024
|
+
|
|
2025
|
+
**Quick Reference:**
|
|
2026
|
+
|
|
2027
|
+
```python
|
|
2028
|
+
# Match everything
|
|
2029
|
+
TypeSchema(var("X")) # Wildcard - matches all types
|
|
2030
|
+
|
|
2031
|
+
# Match specific Variable
|
|
2032
|
+
TypeSchema(var("$Int")) # Exact - matches only "Int" Variable
|
|
2033
|
+
|
|
2034
|
+
# Match all Applications
|
|
2035
|
+
TypeSchema(app(var("X"), var("Y"))) # Structural pattern
|
|
2036
|
+
|
|
2037
|
+
# Match specific structure
|
|
2038
|
+
TypeSchema(app(var("$A"), var("$B"))) # Exact input and output
|
|
2039
|
+
|
|
2040
|
+
# Mixed matching
|
|
2041
|
+
TypeSchema(app(var("$Person"), var("Y"))) # Exact input, any output
|
|
2042
|
+
```
|
|
2043
|
+
|
|
2044
|
+
## API Reference
|
|
2045
|
+
|
|
2046
|
+
### Core Module (`implica.core`)
|
|
2047
|
+
|
|
2048
|
+
**Types:**
|
|
2049
|
+
|
|
2050
|
+
- `var(name: str) -> Variable`: Create a type variable
|
|
2051
|
+
- `app(input_type: BaseType, output_type: BaseType) -> Application`: Create a function type
|
|
2052
|
+
- `type_from_string(type_str: str) -> BaseType`: Parse a type from a string representation
|
|
2053
|
+
- `Variable`: Atomic type variable
|
|
2054
|
+
- `name: str`: The name of the variable
|
|
2055
|
+
- `uid: str`: Unique identifier (SHA256 hash)
|
|
2056
|
+
- `variables: list[Variable]`: Returns `[self]`
|
|
2057
|
+
- `Application`: Function application type
|
|
2058
|
+
- `input_type: BaseType`: Input type of the function
|
|
2059
|
+
- `output_type: BaseType`: Output type of the function
|
|
2060
|
+
- `uid: str`: Unique identifier (SHA256 hash)
|
|
2061
|
+
- `variables: list[Variable]`: Returns all variables from input and output types (may include duplicates)
|
|
2062
|
+
|
|
2063
|
+
**Combinators:**
|
|
2064
|
+
|
|
2065
|
+
- `S(A, B, C) -> Combinator`: Create S combinator with type `(A -> B -> C) -> (A -> B) -> A -> C`
|
|
2066
|
+
- `K(A, B) -> Combinator`: Create K combinator with type `A -> B -> A`
|
|
2067
|
+
- `combinator(name: str, type: Application) -> Combinator`: Create a custom combinator
|
|
2068
|
+
- `Combinator`: Generic combinator with name and Application type
|
|
2069
|
+
- `name: str`: Name/identifier of the combinator
|
|
2070
|
+
- `type: Application`: **Must be an Application type** (function type), not a Variable
|
|
2071
|
+
- Raises `ValidationError` if type is not an Application
|
|
2072
|
+
|
|
2073
|
+
**Type Schemas:**
|
|
2074
|
+
|
|
2075
|
+
- `schema(pattern: BaseType) -> TypeSchema`: Helper function to create a TypeSchema
|
|
2076
|
+
- `TypeSchema(pattern: BaseType)`: Create a type schema for pattern matching
|
|
2077
|
+
- `pattern: BaseType`: The type pattern containing variables to match against
|
|
2078
|
+
- `match(target: BaseType) -> Optional[TypeMatch]`: Try to match a type against the pattern
|
|
2079
|
+
- `get_pattern_variables() -> Set[str]`: Get all variable names in the pattern
|
|
2080
|
+
- `TypeMatch`: Result of a successful pattern match
|
|
2081
|
+
- `original_type: BaseType`: The type that was successfully matched
|
|
2082
|
+
- `bindings: Dict[str, BaseType]`: Mapping from variable names to their matched types
|
|
2083
|
+
|
|
2084
|
+
### Graph Module (`implica.graph`)
|
|
2085
|
+
|
|
2086
|
+
**Elements:**
|
|
2087
|
+
|
|
2088
|
+
- `node(type: BaseType, properties: dict[str, Any] = None) -> Node`: Create a node from any type (Variable or Application)
|
|
2089
|
+
- `properties`: Optional dict of metadata to attach to the node
|
|
2090
|
+
- `edge(combinator: Combinator, properties: dict[str, Any] = None) -> Edge`: Create an edge from a combinator (automatically infers source/destination nodes)
|
|
2091
|
+
- `properties`: Optional dict of metadata to attach to the edge
|
|
2092
|
+
- Raises `ValueError` if combinator doesn't have an Application type
|
|
2093
|
+
- `Node`: Graph node representing a type
|
|
2094
|
+
- `type: BaseType`: The type this node represents (can be Variable or Application)
|
|
2095
|
+
- `uid: str`: Unique identifier
|
|
2096
|
+
- `properties: dict[str, Any]`: Metadata dictionary (empty by default)
|
|
2097
|
+
- `Edge`: Graph edge representing a transformation via a combinator
|
|
2098
|
+
- `src_node: Node`: Source node (must match combinator's input type)
|
|
2099
|
+
- `dst_node: Node`: Destination node (must match combinator's output type)
|
|
2100
|
+
- `combinator: Combinator`: The combinator (must have Application type)
|
|
2101
|
+
- `properties: dict[str, Any]`: Metadata dictionary (empty by default)
|
|
2102
|
+
- Raises `ValueError` if:
|
|
2103
|
+
- Combinator type is not an Application
|
|
2104
|
+
- Input/output types don't match source/destination nodes
|
|
2105
|
+
|
|
2106
|
+
**Graph:**
|
|
2107
|
+
|
|
2108
|
+
- `Graph()`: Create a new empty graph
|
|
2109
|
+
- `graph.connect() -> Connection`: Create a transactional connection
|
|
2110
|
+
- `graph.validate() -> bool`: Validate graph consistency
|
|
2111
|
+
- `graph.has_node(uid: str) -> bool`: Check if node exists
|
|
2112
|
+
- `graph.get_node(uid: str) -> Node`: Get node by UID
|
|
2113
|
+
- `graph.get_node_by_type(type: BaseType) -> Optional[Node]`: Get node by type
|
|
2114
|
+
- `graph.get_outgoing_edges(uid: str) -> list[Edge]`: Get outgoing edges
|
|
2115
|
+
- `graph.get_incoming_edges(uid: str) -> list[Edge]`: Get incoming edges
|
|
2116
|
+
- `graph.nodes() -> Iterator[Node]`: Iterate over nodes
|
|
2117
|
+
- `graph.edges() -> Iterator[Edge]`: Iterate over edges
|
|
2118
|
+
- `graph.node_count() -> int`: Get number of nodes
|
|
2119
|
+
- `graph.edge_count() -> int`: Get number of edges
|
|
2120
|
+
|
|
2121
|
+
**Schema-Based Query Methods:**
|
|
2122
|
+
|
|
2123
|
+
- `graph.get_nodes_matching_schema(schema: TypeSchema) -> list[tuple[Node, TypeMatch]]`: Get all nodes matching a schema with their variable bindings
|
|
2124
|
+
- `graph.filter_nodes_by_schema(schema: TypeSchema, properties: dict[str, Any] = None) -> list[Node]`: Filter nodes by schema and optionally by properties
|
|
2125
|
+
- `graph.get_edges_matching_schema(src_schema: TypeSchema = None, dst_schema: TypeSchema = None, combinator_schema: TypeSchema = None) -> list[tuple[Edge, dict[str, TypeMatch]]]`: Get edges matching schemas with bindings
|
|
2126
|
+
- `graph.filter_edges_by_schema(src_schema: TypeSchema = None, dst_schema: TypeSchema = None, combinator_schema: TypeSchema = None, properties: dict[str, Any] = None) -> list[Edge]`: Filter edges by schemas and properties
|
|
2127
|
+
- `graph.count_nodes_matching_schema(schema: TypeSchema, properties: dict[str, Any] = None) -> int`: Count nodes matching schema
|
|
2128
|
+
- `graph.has_node_matching_schema(schema: TypeSchema, properties: dict[str, Any] = None) -> bool`: Check if any node matches schema
|
|
2129
|
+
- `graph.count_edges_matching_schema(src_schema: TypeSchema = None, dst_schema: TypeSchema = None, combinator_schema: TypeSchema = None, properties: dict[str, Any] = None) -> int`: Count edges matching schemas
|
|
2130
|
+
- `graph.has_edge_matching_schema(src_schema: TypeSchema = None, dst_schema: TypeSchema = None, combinator_schema: TypeSchema = None, properties: dict[str, Any] = None) -> bool`: Check if any edge matches schemas
|
|
2131
|
+
|
|
2132
|
+
**Connection:**
|
|
2133
|
+
|
|
2134
|
+
- `Connection`: Transactional graph modification context
|
|
2135
|
+
- `conn.add_node(node: Node = None, *, type: BaseType = None, properties: dict[str, Any] = None) -> Connection`: Queue node addition
|
|
2136
|
+
- Pass either `node` (pre-created) or `type` and optional `properties` (creates node)
|
|
2137
|
+
- `conn.add_edge(combinator: Combinator, properties: dict[str, Any] = None) -> Connection`: Queue edge addition
|
|
2138
|
+
- Creates edge from combinator with optional properties
|
|
2139
|
+
- `conn.remove_node(uid: str) -> Connection`: Queue node removal
|
|
2140
|
+
- `conn.remove_edge(uid: str) -> Connection`: Queue edge removal
|
|
2141
|
+
- `conn.add_many_nodes(nodes: list[Node] = None, *, types: list[BaseType] = None, properties: dict[str, Any] | list[dict[str, Any]] = None) -> Connection`: Queue multiple node additions
|
|
2142
|
+
- Pass either `nodes` (pre-created) or `types` with optional `properties`
|
|
2143
|
+
- `properties` can be single dict (applied to all) or list of dicts (one per node)
|
|
2144
|
+
- If list, length must match `types` length
|
|
2145
|
+
- `conn.add_many_edges(combinators: list[Combinator], properties: dict[str, Any] | list[dict[str, Any]] = None) -> Connection`: Queue multiple edge additions
|
|
2146
|
+
- `properties` can be single dict (applied to all) or list of dicts (one per edge)
|
|
2147
|
+
- If list, length must match `combinators` length
|
|
2148
|
+
- `conn.try_add_node(node: Node = None, *, type: BaseType = None, properties: dict[str, Any] = None) -> Connection`: Queue node addition (no error if exists)
|
|
2149
|
+
- `conn.try_add_edge(combinator: Combinator, properties: dict[str, Any] = None) -> Connection`: Queue edge addition (no error if exists)
|
|
2150
|
+
- `conn.try_add_many_nodes(nodes: list[Node] = None, *, types: list[BaseType] = None, properties: dict[str, Any] | list[dict[str, Any]] = None) -> Connection`: Queue multiple node additions (no error if any exist)
|
|
2151
|
+
- `conn.try_add_many_edges(combinators: list[Combinator], properties: dict[str, Any] | list[dict[str, Any]] = None) -> Connection`: Queue multiple edge additions (no error if any exist)
|
|
2152
|
+
- `conn.remove_many_nodes(node_uids: list[str]) -> Connection`: Queue multiple node removals
|
|
2153
|
+
- `conn.remove_many_edges(edge_uids: list[str]) -> Connection`: Queue multiple edge removals
|
|
2154
|
+
- `conn.try_remove_node(node_uid: str) -> Connection`: Queue node removal (no error if not exists)
|
|
2155
|
+
- `conn.try_remove_edge(edge_uid: str) -> Connection`: Queue edge removal (no error if not exists)
|
|
2156
|
+
- `conn.commit()`: Apply all queued operations
|
|
2157
|
+
- `conn.rollback()`: Discard all queued operations
|
|
2158
|
+
|
|
2159
|
+
**Schema-Based Mutation Methods:**
|
|
2160
|
+
|
|
2161
|
+
- `conn.add_nodes_for_schema(schema: TypeSchema, types: list[BaseType], properties: dict[str, Any] = None) -> Connection`: Add only types matching schema
|
|
2162
|
+
- `conn.try_add_nodes_for_schema(schema: TypeSchema, types: list[BaseType], properties: dict[str, Any] = None) -> Connection`: Add matching types (no error if exist)
|
|
2163
|
+
- `conn.remove_nodes_matching_schema(schema: TypeSchema, properties: dict[str, Any] = None) -> Connection`: Remove all nodes matching schema
|
|
2164
|
+
- `conn.try_remove_nodes_matching_schema(schema: TypeSchema, properties: dict[str, Any] = None) -> Connection`: Remove matching nodes (no error if not exist)
|
|
2165
|
+
- `conn.remove_edges_matching_schema(src_schema: TypeSchema = None, dst_schema: TypeSchema = None, combinator_schema: TypeSchema = None, properties: dict[str, Any] = None) -> Connection`: Remove edges matching schemas
|
|
2166
|
+
- `conn.try_remove_edges_matching_schema(src_schema: TypeSchema = None, dst_schema: TypeSchema = None, combinator_schema: TypeSchema = None, properties: dict[str, Any] = None) -> Connection`: Remove matching edges (no error if not exist)
|
|
2167
|
+
- `conn.update_nodes_matching_schema(schema: TypeSchema, new_properties: dict[str, Any], filter_properties: dict[str, Any] = None) -> Connection`: Update properties of nodes matching schema
|
|
2168
|
+
- `conn.try_update_nodes_matching_schema(schema: TypeSchema, new_properties: dict[str, Any], filter_properties: dict[str, Any] = None) -> Connection`: Update matching nodes (no error if not exist)
|
|
2169
|
+
- `conn.update_edges_matching_schema(new_properties: dict[str, Any], src_schema: TypeSchema = None, dst_schema: TypeSchema = None, combinator_schema: TypeSchema = None, filter_properties: dict[str, Any] = None) -> Connection`: Update properties of edges matching schemas
|
|
2170
|
+
- `conn.try_update_edges_matching_schema(new_properties: dict[str, Any], src_schema: TypeSchema = None, dst_schema: TypeSchema = None, combinator_schema: TypeSchema = None, filter_properties: dict[str, Any] = None) -> Connection`: Update matching edges (no error if not exist)
|
|
2171
|
+
|
|
2172
|
+
### Mutations Module (`implica.mutations`)
|
|
2173
|
+
|
|
2174
|
+
- `Mutation`: Abstract base class for all mutations
|
|
2175
|
+
- `AddNode(node)`: Add a single node
|
|
2176
|
+
- `RemoveNode(node_uid)`: Remove a single node
|
|
2177
|
+
- `AddEdge(edge)`: Add a single edge
|
|
2178
|
+
- `RemoveEdge(edge_uid)`: Remove a single edge
|
|
2179
|
+
- `AddManyNodes(nodes)`: Add multiple nodes atomically
|
|
2180
|
+
- `AddManyEdges(edges)`: Add multiple edges atomically
|
|
2181
|
+
- `TryAddNode(node)`: Add a node or do nothing if it exists
|
|
2182
|
+
- `TryAddEdge(edge)`: Add an edge or do nothing if it exists
|
|
2183
|
+
- `RemoveManyNodes(node_uids)`: Remove multiple nodes atomically
|
|
2184
|
+
- `RemoveManyEdges(edge_uids)`: Remove multiple edges atomically
|
|
2185
|
+
- `TryRemoveNode(node_uid)`: Remove a node or do nothing if it doesn't exist
|
|
2186
|
+
- `TryRemoveEdge(edge_uid)`: Remove an edge or do nothing if it doesn't exist
|
|
2187
|
+
|
|
2188
|
+
### Utilities Module (`implica.utils`)
|
|
2189
|
+
|
|
2190
|
+
**Parsing Functions:**
|
|
2191
|
+
|
|
2192
|
+
- `tokenize(type_str: str) -> list[str]`: Tokenize a type string into tokens
|
|
2193
|
+
- Returns a list of tokens: variable names, `'('`, `')'`, and `'->'`
|
|
2194
|
+
- Raises `ValueError` if invalid characters are found
|
|
2195
|
+
- `parse_type(tokens: list[str]) -> tuple[BaseType, list[str]]`: Parse tokens into a type
|
|
2196
|
+
- Uses recursive descent parsing with right-associativity for arrows
|
|
2197
|
+
- Returns the parsed type and any remaining tokens
|
|
2198
|
+
- Raises `ValueError` if tokens cannot be parsed
|
|
2199
|
+
- `parse_primary(tokens: list[str]) -> tuple[BaseType, list[str]]`: Parse a primary type
|
|
2200
|
+
- Handles variables and parenthesized types
|
|
2201
|
+
- Returns the parsed type and remaining tokens
|
|
2202
|
+
- Raises `ValueError` if tokens cannot be parsed
|
|
2203
|
+
|
|
2204
|
+
**Note:** These functions are primarily used internally by `type_from_string()`, but are exposed for advanced use cases where you need direct control over the parsing process.
|
|
2205
|
+
|
|
2206
|
+
**Example:**
|
|
2207
|
+
|
|
2208
|
+
```python
|
|
2209
|
+
from implica.utils import tokenize, parse_type
|
|
2210
|
+
|
|
2211
|
+
# Tokenize a type string
|
|
2212
|
+
tokens = tokenize("(A -> B) -> C")
|
|
2213
|
+
print(tokens) # Output: ['(', 'A', '->', 'B', ')', '->', 'C']
|
|
2214
|
+
|
|
2215
|
+
# Parse the tokens
|
|
2216
|
+
type_result, remaining = parse_type(tokens)
|
|
2217
|
+
print(type_result) # Output: (A -> B) -> C
|
|
2218
|
+
print(remaining) # Output: []
|
|
2219
|
+
|
|
2220
|
+
# Custom parsing workflow
|
|
2221
|
+
def parse_and_validate(type_str: str) -> bool:
|
|
2222
|
+
"""Check if a string is a valid type expression."""
|
|
2223
|
+
try:
|
|
2224
|
+
tokens = tokenize(type_str)
|
|
2225
|
+
result, remaining = parse_type(tokens)
|
|
2226
|
+
return len(remaining) == 0 # Valid if no tokens remain
|
|
2227
|
+
except ValueError:
|
|
2228
|
+
return False
|
|
2229
|
+
|
|
2230
|
+
print(parse_and_validate("A -> B")) # Output: True
|
|
2231
|
+
print(parse_and_validate("A ->")) # Output: False
|
|
2232
|
+
print(parse_and_validate("(A -> B) -> C")) # Output: True
|
|
2233
|
+
```
|
|
2234
|
+
|
|
2235
|
+
## Development
|
|
2236
|
+
|
|
2237
|
+
### Setup
|
|
2238
|
+
|
|
2239
|
+
```bash
|
|
2240
|
+
# Clone the repository
|
|
2241
|
+
git clone https://github.com/carlosFerLo/implicational-logic-graph.git
|
|
2242
|
+
cd implicational-logic-graph
|
|
2243
|
+
|
|
2244
|
+
# Install dependencies
|
|
2245
|
+
poetry install
|
|
2246
|
+
|
|
2247
|
+
# Run tests
|
|
2248
|
+
poetry run pytest
|
|
2249
|
+
|
|
2250
|
+
# Run tests with coverage
|
|
2251
|
+
poetry run pytest --cov=src/implicational_logic_graph
|
|
2252
|
+
```
|
|
2253
|
+
|
|
2254
|
+
### Running Tests
|
|
2255
|
+
|
|
2256
|
+
```bash
|
|
2257
|
+
# Run all tests
|
|
2258
|
+
poetry run pytest
|
|
2259
|
+
|
|
2260
|
+
# Run specific test file
|
|
2261
|
+
poetry run pytest tests/test_graph.py
|
|
2262
|
+
|
|
2263
|
+
# Run with verbose output
|
|
2264
|
+
poetry run pytest -v
|
|
2265
|
+
|
|
2266
|
+
# Run with coverage report
|
|
2267
|
+
poetry run pytest --cov=src/implica --cov-report=html
|
|
2268
|
+
```
|
|
2269
|
+
|
|
2270
|
+
## Contributing
|
|
2271
|
+
|
|
2272
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
2273
|
+
|
|
2274
|
+
1. Fork the repository
|
|
2275
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
|
2276
|
+
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
|
|
2277
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
|
2278
|
+
5. Open a Pull Request
|
|
2279
|
+
|
|
2280
|
+
## License
|
|
2281
|
+
|
|
2282
|
+
This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
|
|
2283
|
+
|
|
2284
|
+
## Acknowledgments
|
|
2285
|
+
|
|
2286
|
+
- Based on concepts from combinatory logic and type theory
|
|
2287
|
+
- Inspired by minimal implicational logic models
|
|
2288
|
+
- Built with [Pydantic](https://pydantic-docs.helpmanual.io/) for data validation
|
|
2289
|
+
|
|
2290
|
+
## Citation
|
|
2291
|
+
|
|
2292
|
+
If you use this library in your research, please cite:
|
|
2293
|
+
|
|
2294
|
+
```bibtex
|
|
2295
|
+
@software{implica,
|
|
2296
|
+
author = {Carlos Fernandez},
|
|
2297
|
+
title = {Implica: Implicational Logic Graph Library},
|
|
2298
|
+
year = {2025},
|
|
2299
|
+
url = {https://github.com/CarlosFerLo/implicational-logic-graph}
|
|
2300
|
+
}
|
|
2301
|
+
```
|
|
2302
|
+
|