# Imperative, declarative and functional

> Three ways to write a Pyzed app: methods that change the state, messages handled by one update(), or an immutable state drawn by pure functions. The same to-do list written each way, and how to manage state in classes.

Source: https://albakore.github.io/pyzed/guides/code-styles/

Pyzed does not force one way of writing an app. A handler can be a method, a message or a function that returns a
new state, and all three mix in the same program. This page writes the same to-do list in each style, so you can
compare them line by line, and says when each one fits.

| Style | The state lives in | An event | Fits |
| --- | --- | --- | --- |
| **Imperative** | Attributes of the app (`self.tasks`). | Calls a method that changes them (`on_submit=self.add`). | Small apps, prototypes, designed forms. |
| **Declarative** | Attributes of the app. | Produces a message (`on_submit=Add()`); one `update()` changes the state. | Apps with many ways to do the same thing, logging, undo, tests without a window. |
| **Functional** | One immutable value (`State`). | Asks for a new state (`set_state(draft="")`); the view is a pure function. | Logic you want to test as plain functions, reusable view functions, caching with `@pz.memo`. |

In the three of them the view is **declarative**: `view()` describes the whole window from the state, and Pyzed
works out what changed. The styles differ in how events reach the state.

## The same app three ways

**Imperative:**

`examples/code_styles/imperative.py`:

```python
"""The same to-do list three ways (1 of 3): imperative.

The state is plain attributes of the app and every event calls a method that changes them.
See docs/guides/code-styles.md; the other two are declarative.py and functional.py.

    python examples/code_styles/imperative.py
"""

import pyzed as pz
from pyzed import ui

class Todo(pz.App):
    title = "Tasks"
    size = (420, 360)

    def __init__(self):
        self.tasks = [{"title": "Write the docs", "done": False}]
        self.draft = ""
        self.filter = "All"

    # -- event handlers: each one changes the state --------------------------
    def type(self, text):
        self.draft = text

    def add(self):
        if self.draft.strip():
            self.tasks.append({"title": self.draft.strip(), "done": False})
            self.draft = ""

    def toggle(self, task, done):
        task["done"] = done

    def remove(self, task):
        self.tasks.remove(task)

    # -- the view reads the state ----------------------------------------------
    def view(self):
        shown = [t for t in self.tasks if self.filter == "All" or t["done"] == (self.filter == "Done")]
        return pz.column([
            ui.input(self.draft, self.type, placeholder="New task", on_submit=self.add),
            ui.segmented(["All", "To do", "Done"], self.filter, lambda f: setattr(self, "filter", f)),
            *[pz.row([
                ui.checkbox(t["done"], lambda done, t=t: self.toggle(t, done), label=t["title"]),
                pz.hspace(),
                ui.icon_button("trash-2", lambda t=t: self.remove(t), size="sm"),
            ], align="center") for t in shown],
        ], spacing=8, padding=16)

if __name__ == "__main__":
    pz.run(Todo)
```

**Declarative:**

`examples/code_styles/declarative.py`:

```python
"""The same to-do list three ways (2 of 3): declarative.

Every event is a message (a small dataclass) and one ``update()`` decides what each one does.
See docs/guides/code-styles.md; the other two are imperative.py and functional.py.

    python examples/code_styles/declarative.py
"""

from dataclasses import dataclass

import pyzed as pz
from pyzed import ui

# -- the messages: everything that can happen ------------------------------------
@dataclass
class Type:
    text: str

@dataclass
class Add:
    pass

@dataclass
class Toggle:
    index: int
    done: bool

@dataclass
class Remove:
    index: int

@dataclass
class Filter:
    name: str

class Todo(pz.App):
    title = "Tasks"
    size = (420, 360)

    def __init__(self):
        self.tasks = [{"title": "Write the docs", "done": False}]
        self.draft = ""
        self.filter = "All"

    # -- one place decides how each message changes the state ------------------
    def update(self, message):
        match message:
            case Type(text):
                self.draft = text
            case Add() if self.draft.strip():
                self.tasks.append({"title": self.draft.strip(), "done": False})
                self.draft = ""
            case Toggle(index, done):
                self.tasks[index]["done"] = done
            case Remove(index):
                del self.tasks[index]
            case Filter(name):
                self.filter = name

    # -- the view only describes the window and the messages -------------------
    def view(self):
        rows = [(i, t) for i, t in enumerate(self.tasks)
                if self.filter == "All" or t["done"] == (self.filter == "Done")]
        return pz.column([
            ui.input(self.draft, Type, placeholder="New task", on_submit=Add()),
            ui.segmented(["All", "To do", "Done"], self.filter, Filter),
            *[pz.row([
                ui.checkbox(t["done"], lambda done, i=i: Toggle(i, done), label=t["title"]),
                pz.hspace(),
                ui.icon_button("trash-2", Remove(i), size="sm"),
            ], align="center") for i, t in rows],
        ], spacing=8, padding=16)

if __name__ == "__main__":
    pz.run(Todo)
```

**Functional:**

`examples/code_styles/functional.py`:

```python
"""The same to-do list three ways (3 of 3): functional.

The state is one frozen dataclass, the view and the logic are pure functions, and the only class
keeps the current state. See docs/guides/code-styles.md; the other two are imperative.py and
declarative.py.

    python examples/code_styles/functional.py
"""

from dataclasses import dataclass, replace

import pyzed as pz
from pyzed import ui

@dataclass(frozen=True)
class Task:
    title: str
    done: bool = False

@dataclass(frozen=True)
class State:                                   # the whole state, immutable
    tasks: tuple = (Task("Write the docs"),)
    draft: str = ""
    filter: str = "All"

# -- pure functions: they change nothing, they return new values -----------------
def added(tasks, title):
    return tasks + (Task(title.strip()),) if title.strip() else tasks

def toggled(tasks, index, done):
    return tuple(replace(t, done=done) if i == index else t for i, t in enumerate(tasks))

def removed(tasks, index):
    return tasks[:index] + tasks[index + 1:]

def visible(tasks, filter):
    return [(i, t) for i, t in enumerate(tasks) if filter == "All" or t.done == (filter == "Done")]

# -- the view: a pure function of the state ---------------------------------------
def task_row(task, on_toggle, on_remove):      # a function component
    return pz.row([
        ui.checkbox(task.done, on_toggle, label=task.title),
        pz.hspace(),
        ui.icon_button("trash-2", on_remove, size="sm"),
    ], align="center")

def view(state, set_state):
    return pz.column([
        ui.input(state.draft, lambda text: set_state(draft=text), placeholder="New task",
                 on_submit=lambda: set_state(tasks=added(state.tasks, state.draft), draft="")),
        ui.segmented(["All", "To do", "Done"], state.filter, lambda f: set_state(filter=f)),
        *[task_row(t, lambda done, i=i: set_state(tasks=toggled(state.tasks, i, done)),
                   lambda i=i: set_state(tasks=removed(state.tasks, i)))
          for i, t in visible(state.tasks, state.filter)],
    ], spacing=8, padding=16)

# -- the only class: it keeps the current state ------------------------------------
class Todo(pz.App):
    title = "Tasks"
    size = (420, 360)
    state = State()

    def set_state(self, **changes):
        self.state = replace(self.state, **changes)

    def view(self):
        return view(self.state, self.set_state)

if __name__ == "__main__":
    pz.run(Todo)
```

Run any of them with `python examples/code_styles/imperative.py` (or `declarative.py`, `functional.py`): the three
windows look and behave the same.

## Imperative: methods change the state

Every handler is a method or a lambda, and it changes `self`. Pyzed calls `view()` again afterwards.

```python
def add(self):
    if self.draft.strip():
        self.tasks.append({"title": self.draft.strip(), "done": False})
        self.draft = ""

ui.input(self.draft, self.type, on_submit=self.add)
ui.checkbox(t["done"], lambda done, t=t: self.toggle(t, done), label=t["title"])
```

- A handler that takes no argument is called with nothing (`on_press`, `on_submit`); one of a control that produces
  a value gets it (`on_input(text)`, `on_toggle(done)`, `on_select(item)`).
- To pass your own data, bind it with a default argument: `lambda t=t: self.remove(t)`. Without `t=t`, every lambda
  of the loop would see the last task.
- It is the shortest style, and the one the handlers of a [designed form](https://albakore.github.io/pyzed/guides/classes/#designed-forms-pzform--handlers)
  use: `form.status.text = "Saved"`.

## Declarative: messages and one `update()`

Handlers are **values**, the messages, which go to `update(message)`. A message says *what happened*; `update`
decides *what it means*.

```python
@dataclass
class Type:
    text: str

@dataclass
class Toggle:
    index: int
    done: bool

ui.input(self.draft, Type, on_submit=Add())            # Type(text) is built from the new text
ui.checkbox(t["done"], lambda done, i=i: Toggle(i, done))

def update(self, message):
    match message:
        case Type(text):
            self.draft = text
        case Toggle(index, done):
            self.tasks[index]["done"] = done
```

- **A message is any value that is not callable**: a string (`"save"`), a tuple (`("delete", 3)`) or, best, a small
  dataclass, which reads well in `match`.
- **Controls with a value** call what you give them with that value. A dataclass *class* is callable, so
  `on_input=Type` builds `Type(text)` and sends it to `update`. For events without a value pass an *instance*:
  `on_submit=Add()`.
- A callable that returns a message also works: `lambda done, i=i: Toggle(i, done)`.
- `update` can return a `Command` (focus, close, open a window) or start async work, like any handler.
- Since every change goes through one method, you can log messages, keep a history to undo, replay them or test
  the logic without opening a window:

```python
app = Todo()
app.update(Type("Buy milk"))
app.update(Add())
assert app.tasks[-1]["title"] == "Buy milk"
```

Messages also come from outside the view: shortcuts (`on_key`), timers (`pz.every(1, Tick())`), menus and threads
(`pz.post(Downloaded(...))`). See [App architecture](https://albakore.github.io/pyzed/guides/architecture/#messages).

## Functional: an immutable state and pure functions

The state is **one frozen dataclass**. The view is a function `view(state, set_state)` that only reads it, and the
logic lives in functions that take data and return new data:

```python
@dataclass(frozen=True)
class State:
    tasks: tuple = ()
    draft: str = ""

def added(tasks, title):                        # pure: no self, no side effects
    return tasks + (Task(title.strip()),) if title.strip() else tasks

def view(state, set_state):
    return ui.input(state.draft, lambda text: set_state(draft=text),
                    on_submit=lambda: set_state(tasks=added(state.tasks, state.draft), draft=""))

class Todo(pz.App):                             # the only class: it keeps the current state
    state = State()

    def set_state(self, **changes):
        self.state = replace(self.state, **changes)

    def view(self):
        return view(self.state, self.set_state)
```

- Nothing changes in place: `set_state(...)` builds a new `State` with `dataclasses.replace`. Use tuples and
  `frozenset` instead of lists and sets.
- Pure functions are trivial to test: `assert added((), "Milk") == (Task("Milk"),)`.
- View functions are your components: `task_row(task, on_toggle, on_remove)` takes data and callbacks and returns
  widgets, like the functions of `pyzed.ui`. Since their arguments are plain values, they can be cached with
  [`@pz.memo`](https://albakore.github.io/pyzed/guides/performance/), which rebuilds them only when the arguments change.

For a pure *reducer* (the Elm or Redux way), combine it with messages: `update` replaces the state with what a pure
function returns.

```python
def reduce(state: State, message) -> State:
    match message:
        case Type(text):
            return replace(state, draft=text)
        case Add():
            return replace(state, tasks=added(state.tasks, state.draft), draft="")
    return state

class Todo(pz.App):
    state = State()

    def update(self, message):
        self.state = reduce(self.state, message)

    def view(self):
        return view(self.state)                 # the view only sends messages
```

## Managing state in classes

Wherever the state lives, the same rules keep an app predictable:

| Rule | Why |
| --- | --- |
| Put the initial state in `__init__` (or in class attributes for immutable defaults). | A class attribute that is a list or a dict is shared by every instance; `__init__` gives each app its own. |
| Never change state inside `view()`. | `view()` runs after every event; compute what to show there, nothing else. |
| Compute derived values in `view()` instead of storing them. | `shown = [t for t in self.tasks if ...]` can never go stale. |
| Keep data in plain Python: dataclasses, dicts, lists. | Pyzed redraws from whatever you have; there are no observables to wrap. |
| Replace lists instead of mutating them when a part of the view is cached. | `@pz.memo` compares arguments; `self.tasks = [*self.tasks, t]` is a new value. |
| State of one window goes in its `Window`; shared state in the `App` (`self.app`). | Every window has its own `view()`, and reads the app through `self.app`. |
| State of a reusable piece goes in a `pz.Component` (`pz.state(...)`). | Each instance keeps its own, and the parent only sees its props and events. |

A component has the same three options inside: methods that change `self`, `self.emit(...)` to tell the parent, or a
`render()` that only reads its props. See [Apps, windows, forms and components](https://albakore.github.io/pyzed/guides/classes/).

## Mixing styles

Most real apps mix them: imperative handlers for small UI state (an open menu, the hovered row), messages for the
actions that matter (save, delete, sync), and pure functions for the logic and the reusable parts of the view.

```python
class Editor(pz.App):
    def __init__(self):
        self.menu_open = False             # UI detail: imperative
        self.doc = Document()

    def update(self, message):             # actions: messages
        match message:
            case Save():
                return save(self.doc)      # pure function, returns a Command

    def view(self):
        return pz.column([
            toolbar(self.doc.name, on_save=Save()),                    # a view function
            ui.icon_button("menu", lambda: setattr(self, "menu_open", True)),
        ])
```

Each page of the [library](https://albakore.github.io/pyzed/library/) shows the events of its component in the three styles, under
**Events → Handling the events**.
