Skip to content

Commit

Permalink
Refactor IO actions (#20)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbrattli authored Sep 28, 2020
1 parent b34be63 commit 25b40b6
Show file tree
Hide file tree
Showing 6 changed files with 88 additions and 91 deletions.
4 changes: 2 additions & 2 deletions examples/hello2.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
from oslash import Put, Get, IO
from oslash import Put, Get, Return, Unit

main = Put("What is your name?",
Get(lambda name:
Put("What is your age?",
Get(lambda age:
Put("Hello " + name + "!",
Put("You are " + age + " years old",
IO()
Return(Unit)
)
)
)
Expand Down
4 changes: 2 additions & 2 deletions oslash/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,15 @@
from .maybe import Maybe, Just, Nothing
from .either import Either, Right, Left
from .list import List
from .ioaction import IO, Put, Get, ReadFile, put_line, get_line, read_file
from .ioaction import IO, Put, Get, Return, ReadFile, put_line, get_line, read_file
from .writer import Writer, MonadWriter, StringWriter
from .reader import Reader, MonadReader
from .identity import Identity
from .state import State
from .do import do, let, guard

from .monadic import *
from .util import fn
from .util import fn, Unit

from ._version import get_versions
__version__ = get_versions()['version']
Expand Down
3 changes: 2 additions & 1 deletion oslash/cont.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@

TCont = Callable[[T], TResult]


class Cont(Generic[T, TResult]):
"""The Continuation Monad.
Expand All @@ -39,7 +40,7 @@ def unit(cls, value: T) -> 'Cont[T, TResult]':
Haskell: a -> Cont a
"""
fn : Callable[[TCont], TResult] = lambda cont: cont(value)
fn: Callable[[TCont], TResult] = lambda cont: cont(value)
return Cont(fn)

def map(self, fn: Callable[[T], T2]) -> 'Cont[T2, TResult]':
Expand Down
156 changes: 75 additions & 81 deletions oslash/ioaction.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,12 @@
"""Implementation of IO Actions.
"""Implementation of IO Actions."""

Many thanks to Chris Taylor and his excellent blog post "IO Is Pure",
http://chris-taylor.github.io/blog/2013/02/09/io-is-not-a-side-effect/
"""

from typing import Any, Callable, Generic, TypeVar, Tuple, Optional
from abc import abstractmethod
from typing import Any, Callable, Generic, TypeVar, Tuple

from .typing import Applicative
from .typing import Functor
from .typing import Monad
from .util import indent as ind
from .util import indent as ind, Unit

TSource = TypeVar("TSource")
TResult = TypeVar("TResult")
Expand All @@ -24,40 +21,24 @@ class IO(Generic[TSource]):
happen.
"""

def __init__(self, value: Optional[TSource] = None) -> None:
"""Create IO Action."""

super().__init__()
self._value = value

@classmethod
def unit(cls, value: TSource):
return cls(value)
return Return(value)

def bind(self, func: Callable[[Optional[TSource]], "IO[Optional[TResult]]"]) -> "IO[Optional[TResult]]":
@abstractmethod
def bind(self, func: Callable[[TSource], "IO[TResult]"]) -> "IO[TResult]":
"""IO a -> (a -> IO b) -> IO b."""

return func(self._value)

@classmethod
def pure(
cls, value: Optional[Callable[[Optional[TSource]], Optional[TResult]]]
) -> "IO[Optional[Callable[[Optional[TSource]], Optional[TResult]]]]":
return IO(value)

def apply(
self: "IO[Optional[Callable[[Optional[TSource]], Optional[TResult]]]]", something: "IO[Optional[TSource]]"
) -> "IO[Optional[TResult]]":
"""Apply wrapped function over something."""
assert self._value is not None
return something.map(self._value)
raise NotImplementedError

def map(self, func: Callable[[Optional[TSource]], Optional[TResult]]) -> "IO[Optional[TResult]]":
return IO(func(self._value))
@abstractmethod
def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]":
raise NotImplementedError

def run(self, world: int) -> Optional[TSource]:
@abstractmethod
def run(self, world: int) -> TSource:
"""Run IO action."""
return self._value
raise NotImplementedError

def __or__(self, func):
"""Use | as operator for bind.
Expand All @@ -67,49 +48,71 @@ def __or__(self, func):
return self.bind(func)

def __call__(self, world: int = 0) -> Any:
"""Nothing more to run."""
"""Run io action."""
return self.run(world)

@abstractmethod
def __str__(self, m: int = 0, n: int = 0) -> str:
a = self._value
return "%sReturn %s" % (ind(m), [a])
raise NotImplementedError

def __repr__(self) -> str:
return self.__str__()


class Put(IO):
class Return(IO[TSource]):
def __init__(self, value: TSource) -> None:
"""Create IO Action."""

self._value = value

def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]":
return Return(func(self._value))

def bind(self, func: Callable[[TSource], "IO[TResult]"]) -> "IO[TResult]":
"""IO a -> (a -> IO b) -> IO b."""

return func(self._value)

def run(self, world: int) -> TSource:
"""Run IO action."""
return self._value

def __str__(self, m: int = 0, n: int = 0) -> str:
a = self._value
return f"{ind(m)}Return {a}"


class Put(IO[TSource]):
"""The Put action.
A container holding a string to be printed to stdout, followed by
another IO Action.
"""

def __init__(self, text: str, action: IO) -> None:
super().__init__((text, action))
def __init__(self, text: str, io: IO) -> None:
self._value = text, io

def bind(self, func: Callable[[Optional[TSource]], IO[Optional[TResult]]]) -> "Put":
def bind(self, func: Callable[[TSource], IO[TResult]]) -> 'IO[TResult]':
"""IO a -> (a -> IO b) -> IO b"""

assert self._value is not None
text, a = self._value
return Put(text, a.bind(func))
text, io = self._value
return Put(text, io.bind(func))

def map(self, func: Callable[[Optional[TSource]], Optional[TResult]]) -> "Put":
def map(self, func: Callable[[TSource], TResult]) -> "IO[TResult]":
# Put s (fmap f io)
assert self._value is not None
text, action = self._value
return Put(text, action.map(func))

def run(self, world: int) -> IO:
def run(self, world: int) -> TSource:
"""Run IO action"""

assert self._value is not None
text, action = self._value
new_world = pure_print(world, text)
return action(world=new_world)

def __call__(self, world: int = 0) -> IO:
def __call__(self, world: int = 0) -> TSource:
return self.run(world)

def __str__(self, m: int = 0, n: int = 0) -> str:
Expand All @@ -119,96 +122,91 @@ def __str__(self, m: int = 0, n: int = 0) -> str:
return '%sPut ("%s",\n%s\n%s)' % (ind(m), s, a, ind(m))


class Get(IO):
"""A container holding a function from string -> IO, which can
class Get(IO[TSource]):
"""A container holding a function from string -> IO[TSource], which can
be applied to whatever string is read from stdin.
"""

def __init__(self, func: Callable[[str], IO]) -> None:
super().__init__(func)
def __init__(self, fn: Callable[[str], IO[TSource]]) -> None:
self._fn = fn

def bind(self, func: Callable[[Any], IO]) -> IO:
def bind(self, func: Callable[[TSource], IO[TResult]]) -> IO[TResult]:
"""IO a -> (a -> IO b) -> IO b"""

assert self._value is not None
g = self._value
g = self._fn
return Get(lambda text: g(text).bind(func))

def map(self, func: Callable[[Any], Any]) -> "Get":
def map(self, func: Callable[[TSource], TResult]) -> IO[TResult]:
# Get (\s -> fmap f (g s))
assert self._value is not None
g = self._value
g = self._fn
return Get(lambda s: g(s).map(func))

def run(self, world: int) -> IO:
def run(self, world: int) -> TSource:
"""Run IO Action"""

assert self._value is not None
func = self._value
func = self._fn
new_world, text = pure_input(world)
action = func(text)
return action(world=new_world)

def __call__(self, world: int = 0) -> IO:
def __call__(self, world: int = 0) -> TSource:
return self.run(world)

def __str__(self, m: int = 0, n: int = 0) -> str:
assert self._value is not None
g = self._value
g = self._fn
i = "x%s" % n
a = g(i).__str__(m + 1, n + 1)
return "%sGet (%s => \n%s\n%s)" % (ind(m), i, a, ind(m))


class ReadFile(IO):
"""A container holding a filename and a function from string -> IO,
class ReadFile(IO[str]):
"""A container holding a filename and a function from string -> IO[str],
which can be applied to whatever string is read from the file.
"""

def __init__(self, filename: str, func: Callable[[str], IO]) -> None:
super().__init__((filename, func))
self.open_func = open
self._get_value = lambda: (filename, func)
self._value = filename, func

def bind(self, func: Callable[[Any], IO]) -> IO:
"""IO a -> (a -> IO b) -> IO b"""

filename, g = self._get_value()
filename, g = self._value
return ReadFile(filename, lambda s: g(s).bind(func))

def map(self, func: Callable[[Any], Any]) -> IO:
# Get (\s -> fmap f (g s))
filename, g = self._get_value()
filename, g = self._value
return Get(lambda s: g(s).map(func))

def run(self, world: int) -> IO:
def run(self, world: int) -> str:
"""Run IO Action"""

filename, func = self._get_value()
filename, func = self._value
f = self.open_func(filename)
action = func(f.read())
return action(world=world + 1)

def __call__(self, world: int = 0) -> IO:
def __call__(self, world: int = 0) -> str:
return self.run(world)

def __str__(self, m: int = 0, n: int = 0) -> str:
filename, g = self._get_value()
filename, g = self._value
i = "x%s" % n
a = g(i).__str__(m + 2, n + 1)
return '%sReadFile ("%s",%s => \n%s\n%s)' % (ind(m), filename, i, a, ind(m))


def get_line() -> IO:
return Get(lambda text: IO(text))
def get_line() -> IO[str]:
return Get(Return)


def put_line(string: str) -> IO:
return Put(string, IO(None))
def put_line(text: str) -> IO:
return Put(text, Return(Unit))


def read_file(filename: str) -> IO:
return ReadFile(filename, lambda text: IO(text))
return ReadFile(filename, Return)


def pure_print(world: int, text: str) -> int:
Expand All @@ -222,17 +220,13 @@ def pure_input(world: int) -> Tuple[int, str]:


assert isinstance(IO, Functor)
assert isinstance(IO, Applicative)
assert isinstance(IO, Monad)

assert isinstance(Put, Functor)
assert isinstance(Put, Applicative)
assert isinstance(Put, Monad)

assert isinstance(Get, Functor)
assert isinstance(Get, Applicative)
assert isinstance(Get, Monad)

assert isinstance(ReadFile, Functor)
assert isinstance(ReadFile, Applicative)
assert isinstance(ReadFile, Monad)
7 changes: 4 additions & 3 deletions oslash/list.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from functools import partial, reduce

from typing import Generic, Callable, Iterator, TypeVar, Iterable, Sized, Union, Any, cast, Optional
from typing import Callable, Iterator, TypeVar, Iterable, Sized, Any, cast, Optional

from .typing import Applicative
from .typing import Functor
Expand All @@ -11,13 +11,14 @@
TResult = TypeVar("TResult")
TSelector = Callable[[TSource, Optional[Callable]], Any]

class List(Iterable[TSource]):

class List(Iterable[TSource], Sized):
"""The list monad.
Wraps an immutable list built from lambda expressions.
"""

def __init__(self, lambda_list: Optional[Callable[[TSelector], Any]]=None) -> None:
def __init__(self, lambda_list: Optional[Callable[[TSelector], Any]] = None) -> None:
"""Initialize List."""

self._value = lambda_list
Expand Down
5 changes: 3 additions & 2 deletions tests/test_ioaction.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
from typing import Tuple

import oslash.ioaction
from oslash import Put, IO, put_line, get_line
from oslash import Put, Return, put_line, get_line
from oslash.util import Unit


class MyMock:
Expand Down Expand Up @@ -33,6 +34,6 @@ def test_put_line(self) -> None:

def test_put_return(self) -> None:
pm = MyMock()
action = Put("hello, world!", IO())
action = Put("hello, world!", Return(Unit))
action()
self.assertEqual(pm.value, "hello, world!")

0 comments on commit 25b40b6

Please sign in to comment.