-
Notifications
You must be signed in to change notification settings - Fork 62
Basic documentation for Struct Classes #603
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
Open
JeffersGlass
wants to merge
7
commits into
spylang:main
Choose a base branch
from
JeffersGlass:docs-struct-basics
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
d719102
Add basic documentation for struct classes
JeffersGlass c2b3f5e
'struct class' -> 'struct'
JeffersGlass e073d20
Apply suggestions
JeffersGlass 7d134d8
Add not on structs being passed by value
JeffersGlass 8632f6e
Add section on pointers to structers being mutable
JeffersGlass 49dbbae
Add clarifying note
JeffersGlass 9ad9fbd
Add description of the pattern
JeffersGlass File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,221 @@ | ||
| title: Structs | ||
| --- | ||
|
|
||
| Structs (currently the only classes in SPy) are immutable data structures analogous to C structs. | ||
|
|
||
| /// warning | ||
| Class construction and layout are a part of SPy that's rapidly evolving. All of the constructs, names, functions, or decorators here are likely to change! | ||
| /// | ||
|
|
||
| ## Declaration | ||
|
|
||
| Structs are declared with the `@struct` decorator on a CPython-style class definition. Their fixed list of fields follows with type annotations. Note that default values for these fields are not currently supported. | ||
|
|
||
| ```py | ||
| @struct | ||
| class Person: | ||
| name: str | ||
| age: int | ||
|
|
||
| def main() -> None: | ||
| p = Person('Alice', 99) | ||
| print(p.name, "is", p.age, "years old") # Alice is 99 years old | ||
|
|
||
| ``` | ||
|
|
||
| Structs may also be defined with the [generic class syntax](../howto/generics.md#generic-class-syntax), which is syntactic sugar for a generic function which defines an internal struct. See the [generics](../howto/generics.md) documentation for more info. | ||
|
|
||
| ## Attributes | ||
|
|
||
| Structs are shallow immutable. Attributes cannot be set after creation, nor can new attributes be assigned to an instance of a struct class after creation: | ||
|
|
||
| ```py | ||
| @struct | ||
| class Person: | ||
| name: str | ||
| age: int | ||
|
|
||
| def main() -> None: | ||
| p = Person('Alice', 99) | ||
| p.name = 'Bob' # TypeError: type `Person` does not support assignment to attribute 'name' | ||
| p.height = 190 # TypeError: type `Person` does not support assignment to attribute 'height' | ||
| ``` | ||
|
|
||
|
|
||
|
|
||
| However, objects which are attributes of struct classes may be mutated. | ||
|
|
||
| ```py | ||
| @struct | ||
| class Person: | ||
| name: str | ||
| age: int | ||
| books: list[str] | ||
|
|
||
|
|
||
| def main() -> None: | ||
| p = Person('Alice', 99, []) | ||
| print(p.name, "has", len(p.books), "book(s)") # Alice has 0 book(s) | ||
|
|
||
| p.books.append("An Introduction to Python") | ||
| print(p.name, "has", len(p.books), "book(s)") # Alice has 1 book(s) | ||
| ``` | ||
|
|
||
| If the struct attribute is a pointer to an object, that object at that pointer may also be mutated. (See [Constructors](#constructors) below for info on the `__new__` method): | ||
|
|
||
| <!-- TODO once there is reference documentation on pointers/memory, link to it here --> | ||
|
|
||
| ```py | ||
| from unsafe import gc_ptr, gc_alloc | ||
|
|
||
| @struct | ||
| class Person: | ||
| name: str | ||
| age: gc_ptr[int] | ||
|
|
||
| def __new__(name: str, age: int) -> Person: | ||
| _age = gc_alloc[int](1) | ||
| _age[0] = age | ||
| return Person.__make__(name, _age) | ||
|
|
||
| def do_birthday(self) -> None: | ||
| self.age[0] = self.age[0] + 1 | ||
|
|
||
|
|
||
| def main() -> None: | ||
| p = Person('Alice', 99) | ||
| print(p.name, "is", p.age[0], "years old") # Alice is 99 years old | ||
|
|
||
| p.do_birthday() | ||
| print(p.name, "is", p.age[0], "years old") # Alice is 100 years old | ||
| ``` | ||
|
|
||
| ## Pointers to Structs | ||
|
|
||
| If a struct is manually allocated using `gc_alloc` or similar, its attributes can be mutated. See the [low level memory docuementary on heap-allocated structs](../llmem.md#heap-allocated-structs) for more info. | ||
|
|
||
| ```py | ||
| from unsafe import gc_alloc, gc_ptr | ||
|
|
||
| @struct | ||
| class Point: | ||
| x: int | ||
|
|
||
| # spy build foo.spy | ||
| def main() -> None: | ||
| p = gc_alloc[Point](1) | ||
| p.x = 1 | ||
| print(p.x) | ||
| p.x = 2 | ||
| print(p.x) | ||
| ``` | ||
|
|
||
| ## Constructors | ||
|
|
||
| Structs use a default constructor which populates all their attributes in-order, and all attributes must be provided each time the default constructor is called. E.g. the example above, the constructor `p = Person('Alice', 99, [])` requires an empty list to be passed for the `books` attribute. We can call this constructor explicitly using the `__make__` method: | ||
|
|
||
| ```py | ||
| @struct | ||
| class Person: | ||
| name: str | ||
| age: int | ||
| books: list[str] | ||
|
|
||
| def main() -> None: | ||
| p = Person.__make__('Bob', 6, []) | ||
| print(p.age) # 6 | ||
| ``` | ||
|
|
||
| User-facing constructors can be customized by overwriting the `__new__` method; the `__make__` must be called within to handle the initialization of the complete struct. Note that, as in Python, `__new__` functions like a staticmethod (it does not take a `self` parameter) | ||
|
|
||
| ```py | ||
| @struct | ||
| class Person: | ||
| name: str | ||
| age: int | ||
| books: list[str] | ||
|
|
||
| def __new__(name: str, age: int) -> Person: | ||
| _books: list[str] = [] | ||
| return Person.__make__(name, age, _books) | ||
|
|
||
| def main() -> None: | ||
| p = Person('Alice', 99) | ||
| print(p.name, "has", len(p.books), "books") # Alice has 0 books | ||
| ``` | ||
|
|
||
| By convention, if a struct is simply being used to wrap a lower-level type (to add constructors, methods, etc), that lower level type is denoted as `__ll__`. | ||
|
|
||
| See the [smallpoint example](https://github.com/spylang/spy/examples/3_low_level/smallpoint.spy) for a more in-depth example and explanation. See the implementations of the [list](https://github.com/spylang/spy/stdlib/_list.spy), [dict](https://github.com/spylang/spy/stdlib/_dict.spy), and [file](https://github.com/spylang/spy/stdlib/_file.spy) objects in spy for usage of this pattern in the SPy standard library. | ||
|
|
||
| /// info | ||
| Eventually, the `__ll__` notation may likely be special-cased to make it a truly private attribute within the struct. Currently, it is just a naming convetion. | ||
| /// | ||
|
|
||
| ```py | ||
| from unsafe import gc_ptr, gc_alloc | ||
|
|
||
| @struct | ||
| class BookData: | ||
| name: str | ||
| stars: int # out of 10 | ||
|
|
||
| @struct | ||
| class Book: | ||
| __ll__: gc_ptr[BookData] | ||
|
|
||
| def __new__(name: str, stars: float) -> Book: | ||
| data = gc_alloc[BookData](1) | ||
| data.name = name | ||
| data.stars = stars | ||
| return Book.__make__(data) | ||
|
|
||
| def plus_one_star(self) -> None: | ||
| self.__ll__.stars = self.__ll__.stars + 1.0 | ||
|
|
||
| def __repr__(self) -> str: | ||
| return "Book(name='" + self.__ll__.name + "', stars=" + str(self.__ll__.stars) + ")" | ||
|
|
||
|
|
||
| def main() -> None: | ||
| b = Book("An Introduction to Python", 10.0) | ||
| print(b) # Book(name='An Introduction to Python', stars=10.0) | ||
| b.plus_one_star() # Book(name='An Introduction to Python', stars=11.0) | ||
| print(b) | ||
| ``` | ||
|
|
||
| <!-- | ||
| TODO Add notes about metafuncs as constructors. This may be more generally useful once typing for things | ||
| like `str | None` is available. | ||
| --> | ||
|
|
||
| ## Methods | ||
|
|
||
| Structs may have methods defined inside their class body; the struct object itself is passed as the first parameter (usually called `self`), just as in CPython: | ||
|
|
||
| ```py | ||
| @struct | ||
| class Person: | ||
| name: str | ||
|
|
||
| def say_hi_to(self, other: Person) -> None: | ||
| print("Hi " + other.name + "! My name is " + self.name) | ||
|
|
||
| def is_teenager(self) -> bool: | ||
| return 13 <= self.age and self.age <= 19 | ||
|
|
||
| def main() -> None: | ||
| c = Person("Charlie", 55) | ||
| c.say_hi_to(Person("Donna", 57)) # Hi Donna! My name is Charlie | ||
| print(c.is_teenager()) # False | ||
| ``` | ||
|
|
||
| Methods may also be [blue functions](../reference/spy_builtin_functions.md#blue), [generic blue functions](../reference/spy_builtin_functions.md#bluegeneric), or [metafunctions](../reference/spy_builtin_functions.md#bluemetafunc). | ||
|
|
||
| ## Inheritance | ||
|
|
||
| Inheriting from a base class is not currently implemented in SPy. | ||
|
|
||
| ## Structs as Arguments | ||
|
|
||
| When used as a function argument, Structs are passed by value, meaning a copy of the struct is created for each call. The performance implications of this should be kept in mind for large structs. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
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.
What about something like this: