|
1 | 1 | # Standard Library |
2 | 2 |
|
3 | | -Grain aims to have a comprehensive and consistent standard library. |
| 3 | +Grain aims to cultivate a **comprehensive and consistent standard library**. |
4 | 4 |
|
5 | | -To us, this means that someone should be able to reach for the standard library to perform most of their day-to-day work, and it will always work the way they expect in the context of Grain. |
| 5 | +To us, this means that users should be able to reach for the standard library to perform most day-to-day tasks, and it should behave predictably within the context of Grain. |
6 | 6 |
|
7 | 7 | ## Guidelines |
8 | 8 |
|
9 | | -Here are some guidelines for making additions to the standard library while also keeping it high-quality and consistent! |
| 9 | +Here are some guidelines for making additions to the standard library while maintaining a high-quality and consistent user experience. |
10 | 10 |
|
11 | | -1. New data types should exist in their own modules. For example, the `Range` enum is the data type exported from `range.gr`. An exception to this are data types that are ubiquitous (`Option`/`Result`/`List`), which should live in `pervasives.gr`. |
12 | | -1. Prefer data constructors when possible. Only use separate constructor functions, like `make` or `init`, if needed to set an initialization value or to hide internals of your data structure. |
13 | | -1. All functions that operate on a data type should exist in the same module as that data type. For example, all `Map` methods exist in the `map.gr` file. |
14 | | -1. Fallible functions should almost always return an `Option` or `Result`. Usage of `fail` should be reserved for exceptional cases, such as argument validation producing an index out-of-bounds failure. |
15 | | - * `Option` should be preferred if you might or might not be able to get some value. Typically, these would be functions that could return `null` in other languages when they didn't produce a value. For example, `List.find` returns `None` when no item in the list matches the condition. |
16 | | - * `Result` is useful if you have multiple failures, or if the consumer might need additional context around the failure. Typically, these functions would throw exceptions in other languages. For example, `Number.parseInt` returns `Err(reason)` when it fails to parse a string into an integer. |
17 | | -1. If possible, keep dependencies on other standard library modules to a minimum. For example, `Array` re-implements some simple `List` operations to avoid depending on the entire `List` module. |
| 11 | +### Scope |
| 12 | + |
| 13 | +The standard library is intended to support common day-to-day needs of Grain developers while remaining reliable, and easy to maintain. Standard library modules include functionality that: |
| 14 | + |
| 15 | +- Serves a broad set of programs |
| 16 | +- Enhances the developer experience by being ergonomic and consistent |
| 17 | +- Works reliably in standard WebAssembly and [WASI](https://github.com/WebAssembly/WASI) environments |
| 18 | +- Avoids unnecessary complexity and maintenance burdens |
| 19 | + |
| 20 | +If a feature fits naturally into everyday development, improves clarity or safety, and can be implemented cleanly within these constraints, it might fit well into the standard library. For example, while the `Json` and `Regexp` modules are relatively complex, they are ubiquitous and require little ongoing work to maintain. In contrast, something more niche like a `Protobuf` module is likely better off as a community library as it evolves independently and requiring active maintenance, check out [awesome-grain](https://github.com/grain-lang/awesome-grain) for some community libraries. |
| 21 | + |
| 22 | +### Organization |
| 23 | + |
| 24 | +A consistent and well-structured standard library makes it easier for developers to find what they need and understand how to use it. In order to keep things clean and predictable, we organize the standard library around clear, single-purpose modules. |
| 25 | + |
| 26 | +Each module should represent a singular focused concept, like a data structure (i.e. `Map`, `Set`) or utility (i.e. `Marshal`, `Json`). Data types should live within their respective modules unless they are ubiquitous and require deep language integration such as `Option`, `Result`, `List` or `Range` which live either directly in the compiler, or `Pervasives`. |
| 27 | + |
| 28 | +Within an individual module exports should be grouped by functionality, with common exports being closer to the top of the module. Modified functionality such as immutable variants (i.e `Map.Immutable`, `Set.Immutable`) should exist within submodules. |
| 29 | + |
| 30 | +The goal is to make the standard library intuitive to explore and easy to maintain, |
| 31 | +with as little surprise as possible for contributors and users alike. |
| 32 | + |
| 33 | +### Testing |
| 34 | + |
| 35 | +Every exposed function or module in the standard library should be thoroughly tested to ensure reliability, correctness, and predictability. Good testing helps to prevent regressions as features and changes are introduced. Tests exist in `../../compiler/test/stdlib/<module>.test.gr` when adding a new module, make sure to add `assertStdlib("<module>.test");` to `../../compiler/test/suites/stdlib.re`. |
| 36 | + |
| 37 | +Testing should focus on: |
| 38 | +- **Correctness**: Ensure the function or module behaves as expected under both normal and edge cases |
| 39 | + - The `Number` library is a great example where we test each unique `Number` layout along with various edge cases |
| 40 | +- **Consistency**: Ensure the behavior of a function or module is consistent across different environments (i.e. `Windows`, Mac`, `Linux`) |
| 41 | +- **Simplicity**: Keep tests straightforward and focused. Each test should try to verify a single, simple behavior |
| 42 | +- **Methodical**: Tests should be ordered in a methodical manner isolating failures |
| 43 | + - As an example, if you are testing `Uint8` ensure you test `Uint8.(==)` before using it within other tests so we can quickly identify failures |
| 44 | + |
| 45 | +A solid test suite helps to maintain the quality and stability of the standard library as a whole, while providing confidence that behaviors are consistent and reliable. |
| 46 | + |
| 47 | +### Documentation |
| 48 | + |
| 49 | +Every exposed `type` `module` and `value` **must** include a Graindoc docblock, [Documentation can be found here](https://grain-lang.org/docs/tooling/graindoc). As we strive for consistency and clarity, a great starting place for new documentation is finding existing documentation with similar functionality and adapting it your needs. We haven't gotten around to documenting all of our documentation patterns yet, but a non-inclusive list can be found [here](https://github.com/grain-lang/grain/issues/828). |
| 50 | + |
| 51 | +### Common Patterns |
| 52 | + |
| 53 | +#### Fallible Functions |
| 54 | + |
| 55 | +Fallible functions (functions that may fail), should almost always return either an `Option` or `Result`. |
| 56 | + |
| 57 | +- `Option` is preferred when a function may or may not return a value |
| 58 | + - These are cases where `null` might typically be returned in other languages |
| 59 | + - Example: `List.find` which returns `Some(index)` containing the index of the first element found or `None` otherwise |
| 60 | +- `Result` is preferred when a function may fail |
| 61 | + - There are numerous failure modes |
| 62 | + - The caller might need more information about what went wrong |
| 63 | + - Example: `Number.parseInt` which returns `Ok(value)` containing the parsed number on a successful parse or `Err(err)` containing a variant of `ParseIntError` |
| 64 | +- `throw`/`fail` is reserved for rare edge cases |
| 65 | + - The failure is very rare and recovery is unlikely |
| 66 | + - Grain doesn't yet have exception handling, meaning users cannot recover when these occur |
| 67 | + - Example: `Number.(/)` which may throw `DivisionByZero` if you divide by zero |
0 commit comments