Skip to content

Commit

Permalink
Merge pull request #6 from harveywi/wjh-monad-syntax-v2
Browse files Browse the repository at this point in the history
Less ad hoc monad desugaring, some code cleanup, unit tests.
  • Loading branch information
William Harvey authored Oct 20, 2019
2 parents aa5e3b9 + 6c6361d commit 8cd9f68
Show file tree
Hide file tree
Showing 14 changed files with 611 additions and 246 deletions.
16 changes: 11 additions & 5 deletions Pipfile.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

55 changes: 36 additions & 19 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,9 @@ It's a fantastic day to write ZIO programs in Python.
Enter your name: William
Good to meet you, William!
Your age is 42.
That's all folks.
X is 1
Y is 2
Z is 3
1000
Done.
```
Expand Down Expand Up @@ -88,8 +90,6 @@ result = runtime.unsafe_run_sync(program.provide(live_console))
```

# Monad comprehension syntactic sugar
(A Pythonista somewhere squirms and writhes in agony. Shrug!)

Using `flat_map` and `map` throughout Python code quickly becomes unruly. ZIO
for Python uses [macropy](https://github.com/lihaoyi/macropy) to enable an
alternative syntax that is much flatter AND THEREFORE MORE PYTHONIC. (You
Expand All @@ -98,31 +98,28 @@ cannot argue with me here, don't even try.)
To use it, first import the following at the top of your source file:

```python
from zio_py.syntax import macros, monad
from zio_py.syntax import macros, monadic
```

This will load the macro tooling, which you can invoke by creating a `with` block
to create a monadic value that is bound to the variable called `program` (you
can call it whatever you want):
This will load the macro tooling, which you can invoke within a function body
by decorating the function with a `@monadic` decorator:

```python
# To keep the IDE happy, declare `program` here. Technically this
# is not necessary, as it is instantiated/bound via the `with monad`
# block below.
program: ZIO[Console, Exception, int]

with monad(program):
@monadic
def program() -> ZIO[Console, Exception, int]:
...
```

Now put your ZIO code in place of the ellipsis and sprinkle on some syntactic
sugar. Simple!

```python
with monad(program):
@monadic
def program() -> ZIO[Console, Exception, int]:
~print_line("Good morning!")
name = ~read_line("What is your name? ")
~print_line(f"Good to meet you, {name}!")
return print_line(f"Good to meet you, {name}!")

```

You might be wondering about the `~` operators (a.k.a.
Expand Down Expand Up @@ -150,7 +147,15 @@ Python monads.

Here is a more sophisticated program:
```python
with monad(program):
from zio_py.zio import ZIO, ZIOStatic
from zio_py.console import Console, print_line, read_line
from zio_py.syntax import macros, monadic # noqa: F401


# To use the `~` short-hand syntax for monadic binds within a function,
# decorate your function with the `@monadic` decorator.
@monadic
def my_program() -> ZIO[Console, Exception, int]:
# You can declare variables that are not lifted into a monadic context.
w = 'sunshine'

Expand All @@ -164,12 +169,24 @@ with monad(program):
# This is a monadic bind where the variable is called `name`.
# Desugars (approximately) to `read_line(...).flat_map(lambda name: ...)`
name = ~read_line("Enter your name: ")

# Yes, type inference with mypy and in the IDE works!
# reveal_type(name) will show `str`

your_age = 42
~print_line(f"Good to meet you, {name}!")
~print_line(f"Your age is {your_age}.")

# The `~ZIOStatic.succeed(1000)` is like `return 1000` in Haskell, or
# The usual complex assignment syntaxes work as well.
[x, y, z] = ~ZIOStatic.succeed([1, 2, 3])
~print_line(f"X is {x}")
~print_line(f"Y is {y}")
~print_line(f"Z is {z}")

# The `ZIOStatic.succeed(1000)` is like `return 1000` in Haskell, or
# `yield 1000` in Scala.
print("That's all folks.")
~ZIOStatic.succeed(1000)
# The rule is simple; you just have to return a value consistent with the
# type signature of your function (like always). Mypy will complain
# at you if you get anything wrong.
return ZIOStatic.succeed(1000)
```
8 changes: 2 additions & 6 deletions example/example.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,16 @@
`example.my_program.py` and `run_my_program.py`.
"""

from typing import NoReturn

from zio_py.console import Console, LiveConsole, read_line, print_line
from zio_py.console import Console, LiveConsole, print_line, read_line
from zio_py.runtime import Runtime
from zio_py.zio import ZIO, ZIOStatic

from zio_py.zio import ZIO

if __name__ == "__main__":
program: ZIO[Console, Exception, None] = \
print_line("Good morning!") \
.flat_map(lambda _: read_line("What is your name? ")
.flat_map(lambda name: print_line(f"Good to meet you, {name}!"))) # noqa


runtime = Runtime[LiveConsole]()
live_console = LiveConsole()

Expand Down
33 changes: 21 additions & 12 deletions example/my_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,15 @@
See: https://docs.scala-lang.org/tutorials/FAQ/yield.html
"""

from typing import Any

from zio_py.zio import ZIO, ZIOStatic
from zio_py.console import Console, print_line, read_line
from zio_py.syntax import macros, monad
from zio_py.syntax import macros, monadic # noqa: F401
from zio_py.zio import ZIO, ZIOStatic

# To keep the IDE happy, declare `program` here. Technically this
# is not necessary, as it is instantiated/bound via the `with monad`
# block below.
program: ZIO[Console, Exception, int]

with monad(program):
# To use the `~` short-hand syntax for monadic binds within a function,
# decorate your function with the `@monadic` decorator.
@monadic
def my_program() -> ZIO[Console, Exception, int]:
# You can declare variables that are not lifted into a monadic context.
w = 'sunshine'

Expand All @@ -32,11 +29,23 @@
# This is a monadic bind where the variable is called `name`.
# Desugars (approximately) to `read_line(...).flat_map(lambda name: ...)`
name = ~read_line("Enter your name: ")

# Yes, type inference with mypy and in the IDE works!
# reveal_type(name) will show `str`

your_age = 42
~print_line(f"Good to meet you, {name}!")
~print_line(f"Your age is {your_age}.")

# The `~ZIOStatic.succeed(1000)` is like `return 1000` in Haskell, or
# The usual complex assignment syntaxes work as well.
[x, y, z] = ~ZIOStatic.succeed([1, 2, 3])
~print_line(f"X is {x}")
~print_line(f"Y is {y}")
~print_line(f"Z is {z}")

# The `ZIOStatic.succeed(1000)` is like `return 1000` in Haskell, or
# `yield 1000` in Scala.
print("That's all folks.")
~ZIOStatic.succeed(1000)
# The rule is simple; you just have to return a value consistent with the
# type signature of your function (like always). Mypy will complain
# at you if you get anything wrong.
return ZIOStatic.succeed(1000)
7 changes: 3 additions & 4 deletions example/run_my_program.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,14 @@
Note that you must `import macropy.activate` at the top of the file.
"""

import macropy.activate
from example.my_program import program

import macropy.activate # noqa: F401
from example.my_program import my_program
from zio_py.console import LiveConsole
from zio_py.runtime import Runtime

if __name__ == "__main__":
runtime = Runtime[LiveConsole]()
live_console = LiveConsole()
print("Running...")
print(runtime.unsafe_run_sync(program.provide(live_console)))
print(runtime.unsafe_run_sync(my_program().provide(live_console)))
print("Done.")
34 changes: 20 additions & 14 deletions setup.py
Original file line number Diff line number Diff line change
@@ -1,25 +1,31 @@
from distutils.core import setup

with open('README.md') as f:
long_description = f.read()

setup(
name = 'zio_py',
packages = ['zio_py'],
version = '0.0.3a',
name='zio_py',
packages=['zio_py'],
version='0.0.4a',
license='Apache license 2.0',
description = 'Python port of Scala ZIO for pure functional programming',
author = 'William Harvey',
author_email = '[email protected]',
url = 'https://github.com/harveywi/ziopy',
download_url = 'https://github.com/harveywi/ziopy/archive/0.0.3a.tar.gz',
keywords = ['ZIO', 'IO', 'monads', 'pure fp', 'functional programming',
description='Python port of Scala ZIO for pure functional programming',
long_description=long_description,
long_description_content_type='text/markdown', # This is important!
author='William Harvey',
author_email='[email protected]',
url='https://github.com/harveywi/ziopy',
download_url='https://github.com/harveywi/ziopy/archive/0.0.4a.tar.gz',
keywords=['ZIO', 'IO', 'monads', 'pure fp', 'functional programming',
'monad syntax'],
install_requires=[
'macropy3==1.1.0b2'
],
python_requires='>=3.7',
classifiers=[
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries :: Python Modules',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python :: 3.7'
'Development Status :: 3 - Alpha',
'Intended Audience :: Developers',
'Topic :: Software Development :: Libraries :: Python Modules',
'License :: OSI Approved :: Apache Software License',
'Programming Language :: Python :: 3.7'
]
)
1 change: 1 addition & 0 deletions tests/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
import macropy.activate # noqa: F401
9 changes: 9 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
from typing import Any

import pytest
from zio_py.runtime import Runtime


@pytest.fixture
def simple_runtime() -> Runtime[Any]:
return Runtime()
Loading

0 comments on commit 8cd9f68

Please sign in to comment.