-
Notifications
You must be signed in to change notification settings - Fork 62
Pattern and destructuring blog post #64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,312 @@ | ||
| --- | ||
| slug: destruction-in-compact | ||
| title: Unpacking - A Guide to Patterns and Destructuring in Compact 📦 | ||
| authors: parisa | ||
| tags: [compact] | ||
| image: /img/blog/zkp.jpg | ||
| date: 2025-08-22 | ||
| --- | ||
| # Unpacking - A Guide to Patterns and Destructuring in Compact 📦 | ||
|
|
||
| Compact offers powerful features to streamline your contract development. | ||
| One such feature, pattern destruction, allows for elegant and efficient | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. destruction -> destructuring Here and everywhere below. |
||
| extraction of values from complex data structures like tuples and structures. | ||
| If you're coming from languages like TypeScript, you'll find the concept | ||
| familiar. This post will introduce you to pattern destruction, explain what | ||
| can be destructured, where it's permitted, and show you how to | ||
| leverage it in your Compact code. | ||
|
|
||
| Patterns and destructuring were introduced in | ||
| [Compact 0.14.0](#https://docs.midnight.network/relnotes/compact/compact-0-14-0#you-can-use-typescript-compatible-destructuring). | ||
| The code snippets provided in this blog post are written in Compact 0.17.0. | ||
| In code snippets a `demo` circuit is used to provide a context to demonstrate the | ||
| statements. In such cases, the meat is the statements inside the body of the `demo` circuit. | ||
|
|
||
| ## What is Pattern Destructuring? ✨ | ||
|
|
||
| At its core, pattern destruction in Compact is a syntactic convenience that | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. One thing I like to emphasize is that destructuring is more "declarative", as opposed to writing imperative code to traverse structures and extract subparts. It might be nice to say something like that but I don't know how to make it simple for people who don't know what "declarative" and "imperative" mean in this context. Sometimes I say that destructuring is a better alternative to writing code to extract subparts of data. But that's not quite correct (because you're still writing code, it's just better code). |
||
| allows you to **unpack** values from data structures (specifically tuples and | ||
| structures) and bind them to new identifiers in a single, concise program | ||
| element. Instead of accessing individual elements or fields one by one by | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would not use "program element" because element is being used for subparts of tuples and vectors. Perhaps it's a "concise program construct". |
||
| indexing into a tuple or a structure, | ||
| destructuring lets you define a pattern that mirrors the shape of the data, | ||
| directly assigning its components to identifiers. This makes your code cleaner, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we might avoid the term "assigning" here because it suggests mutation. How about "directly binding"? If you like it, search for all occurrences of "assign" and replace them. |
||
| more readable, and often less prone to errors. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Another benefit (I think) related to all those is that it's easier to maintain. When you change the definition of a type, you get compiler errors at the places that you need to update (you won't get those with imperative code that doesn't handle a new field, for instance). And the fix is often kind of small, simple and obvious compare to writing more statements. |
||
|
|
||
| ## What Can You Destructure? 🧩 | ||
|
|
||
| In Compact, pattern destructuring is primarily designed to be used with values of three key types: | ||
| **tuples**, **vectors**, and **structures**. Understanding these types is fundamental | ||
| to effectively using destructuring. | ||
|
|
||
| - Tuple type `[T, ...]`: Tuple types are ordered, heterogeneous | ||
| collections of types (e.g., `[Field, Boolean, Uint<8>]`). | ||
| Destructuring tuple values allows you to bind individual | ||
| elements to identifiers based on their position within a tuple value and typecheck | ||
| the actual element type with the declared type annotation if one exists. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the type checking (I prefer "type check" because "typecheck" feels too jargony to me: https://books.google.com/ngrams/graph?content=typecheck%2Ctype+check&year_start=1800&year_end=2022&corpus=en&smoothing=3) point needs to be elaborated here (or postponed). I don't think it's crystal clear what the "actual element type" is nor the "declared type annotation" (we haven't seen an example of destructuring). |
||
| - Vector type `Vector<n, T>`: Vector types are homogeneous tuple types | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might emphasize that vector types are a special case of tuples by putting that in a paragraph after this (now two bullet: vector and struct) list. You could say that this vector type is the same type as |
||
| (e.g., `Vector<3, Field>`). | ||
| Destructuring vector values is the same as destructuring tuple values since | ||
| vector values are constructed the same way tuple values are. | ||
| - Structure type `S { f: T ... }`: Structure types are user-defined types with | ||
| named fields (e.g., `struct Point { x: Field, y: Field }`). Destructuring | ||
| structure values lets you extract values of specific fields by their names and typecheck | ||
| the actual field's type with its declared type annotation if one exists. | ||
|
|
||
| ## Where Can You Use It? 📍 | ||
|
|
||
| Pattern destructuring isn't just for a single use case; it's integrated into | ||
| several key areas of Compact programming: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'd probably change "several" to "two" to avoid overselling it. The list below seems anticlimactic after reading "several". ;) |
||
|
|
||
| - Parameters of circuits, anonymous circuits, and a constructor can be a pattern. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Probably flip the order of these to emphasize that |
||
| - `const` binding statements can bind a pattern. This is arguably where you'll use destructuring | ||
| most frequently for local variable assignments, allowing you to unpack values | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think it's fine to use "variable", but probably not "assignment" here (try "local variable binding"). You might choose to avoid "variable" too and write "local constant binding"? |
||
| from expressions into new variables. | ||
|
|
||
| ## How to Destructure Values in Compact 🛠️ | ||
|
|
||
| Let's dive into the specifics of how to use tuple and structure patterns. | ||
| The grammar for a pattern `p` is provided in | ||
| [Compact's reference](https://docs.midnight.network/develop/reference/compact/lang-ref#parameters-patterns-and-destructuring). | ||
|
|
||
|
|
||
| ### Destructuring Tuple Values | ||
|
|
||
| Tuple values can be destructured using a list of patterns enclosed in `[` and `]`. | ||
| You can destructure a tuple into a sequence of identifiers: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hmmm. "A sequence of identifiers" sounds a little bit like a single thing that I might be able to manipulate. |
||
|
|
||
| ```compact | ||
| const [a, b] = [1, true]; // 'a' is 1, 'b' is true | ||
| ``` | ||
|
|
||
| Sometimes you don't need all the values in a tuple. Compact allows you to skip | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure what I think about "skip". It sounds very imperative! It's just not necessary to name all the elements. I'm not sure how I'd concretely change it (or if you even should).
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe "Sometimes you don't need to name all the elements in a tuple. Compact allows you to leave them unnamed by simply omitting the identifier..." |
||
| elements by simply omitting the identifier but you cannot skip the actual element: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You might say that this behavior is the same as TypeScript (at least, I think it is). That implicitly addresses a Compact criticism that this is weird and unnecessary (for people coming from languages like ML or Haskell). I think it's strange to say that you "cannot skip the actual element". In a sense, the pattern does skip (naming) the actual element. I think what you should say is that there is no equivalent for constructing tuple values. You can't leave off an (internal) element when constructing a tuple value. |
||
|
|
||
| ```compact | ||
| const [x, , z] = [10, 20, 30]; // 'x' is 10, 'z' is 30, 20 is skipped | ||
| // const [x, , z] = [10, , 30]; // triggers a parse error | ||
| ``` | ||
|
|
||
| Skipped identifiers count towards the length of the pattern if there exists a | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is the introduction of the fact that patterns have a length, but it's not used until much later. I suggest either introducing it later where it's needed, or elaborating here. |
||
| named identifier after them. For example, `[x, y, ,]` has length 2 but | ||
| `[x, , y]` has length 3. | ||
|
|
||
| :::note | ||
| If you want to enforce length 'l' of a tuple pattern you must have a named identifier | ||
| at the `l-1` index of your pattern. For example, if you need to enforce a tuple | ||
| pattern of legth 3 you need to have a named identifier on index 2 (e.g., all | ||
| `[, , x]`, `[, x, y]`, and `[x, y, z]` enforce a tuple pattern of length 3). | ||
| ::: | ||
|
|
||
| ### Type Annotations for Tuple Patterns | ||
|
|
||
| In a circuit definition, type declarations are required for tuple patterns: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We haven't seen tuple patterns for parameters yet, it might be better to introduce them earlier. I don't know if you want to spell it out, but it might be useful to give people the model that patterns for parameters are purely a convenience. Without it they could get exactly the same effect by immediately (or just before they need to) destructuring the parameter: As a convenience we allow them to move the pattern into the circuit declaration by replacing the right-hand side identifier in the destructuring
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. An alternative, which is a bigger rewrite, is to talk about everything in terms of const binding, and then only at the end talk about parameters as a special case of const binding. |
||
|
|
||
| ```compact | ||
| circuit exampleTuplePattern([x, y]: [Field, Boolean]): Field { | ||
| // x has type Field, y has type Boolean | ||
| return x; | ||
| } | ||
| ``` | ||
|
|
||
| Such a type declaration dictates the structure and the type of each element. For example, | ||
| in `exampleTuplePattern` the input has to be a tuple, it first element has to be a value | ||
| of type `Field`, and its second element has to be a value of type `Boolean`. | ||
| A call to `exampleTuplePattern` that does not meet these restrictions | ||
| (e.g., `exampleTuplePattern([1, 1])`) will trigger a type error. | ||
|
|
||
| For const bindings and anonymous circuits, type annotations are optional but recommended | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure I agree with this recommendation. I would personally favor a style that left those types off, because they are kind of verbose and distracting or because I don't care to work out exactly what they are and have to update type annotations every time I do a benign refactoring. (Would we make the same recommendation for const binding in general, no patterns?) |
||
| for clarity and an easier debugging experience. When a type annotation is provided | ||
| ensure its structure aligns with the pattern. For example, `[a, , b] : [Field, Field, Field]` | ||
| aligns the structure of the pattern and its type whereas `[a, , b] : [Field, Field]` does not: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The second of these is a type error, isn't it? The sentence reads like this is a style recommendation, but I think (hope) it's a requirement. |
||
| the pattern states that it is a tuple with three elements but the type annotation is | ||
| tuple type of length two. | ||
|
|
||
| Unlike patterns, types do not allow skipping elements in a tuple type. For example, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is an interesting one. What is the motivation? To me, I feel like I ought to be able to do that (omit a type for an unnamed element, I certainly can't refer to it so why do I care what the type is? If that type changes on the right-hand side, I really don't want to have to update this type annotation for the value I don't care about.) In fact, I'd actually like to be able to have type inference for some elements and write: Where the type annotations are checked and the missing one is inferred from the return type of
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I feel like restrictions like this are important to justify a little bit from a design standpoint. Is there a technical reason we need it? Do we just like it that way? Is it something that we might change? |
||
| `[a, , b] : [Field, , Field]` causes a parse error. So even when you're skipping an | ||
| element in a pattern you must assign it a dummy type. Unfortunately, you have to | ||
| remember what dummy type you used when you're passing arguments to pattern parameters. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think I would call this a "dummy type". You have to pick a concrete type. What you might want to do is make this circuit generic: And then you have to specialize it correctly at the call site. |
||
| For example, | ||
|
|
||
| ```compact | ||
| circuit haveToUseDummyType([, x]: [Field, Boolean]): Boolean { | ||
| return x; | ||
| } | ||
|
|
||
| export circuit callSite(): Boolean { | ||
| return haveToUseDummyType([1, true]); | ||
|
|
||
| // return haveToUseDummyType([true, true]); | ||
| // triggers a type error since the first element must be | ||
| // a value of type Field | ||
| } | ||
| ``` | ||
|
|
||
| :::note | ||
| We recommend you using a sepecific type as your dummy type throughout all your Compact | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't understand this completely. Following the recommendation, the function works to extract the second element of a tuple of type
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't see the usefulness of this kind of code. I would refactor it so |
||
| code or use a type synonym for such a dummy type when such a feature is available. For | ||
| example, always use `Boolean` as your dummy type. | ||
| ::: | ||
|
|
||
| Similar to tuple types, skipped elements of a tuple value cannot be dropped, for example, | ||
| `const [x, , y] : [Field, Field, Field] = [1, , 3];` triggers a type error. | ||
|
|
||
| When a type annotation exists, the length of the tuple pattern must be less than or equal to | ||
| the length of its tuple type annotation. | ||
| For example, `[a, b]: [Field, Field]` typechecks. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I probably wouldn't say that this type checks (whether it does or not depends on what is on the left-hand side or what is given as the argument at a call site). It's just not a syntax error (and the third one below is a syntax error). It's a structural check. |
||
| So does `[, a]: [Field, Field]`. So does `[a, , , , ,] : [Field, Field]`. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think this is best explained as: we allow a trailing comma in tuples because that can be convenient. We treat any number of trailing commas the same as a single one. So the pattern
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And then, after removing the trailing commas, you introduce fresh identifiers for all the missing elements. So |
||
| But `[, , a] : [Field, Field]` does not since `[, , a]` has length 3 and `[Field, Field]` has length 2. | ||
|
|
||
| Tuple patterns have some flexibility even when they're accompanied by a type annotation. But once they | ||
| have a type annotation the actual binding of the tuple pattern must satisfy the type annotation, | ||
| otherwise it is a static type error. For example, | ||
|
|
||
| ```compact | ||
| export circuit demo(): [] { | ||
| const [x, y] = [1, 2, 3]; // 'x' is 1, 'y' is 2 | ||
| const [, y, , , ] = [1, 2, 3]; // 'y' is 2 | ||
|
|
||
| // const [, y, , , ] : [Field, Field] = [1, 2, 3]; | ||
| // triggers a type error | ||
|
|
||
| const [, y, , , ] : [Field, Field] = [1, 2]; // 'y' is 2 | ||
| } | ||
| ``` | ||
|
|
||
| Same applies when calling a circuit that uses tuple pattern parameters in its definition: | ||
|
|
||
| ```compact | ||
| circuit snd([, b, , ] : [Boolean, Boolean, Field]): Boolean { | ||
| return b; | ||
| } | ||
|
|
||
| export circuit call_snd() : Boolean { // returns true | ||
| return foo([false, true, 1]); | ||
| } | ||
|
|
||
| // export circuit call_snd_bad() : Boolean { | ||
| // return foo([false, true, 1, 1]); | ||
| // } | ||
| // 'call_snd_bad' triggers a type error since 'foo' takes a tuple of three | ||
| // where the first two elements are Booleans and the last one is a Field | ||
| // but it is passed a tuple of four where the first two elements are | ||
| // Booleans and the last two are Fields | ||
| ``` | ||
|
|
||
| ### Destructuring Structure Values | ||
|
|
||
| Structure values are destructured using a list of fields enclosed in `{` and `}`. | ||
|
|
||
| You can extract fields by their names. The order of fields in the pattern doesn't matter: | ||
|
|
||
| ```compact | ||
| struct Point { x: Field, y: Field } | ||
|
|
||
| export circuit demo(): [] { | ||
| const myPoint = Point { x: 10, y: 20 }; | ||
| const { y, x } = myPoint; // 'y' is 20, 'x' is 10 | ||
| } | ||
| ``` | ||
|
|
||
| You can rename a field in a pattern by using the `f: id` syntax: | ||
|
|
||
| ```compact | ||
| struct Person { name: Bytes<16>, age: Uint<8> } | ||
|
|
||
| export circuit demo(): [] { | ||
| const p = Person { name: "Alice", age: 30 }; | ||
| const { name: fullName, age } = p; | ||
| // 'fullName' is "Alice", 'age' is 30, 'name' is unbound | ||
| } | ||
| ``` | ||
|
|
||
| It's a static type error to bind the same identifier more than once in a single destructuring | ||
| block (e.g., `{name: age, age}` is a static type error). If you rename a field, | ||
| you can't refer to the original field name within the scope of the pattern | ||
| (e.g., `({a: b} : S) => { return a; }` would trigger an `unbound identifier a` error). | ||
|
|
||
| ### Type Annotations for Structure Patterns | ||
|
|
||
| Similar to tuples, type annotations are required for structure pattern parameters in circuit definitions: | ||
|
|
||
| ```compact | ||
| struct Config { version: Uint<8>, isActive: Boolean } | ||
| circuit processConfig({ version, isActive }: Config): Boolean { | ||
| // version has type Uint<8>, isActive has type Boolean | ||
| return isActive; | ||
| } | ||
| ``` | ||
|
|
||
| For `const` bindings and anonymous circuits, type annotation for structure patterns | ||
| (e.g., `const {a, b} : S = ...`) is optional. It's a static type error if the provided | ||
| type `T` is not a structure type or if a field in the pattern doesn't exist in `T`. | ||
| However, you don't need to bind all fields of `T`. | ||
|
|
||
| ### Nested Patterns | ||
|
|
||
| You can even have nested tuple patterns. For example | ||
| `const [outer1, [inner1, inner2]] = [1, [2, 3]];` | ||
| binds 'outer1' to 1, 'inner1' to 2, and 'inner2' to 3. | ||
|
|
||
| If you provide a type annotation for the outer tuple, | ||
| like `[outer1, [inner1, inner2]] : [Field, [Uint<8>, Uint<8>]]`, | ||
| it's a static type error if a pattern like `[inner1, inner2]` doesn't correspond to a tuple type | ||
| in the type annotation. | ||
|
|
||
|
|
||
| You can also use nested patterns for structure values. This is | ||
| particularly powerful for complex data: | ||
|
|
||
| ```compact | ||
| enum Material { | ||
| wood, | ||
| glass, | ||
| steel | ||
| } | ||
|
|
||
| struct Box { | ||
| dimensions: [Field, Field, Field], | ||
| material: Material | ||
| } | ||
|
|
||
| export circuit demo(): [] { | ||
| const myBox = Box { dimensions: [1, 2, 3], material: Material.wood }; | ||
| const { dimensions: [length, width, height], material } = myBox; | ||
| // 'length' is 1, 'width' is 2, 'height' is 3, 'material' is 'wood' | ||
| // 'dimensions' is unbound | ||
| } | ||
| ``` | ||
|
|
||
| If a field in a structure pattern (e.g., `dimensions: [length, width, height]`) is a tuple or | ||
| structure pattern itself, its pattern must match the type of the field in the declared structure | ||
| type. For example, given the `Material` and `Box` types defined above: | ||
|
|
||
| ```compact | ||
| // export circuit demo_bad(): [] { | ||
| // const { dimensions: [x, , , z] } = Box { [1, 2, 3], Material.wood}; | ||
| // this triggers a type error since the pattern expect 'dimensions' | ||
| // to be a tuple of four elements but based on the declaration of | ||
| // 'Box' it's a tuple of three Fields | ||
| //} | ||
|
|
||
| export circuit demo_tricky(): [] { | ||
| const { dimensions: [x, z, , , ] } = Box { [1, 2, 3], Material.wood}; | ||
| // 'x' is 1, 'z' is 2, 'dimensions' is unbound | ||
| // you should know by now why this compiles but 'demo_bad' doesn't 🙂 | ||
| } | ||
| ``` | ||
|
|
||
| ## Putting It All Together 🧑💻 | ||
| Pattern destructuring is an indispensable feature in Compact that significantly | ||
| enhances code readability and conciseness when working with tuples and structures. | ||
| Even with its current limitations (forcing you to use a dummy type when skipping | ||
| an element in a tuple pattern and considering the length of a tuple pattern up | ||
| to its last unskipped sub-pattern) it allows you to reach into tuple and structure | ||
| values without indexing. This is exteremly powerful when working with `const`, | ||
| threading through the output of a circuit as a tuple to another circuit for | ||
| further processing, and functionally processing vectors/tuples | ||
| (e.g., `fold((x: Field, [a, b]: [Field, Field]): Field => a + b + x, 0, [[1, 2], [2, 3], [3, 4]]);` | ||
| sums up elements of a vector of tuples of two Fields very simply). | ||
| By understanding where and how to use it, you can write more expressive and | ||
| maintainable smart contracts. Experiment with these patterns in your Compact code | ||
| to fully grasp their utility and make your contract development more efficient! | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This will be in the URL. I'd change it to
destructuring-in-compactorcompact-destructuring.("Destruction" sounds like the opposite of construction, which is sort of is but maybe that's misleading.)