# Modeling Types

> newtype for distinct aliases, sealed and case for closed unions with pattern matching.

Source: https://docs.pyrula.io/primitives/modeling/

## newtype

`newtype` makes a `UserId` distinct from a bare `int` for type checkers, while it behaves like one at runtime.

```python
from pyrula import newtype, newtype_strict

UserId = newtype("UserId", int)
UserId(1)                       # 1, typed as UserId

Email = newtype_strict("Email", str, validate=lambda s: "@" in s)
Email("a@example.com")          # ok
Email("nope")                   # raises ValueError
```

`newtype_strict` also takes `err_msg` for the failure message.

## Sealed unions

`@sealed` marks a closed set of cases and `@case` declares each as an immutable dataclass. `match` runs the first matching `on(Type, handler)` and raises if none match.

```python
from pyrula import sealed, case, match, on

@sealed
class Shape:
    pass

@case
class Circle(Shape):
    radius: float

@case
class Rect(Shape):
    w: float
    h: float

def area(shape: Shape) -> float:
    return match(shape)(
        on(Circle, lambda c: 3.14159 * c.radius ** 2),
        on(Rect, lambda r: r.w * r.h),
    )

area(Circle(2.0))   # 12.566...
```

A sealed type accepts subclasses only from its defining module; any other raises `TypeError` at class creation, with no opt-out for tests.

Cases are frozen dataclasses, so `dataclasses.replace` and `asdict` work:

```python
import dataclasses

c = Circle(2.0)
dataclasses.replace(c, radius=3.0)   # Circle(radius=3.0)
dataclasses.asdict(c)                # {"radius": 2.0}
```

The JSON form is `{"CaseName": {fields}}`; see [JSON form](https://docs.pyrula.io/primitives/json-form.md).
