feat(manual): wrap all code in figures

This commit is contained in:
2026-06-28 22:20:15 +02:00
parent 4b2b0fe476
commit 759b416bf3

View File

@@ -141,7 +141,7 @@ This command will generate a file as shown in @qs-stubs, providing stub classes
x: Meter x: Meter
y: Meter y: Meter
```, ```,
caption: [Generated stubs], caption: [Generated stubs from example definitions of @qs-midas],
) <qs-stubs> ) <qs-stubs>
== Using Midas in Python == Using Midas in Python
@@ -207,86 +207,116 @@ A `*.midas` file contains a number of statements, which can be:
A *`type`* statement lets you define a new type. It requires a unique name and base type. A *`type`* statement lets you define a new type. It requires a unique name and base type.
The simplest form of a *`type`* statement is: The simplest form of a *`type`* statement is:
```midas #figure(
type MyType = float ```midas
``` type MyType = float
```,
caption: [Simple `type` statement declaring a new type "`MyType`" as a subtype of `float`],
) <midas-simple-alias>
This statement defines a new type called `MyType` which is a subtype of `float`. `MyType` is a `float` but a `float` is not necessarily `MyType`. This statement defines a new type called `MyType` which is a subtype of `float`. `MyType` is a `float` but a `float` is not necessarily `MyType`.
=== Builtin / base types === Builtin / base types
A number of base types are provided out of the box. They correspond to Python's builtin types: A number of base types are provided out of the box, which can be used to derive other types.
- `object`
- `str` They correspond to Python's builtin types:
- `float` ```py object```,
- `int` ```py str```,
- `bool` ```py float```,
- `list` ```py int```,
- `dict` ```py bool```,
- `None` ```py list```,
```py dict```,
```py None```.
Some differences are to be noted however. Some differences are to be noted however.
1. `bool` is not a subtype of `int` 1. ```py bool``` is not a subtype of ```py int```
2. `list` are homogeneous, i.e. all items must be of the same type 2. ```py list``` are homogeneous, i.e. all items must be of the same type
3. `dict` keys and values are homogeneous, i.e. all keys must be of the same type and all values must be of the same type (can be different from keys). 3. ```py dict``` keys and values are homogeneous, i.e. all keys must be of the same type and all values must be of the same type (can be different from keys).
=== Function types === Function types
A function type is written in a similar notation to Python function definitions: A function type is written in a similar notation to Python function definitions:
```midas #figure(
type Repeater = fn(text: str, count: int) -> str ```midas
``` type Repeater = fn(text: str, count: int) -> str
```,
caption: [Simple function type definition],
)
Midas supports positional-only, keyword-only and mixed arguments (using the `/` and `*` separators). You may omit the name of positional-only arguments. The return type is required. Midas supports positional-only, keyword-only and mixed arguments (using the `/` and `*` separators). You may omit the name of positional-only arguments. The return type is required.
Optional parameters can be indicated by adding a question mark (`?`) after their type: Optional parameters can be indicated by adding a question mark (`?`) after their type:
```midas #figure(
type Repeater = fn(text: str, count: int, *, sep: str?) -> str ```midas
``` type Repeater = fn(text: str, count: int, *, sep: str?) -> str
```,
caption: [Function type definition with an optional keyword-only parameter],
)
#gc.warning[ #gc.warning[
Sink arguments (`*args`, `**kwargs`) are not currently supported. Sink arguments (`*args`, `**kwargs`) are not currently supported.
] ]
=== Constraint types
A useful feature provided by Midas is the possibility to combine types with custom value constraints. For example, you might want to define a type for positive amounts of money:
#figure(
```midas
type Money = float
type Income = Money where _ >= 0
```,
caption: [Simple constraint type definition],
)
Constraints can be combined with any type using the `where` keyword, followed by a constraint expression (see @constraint-expr).
=== Generic types === Generic types
For more complex types, you might want to use type parameters. For example, to define a container, we might write: For more complex types, you might want to use type parameters. For example, to define a container, we might write:
```midas #figure(
type Container[T] = object ```midas
``` type Container[T] = object
```,
caption: [Simple generic container type definition],
)
To better refine a generic type, you can also bound type parameters using the following syntax: To better refine a generic type, you can also bound type parameters using the following syntax:
```midas #figure(
type Container[T <: float] = object ```midas
``` type Container[T <: float] = object
```,
caption: [Generic container type definition with a bound],
)
This can be read as "`Container` is a generic type which takes one type parameter `T` that must be a subtype of `float`". This can be read as "`Container` is a generic type which takes one type parameter `T` that must be a subtype of `float`".
You can use a generic type, i.e. instantiate it, by using a similar syntax with concrete type as arguments: You can use a generic type, i.e. instantiate it, by using a similar syntax with concrete type as arguments:
```midas #figure(
type MyContainer = Container[MyType] ```midas
``` type MyContainer = Container[MyType]
```,
caption: [Application of a generic type],
)
Generic types can also take multiple parameters, which are then separated by commas: Generic types can also take multiple parameters, which are then separated by commas:
```midas #figure(
type ZipCodeRegistry = dict[int, str] ```midas
``` type ZipCodeRegistry = dict[int, str]
```,
caption: [Application of a multi-parameter generic type],
)
The _body_ of a generic type, i.e. the right-hand side of the definition, can contain or even be equal to any number of its parameters.#footnote[The latter is not something that is expressible in standard Python, yet it brings a semantic distinction on top of structurally equivalent values.] For example, the following is a valid type statement: The _body_ of a generic type, i.e. the right-hand side of the definition, can contain or even be equal to any number of its parameters.#footnote[The latter is not something that is expressible in standard Python, yet it brings a semantic distinction on top of structurally equivalent values.] For example, the following is a valid type statement:
```midas #figure(
type Price[T <: Currency] = T where _ > 0 ```midas
``` type Price[T <: Currency] = T where _ > 0
```,
=== Constraint types caption: [Type parameters in a generic type's body],
)
A useful feature provided by Midas is the possibility to combine types with custom value constraints. For example, you might want to define a type for positive amounts of money:
```midas
type Money = float
type Income = Money where _ >= 0
```
Constraints can be combined with any type using the `where` keyword, followed by a constraint expression (see @constraint-expr).
#pagebreak() #pagebreak()
@@ -297,33 +327,42 @@ Type statements allow you to define new types, kind of like type aliases. Howeve
This is where the `extend` statement comes into play. It allows defining members on a given type. Members can either be properties (`prop`) or methods (`def`). The only difference between the two is that methods must be functions and can be overloaded. This is where the `extend` statement comes into play. It allows defining members on a given type. Members can either be properties (`prop`) or methods (`def`). The only difference between the two is that methods must be functions and can be overloaded.
Here is a simple example showing how to define a property and a method on a custom type: Here is a simple example showing how to define a property and a method on a custom type:
```midas #figure(
type MyType = float ```midas
extend MyType { type MyType = float
prop norm: float extend MyType {
def double: fn() -> MyType prop norm: float
} def double: fn() -> MyType
``` }
```,
caption: [Simple `extend` statement defining a property and a method],
)
An `extend` statement can appear anywhere after the type it extends has been defined. An `extend` statement can appear anywhere after the type it extends has been defined.
You may want to overide Python's dunder methods to implement type checking for some basic operators, like `__add__` for the `+` operator. You may want to override Python's dunder methods to implement type checking for some basic operators, like `__add__` for the `+` operator.
```midas #figure(
type Money = float ```midas
extend Money { type Money = float
def __add__(Money, /) -> Money extend Money {
def __mul__(float, /) -> Money def __add__(Money, /) -> Money
} def __mul__(float, /) -> Money
``` }
```,
caption: [Simple `extend` statement overriding some dunder methods],
)
When extending generic type, you must specify the whole type, including its parameter(s): When extending generic type, you must specify the whole type, including its parameter(s):
```midas #figure(
type Container[T <: float] = object ```midas
extend Container[T <: float] { type Container[T <: float] = object
prop content: T extend Container[T <: float] {
def set_content: fn(content: T) -> None prop content: T
} def set_content: fn(content: T) -> None
``` }
```,
caption: [Generic `extend` statement using type parameters in the declared members],
)
#pagebreak() #pagebreak()
@@ -332,25 +371,37 @@ extend Container[T <: float] {
A *`predicate`* statement lets you define a named constraint expression, like a function, which can then be used in other constraint expressions (either in other predicate statements or in constraint types). See @constraint-expr for more information about the syntax of constraint expressions. A *`predicate`* statement lets you define a named constraint expression, like a function, which can then be used in other constraint expressions (either in other predicate statements or in constraint types). See @constraint-expr for more information about the syntax of constraint expressions.
The left-hand side of a predicate statement is written as a function signature, without a return type. The right-hand side is a constraint expression. For example: The left-hand side of a predicate statement is written as a function signature, without a return type. The right-hand side is a constraint expression. For example:
```midas #figure(
predicate is_positive(v: float) = v >= 0 ```midas
``` predicate is_positive(v: float) = v >= 0
```,
caption: [Simple `predicate` statement defining an `is_positive` predicate],
)
The left-hand side can also be curried to allow partial application. For example: The left-hand side can also be curried to allow partial application. For example:
```midas #figure(
predicate in_range(mn: float, mx: float)(v: float) = mn <= v & v <= mx ```midas
predicate is_ratio = in_range(0.0, 1.0) predicate in_range(mn: float, mx: float)(v: float) = mn <= v & v <= mx
``` predicate is_ratio = in_range(0.0, 1.0)
```,
caption: [Curried `predicate` statement and partial application],
) <midas-predicate-partial>
Notice that the second predicate statement doesn't take any parameters. This is simply a partial application of another predicate, kind of like an alias. You can use it in other expressions to finalize the call: Notice that the second predicate statement doesn't take any parameters. This is simply a partial application of another predicate, kind of like an alias. You can use it in other expressions to finalize the call:
```midas #figure(
type Efficiency = float where is_ratio(_) ```midas
``` type Efficiency = float where is_ratio(_)
```,
caption: [Constraint type definition using the partially applied predicate from @midas-predicate-partial],
)
Of course you can also directly call `in_range`: Of course you can also directly call `in_range`:
```midas #figure(
type Efficiency = float where in_range(0.0, 1.0)(_) ```midas
``` type Efficiency = float where in_range(0.0, 1.0)(_)
```,
caption: [Full call of curried predicate from @midas-predicate-partial],
)
When compiled, named predicates are translated to Python functions which are used in runtime assertions. Only predicates that are referenced are compiled. When compiled, named predicates are translated to Python functions which are used in runtime assertions. Only predicates that are referenced are compiled.
@@ -363,7 +414,6 @@ They can contain comparisons, simple computations, logical operations and must e
Context is quite restricted inside these expressions. You can only reference some builtin functions, such as type constructors (`float(...)`, `str(...)`, etc.), parameters of predicate statements, and named predicates. In constraint type, the special variable `_` can be used to reference the value targeted by the type. For example: Context is quite restricted inside these expressions. You can only reference some builtin functions, such as type constructors (`float(...)`, `str(...)`, etc.), parameters of predicate statements, and named predicates. In constraint type, the special variable `_` can be used to reference the value targeted by the type. For example:
#codly()
#figure( #figure(
```midas ```midas
predicate not_nan(v: float) = v != float("nan") predicate not_nan(v: float) = v != float("nan")
@@ -445,9 +495,12 @@ Once a variable has been given a type, it cannot be changed in the same scope.
The walrus operator (```py :=```) is not currently supported. The walrus operator (```py :=```) is not currently supported.
A simple annotation declaration, without assigning a value, is enough to declare a variable. For example: A simple annotation declaration, without assigning a value, is enough to declare a variable. For example:
```python #figure(
var: float ```python
``` var: float
```,
caption: [Bare Python variable annotation without assignment],
)
Because unpacking is not supported, assigning to multiple values is also not handled by the type checker. Because unpacking is not supported, assigning to multiple values is also not handled by the type checker.
@@ -495,9 +548,12 @@ For example:
), ),
), ),
) )
```python #figure(
parity = ("even" if num % 2 == 0 else "odd") ```python
``` parity = ("even" if num % 2 == 0 else "odd")
```,
caption: [Typing of ternary operator],
)
== Control flow == Control flow
@@ -505,15 +561,18 @@ Some control flow features are supported. For the limited code of this project,
=== `if` / `elif` / `else` <if-else> === `if` / `elif` / `else` <if-else>
Conditional statements are checked relatively strictly by Midas. The test expression, i.e. what comes after the ```py if``` keyword, must be a boolean. While Python allows introducing and leaking new variables from inside an ```py if``` statement, Midas will strictly forbid leaks by restraining bindings to the scope they are defined in. For example, the following valid Python code will not compile with Midas: Conditional statements are checked relatively strictly by Midas. The test expression, i.e. what comes after the ```py if``` keyword, must be a boolean. While Python allows introducing and leaking new variables from inside an ```py if``` statement, Midas will strictly forbid leaks by restraining bindings to the scope they are defined in. For example, the following Python code will not compile with Midas:
```python #figure(
age = 22 ```python
if age >= 18: age = 22
msg = "You're an adult" if age >= 18:
else: msg = "You're an adult"
msg = "You're still a child" else:
print(msg) # -> unknown variable 'msg' msg = "You're still a child"
``` print(msg) # -> unknown variable 'msg'
```,
caption: [`if`/`else` statement cannot leak variables],
)
=== `for` loops === `for` loops
@@ -564,11 +623,14 @@ As for the rest of your code, type annotations are optional, but recommended. If
), ),
), ),
) )
```python #figure(
def double(value: float) -> float: ```python
return value * 2 def double(value: float) -> float:
result = double(4.0) return value * 2
``` result = double(4.0)
```,
caption: [Typing of function's body and call],
)
Anonymous functions (```py lambda```) are not yet supported Anonymous functions (```py lambda```) are not yet supported
@@ -576,9 +638,12 @@ Anonymous functions (```py lambda```) are not yet supported
#gc.info[ #gc.info[
The functions discussed in this section are provided by the `midas.typing` submodule. You can import them in your script like so: The functions discussed in this section are provided by the `midas.typing` submodule. You can import them in your script like so:
```python #figure(
from midas.typing import cast, unsafe_cast ```python
``` from midas.typing import cast, unsafe_cast
```,
caption: [Importing cast functions],
)
] ]
Sometimes, you may want to use a value whose type is not known to the type checker in a place where it expects a particular type. In that case, if you do know that the runtime type will correspond to what is expected, you can use a `cast` expression. Sometimes, you may want to use a value whose type is not known to the type checker in a place where it expects a particular type. In that case, if you do know that the runtime type will correspond to what is expected, you can use a `cast` expression.
@@ -605,10 +670,13 @@ In the following example, a runtime check would be generated to ensure that the
), ),
), ),
) )
```python #figure(
typed_value = cast(PositiveFloat, unknown_value) ```python
print(typed_value) typed_value = cast(PositiveFloat, unknown_value)
``` print(typed_value)
```,
caption: [Typing of `cast` expression],
)
There may be some cases where the cost of checking a value at runtime is simply not worth the safety, for example when dealing with a big dataset. If do wish so, you can use `unsafe_cast` which will only tell the type checker the type of the value, without generating a runtime assertion. This maps to the default behavior of `typing`'s own `cast` function. There may be some cases where the cost of checking a value at runtime is simply not worth the safety, for example when dealing with a big dataset. If do wish so, you can use `unsafe_cast` which will only tell the type checker the type of the value, without generating a runtime assertion. This maps to the default behavior of `typing`'s own `cast` function.