Skip to content

Commit

Permalink
Merge pull request #35 from ar90n/feature/use-async-func-as-effect
Browse files Browse the repository at this point in the history
feat: use async func as Effect
  • Loading branch information
ar90n authored Oct 10, 2022
2 parents 55bbff3 + a1f4006 commit 610acdf
Show file tree
Hide file tree
Showing 4 changed files with 65 additions and 35 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ If you need more exmplaes, please check the [examples](https://github.com/ar90n/
## Concept
Alfort is inspired by TEA(The Elm Architecture). So Alfort makes you create an interactive application with `View`, `Model` and `Update`. If you need more specification about TEA, please see this [documentation](https://guide.elm-lang.org/architecture/).

Therefore, Alfort doesn't support Command. So Alfort uses functions whose type is `Callable[[Callable[[Msg], None]], None]` to achieve side effect.
Therefore, Alfort doesn't support Command. So Alfort uses functions whose type is `Callable[[Callable[[Msg], None]], Coroutine[None, None, Any]]` to achieve side effect.
You can run some tasks which have side effects in this function. And, if you need, you can pass the result of side effect as Message to `dicpatch` which is given as an argument.
This idea is inspired by [hyperapp](https://github.com/jorgebucaran/hyperapp).

Expand Down
7 changes: 4 additions & 3 deletions alfort/app.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import asyncio
from abc import abstractmethod
from dataclasses import dataclass, replace
from itertools import zip_longest
from typing import Callable, Generic, TypeAlias, TypeVar
from typing import Any, Callable, Coroutine, Generic, TypeAlias, TypeVar

from alfort.sub import Context, Subscriptions
from alfort.vdom import (
Expand All @@ -23,7 +24,7 @@
M = TypeVar("M")

Dispatch: TypeAlias = Callable[[M], None]
Effect: TypeAlias = Callable[[Dispatch[M]], None]
Effect: TypeAlias = Callable[[Dispatch[M]], Coroutine[None, None, Any]]
Init: TypeAlias = Callable[[], tuple[S, list[Effect[M]]]]
View: TypeAlias = Callable[[S], VDom]
Update: TypeAlias = Callable[[M, S], tuple[S, list[Effect[M]]]]
Expand Down Expand Up @@ -62,7 +63,7 @@ class Alfort(Generic[S, M, N]):
@classmethod
def _run_effects(cls, dispatch: Dispatch[M], effects: list[Effect[M]]) -> None:
for e in effects:
e(dispatch)
asyncio.create_task(e(dispatch))

@classmethod
def _diff_props(cls, node_props: Props, vdom_props: Props) -> PatchProps:
Expand Down
17 changes: 9 additions & 8 deletions examples/simple_counter/main.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
import curses
from enum import Enum, auto
from typing import Any, Callable
Expand Down Expand Up @@ -52,10 +53,16 @@ def create_element(
) -> TextNode:
raise ValueError("create_element should not be called")

def main(
async def main(
self,
) -> None:
self._main()
while True:
c = chr(stdscr.getch())
if c == "q":
break
if handle := handlers.get(c):
handle()


def main(stdscr: Any) -> None:
Expand Down Expand Up @@ -87,13 +94,7 @@ def on_keydown(dispatch: Dispatch[Msg]) -> UnSubscription:
app = AlfortSimpleCounter(
init=init, view=view, update=update, subscriptions=subscriptions
)
app.main()
while True:
c = chr(stdscr.getch())
if c == "q":
break
if handle := handlers.get(c):
handle()
asyncio.run(app.main())


if __name__ == "__main__":
Expand Down
74 changes: 51 additions & 23 deletions tests/test_app.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import asyncio
from dataclasses import dataclass
from typing import Any, Callable, Generic, TypeVar

Expand All @@ -21,6 +22,10 @@
T = TypeVar("T", bound=Node)


def get_other_tasks() -> set[asyncio.Task[Any]]:
return asyncio.all_tasks() - {asyncio.current_task()}


def to_vnode(node_dom: NodeDom) -> VDom:
match node_dom:
case NodeDomText():
Expand Down Expand Up @@ -240,7 +245,10 @@ def view(state: dict[str, int]) -> VDom:
return str(state["count"])

def init() -> tuple[dict[str, int], list[Effect[Msg]]]:
return ({"count": 0}, [lambda dispatch: dispatch(CountUp(3))])
async def _e(dispatch: Dispatch[Msg]) -> None:
dispatch(CountUp(3))

return ({"count": 0}, [_e])

def update(
msg: Msg, state: dict[str, int]
Expand All @@ -252,14 +260,18 @@ def update(
return ({"count": state["count"] - value}, [])

app = AlfortText(init=init, view=view, update=update)
app.main(root)

assert root.child is not None
assert root.child.text == "3"
root.child.dispatch(CountUp())
assert root.child.text == "4"
root.child.dispatch(CountDown())
assert root.child.text == "3"
async def main_loop() -> None:
app.main(root)
await asyncio.gather(*get_other_tasks())
assert root.child is not None
assert root.child.text == "3"
root.child.dispatch(CountUp())
assert root.child.text == "4"
root.child.dispatch(CountDown())
assert root.child.text == "3"

asyncio.run(main_loop())


def test_enqueue() -> None:
Expand Down Expand Up @@ -287,19 +299,30 @@ def view(state: dict[str, int]) -> VDom:
return str(state["count"])

def init() -> tuple[dict[str, int], list[Effect[Msg]]]:
return ({"count": 5}, [capture, lambda d: enqueue(lambda: capture(d))])
async def _e0(d: Dispatch[Msg]) -> None:
capture(d)

async def _e1(d: Dispatch[Msg]) -> None:
enqueue(lambda: capture(d))

return ({"count": 5}, [_e0, _e1])

def update(
msg: Msg, state: dict[str, int]
) -> tuple[dict[str, int], list[Effect[Msg]]]:
return (state, [])

app = AlfortText(init=init, view=view, update=update, enqueue=enqueue)
app.main(root)

assert view_values == [None]
render()
assert view_values == [None, "5"]
async def main_loop() -> None:
app.main(root)
await asyncio.gather(*get_other_tasks())

assert view_values == [None]
render()
assert view_values == [None, "5"]

asyncio.run(main_loop())


def test_subscriptions() -> None:
Expand Down Expand Up @@ -341,13 +364,18 @@ def _unsubscribe():
return [on_countup] if state["count"] < 2 else []

app = AlfortText(init=init, view=view, update=update, subscriptions=subscriptions)
app.main(root)

assert root.child is not None
assert root.child.text == "0"
assert countup is not None
countup()
assert root.child.text == "1"
countup()
assert root.child.text == "2"
assert countup is None

async def main_loop() -> None:
app.main(root)
await asyncio.gather(*get_other_tasks())

assert root.child is not None
assert root.child.text == "0"
assert countup is not None
countup()
assert root.child.text == "1"
countup()
assert root.child.text == "2"
assert countup is None

asyncio.run(main_loop())

0 comments on commit 610acdf

Please sign in to comment.